环境分离与代码分离
环境分离
我们在webpack.config.js中做了很多配置,但是某些配置需要在开发环境中使用、某些需要在生产环境中使用、也有一些配置在开发环境与生产环境都需要使用。
所以我们最好做一个环境分离,对配置做一个划分,方便我们维护和管理。
方案一:
编写两个不同的配置文件,开发和生产时,分别加载不同的配置文件即可。
//package.json
"scripts": {
"build": "webpack --config ./config/webpack.prod.js",
"serve": "webpack serve --config ./config/webpack.dev.js",
},
方案二:(推荐)
使用相同的一个入口配置文件,通过设置参数来区分它们;(推荐)
目录结构如下:
启动命令配置:
//package.json
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production",
"serve": "webpack serve --config ./config/webpack.common.js --env development"
},
使用webpack.common.js作为入口,入口文件相同。然后根据传入的env参数,区分开不同的环境。
拼接config配置
有了env参数区分环境,就可以根据不同的环境,对当前环境的config配置与公共config配置进行拼接。
安装webpack-merge合并config:
npm install webpack-merge -D
使用:
//webpack.common.js
const { merge } = require("webpack-merge");
module.exports = function(env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production": "development";
//prodConfig为生产环境的config,devConfig为开发环境的config,commonConfig为公共的config
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig, config);
return mergeConfig;
};
我们通过以上配置就实现了环境的分离,以及根据当前环境进行配置的合并。
babel的环境分离
但是有时候在babel.config.js里,也需要根据当前环境判断是否需要某些配置,我们如何判断呢?
//bable.config.js
if (process.env.NODE_ENV === 'development') {
config.plugins.push(require.resolve('react-refresh/babel'))
}
使用process.env.NODE_ENV就可以从进程里拿到当前环境。
要想拿到process.env.NODE_ENV,需要我们在上边的webpack.common.js中通过拿到的当前环境env参数后对process.env.NODE_ENV进行手动赋值。
或者设置webpack的mode属性,也是可以拿到process.env.NODE_ENV。
我们在之前学习mode属性时,也了解到了mode的值会自动给process.env.NODE_ENV设置值。
代码分离(Code Splitting)
为什么需要用到代码分离?
我们试想一下,如果把通过编译打包后,所有的js文件都打包到了一个bundle.js中(包含业务代码、第三方依赖、暂时没用到的模块)。
那浏览器在加载首页时需要下载解析这个bundle.js,那就太影响首页的加载速度了,很影响用户体验。
如果有了代码分离,我们的代码给分离到不同的bundle.js中,我们就可以按需加载或者并行加载这些js文件了。
通过我们的代码分离配置,在真实开发中一般会分离出这几个js文件:
- 同步导入:main.bundle.js(业务代码)
- vendor_chunks.js(所有第三方库)
- common_chunks.js(在多入口中多次引用的代码,比较少见)
- runtime.js;
- 异步导入的话每一个模块的异步导入都对应一个js文件
接下来我们看看代码分离的实现方式。
webpack中常用的代码分离的三种方式:
- 入口起点:使用entry配置多入口,手动实现代码分离;(现在项目大多都为单入口)(不推荐)
- SplitChunks Plugin:SplitChunksPlugin去重和分离代码;(主要用于分离第三方库或在多入口中多次导入的代码)
- 动态导入:通过模块的内联函数调用分离代码。(比如路由懒加载)
方式一:入口起点
实际上,就是在entry配置多个入口,手动实现代码分离。每个入口都会分离出一个bundle文件。
//webpack.config.js
entry: {
main: "./src/main.js",
index: "./src/index.js"
},
output: {
path: path.resolve(__dirname, "./build"),
//每个文件都有对应的name
filename: "[name].bundle.js",
},
缺点:假如我们在两个入口文件,都依赖了同一个第三方库(比如lodash),那打包出来的两个bundle文件里都各自有一份lodash代码。这就很影响性能了。
解决方案:使用Entry Dependencies(入口依赖)。
Entry Dependencies(入口依赖):
//webpack.config.js
entry: {
// main: "./src/main.js",
// index: "./src/index.js"
// 更改为这种写法,dependOn传入共同的第三方依赖
main: { import: "./src/main.js", dependOn: "shared" },
index: { import: "./src/index.js", dependOn: "shared" },
//自定义依赖名称,作为dependOn的value。也可以是数组
// lodash: "lodash",
// dayjs: "dayjs"
shared: ["lodash", "dayjs"]
}
入口依赖其实还是需要手动配置每一个共有的第三方依赖。这里我们了解下即可。(“放弃抢救”,换一种代码分离方式。下一个更乖~)
方式二. SplitChunks Plugin
无需手动安装:该插件webpack已经默认安装和集成。
splitChunks插件会根据规则帮我们进行代码分离。
我们直接在splitChunks下配置我们需要的规则即可。
//webpack.config.js
optimization: {
splitChunks: {
// chunks代码分离规则:async对异步导入进行分离(默认)、initial只对同步导入、all 异步/同步导入都进行分离
chunks: "all",
// 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize。20000约为20kb
minSize: 20000,
// 将大于maxSize的包, 拆分成不小于minSize的包
maxSize: 20000,
// minChunks表示引入的包, 至少在多个entry入口以其依赖被导入了几次(默认 1 )
// 满足条件就打包到 common_chunks里
minChunks: 1,
//缓存组,其中的每一项缓存组都可以继承/覆盖之前提到的 splitChunks 参数值。
//缓存组匹配到的所有第三方依赖都统一打包到一起
cacheGroups: {
vendors: {
//test:如果在node_modules文件夹里能找到(默认为所有modules)
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js",
//name与filename作用相同,区别在于filename可以添加[placeholder]
// name: "vendor-chunks.js",
//priority:优先级,大的优先
priority: -10
},
default: {
//minChunks:在多个入口中,被导入两次就分割(默认为 2 )
minChunks: 2,
filename: "common_[id].js",
priority: -20,
//reuseExistingChunk:使用已执行打包的代码,不再重复打包(默认true)
reuseExistingChunk: true
}
}
},
},
在实际的开发中,常见的配置其实只有chunks属性与cacheGroups属性。其它配置采用webpack的默认值即可(这么多配置了解下干嘛用的即可)
我们也可以一起看一下React脚手架和vue3脚手架都做了哪些splitChunks配置。
React脚手架关于splitChunks的配置:
Vue3脚手架关于splitChunks的配置:
看完我们发现,脚手架里边只配置了chunks或cacheGroups。(其他都是采用默认的)。
确实跟刚才说的那样,不用配置太多属性,使用默认即可。最多配置一下chunks与cacheGroups。
方式三:动态导入
使用动态导入的模块,webpack都会在打包时分离出单独的js文件。
为什么需要用到动态导入?
有时候,我们并不确定某个模块的代码一定会用到(只有在符合条件的情况下才用到),所以就把这部分模块拆分成一个独立的js文件。
这样就可以保证不会用到该模块时,浏览器不需要加载和处理该模块js代码。提高性能。
webpack提供了两种实现动态导入的方式:
第一种:使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
import(/* webpackChunkName: "foo" */"./foo").then(res => {
console.log(res);
});
第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;
动态导入的文件命名
因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
默认情况下,动态导入的文件命名规则是根据output里配置的fileName的。
但是这不符合我们的需要,我们想要的是动态导入的文件命名规则跟fileName的规则不同,这样我们才能很好区分出哪些文件是动态导入的。
使用chunkFilename给动态导入分离出来的文件命名:
//webpack.config.js
output: {
path: resolveApp("./build"),
filename: "[name].bundle.js",
//chunkFilename:配置动态导入分割的文件命名规则
chunkFilename: "[name].chunk.js"
},
[name]是每一个文件对应的name,不配置的话是一个数字(chunkIds的默认规则)。
我们可以使用optimization.chunkIds配置name的规则。(这里了解就行,实例开发中使用默认值即可,一般不会手动配置chunkIds,)
//webpack.config.js
optimization: {
// natural: 使用自然数(不推荐),
// named: 使用包所在目录作为name(在开发环境下的默认值)
// deterministic: 生成id, 针对相同文件生成的id是不变(在生产环境的默认值)
chunkIds: "deterministic",
}
对于动态导入的文件,按照上边说的,name默认跟id一致。但是我们其实还可以用另一种方式修改name,而且其优先级是比chunkIds高的。
使用magic comments(魔法注释)的方式:
// magic comments
import(/* webpackChunkName: "foo" */"./foo").then(res => {
console.log(res);
});
这个magic comments怎么长得这么眼熟呢?因为在之前学习vue-router中用到过。
下边是在vue-router官方网站看到的:
现在才发现,原来在之前学习vue-router的时候就接触过动态导入跟magic comments了。
vue-router的路由懒加载的原理,其实就是我们的代码分离的动态导入方式。
动态导入优化:Prefetch和Preload
上边我们学习到了,异步导入后webpack自动进行的代码分离,其实就是懒加载的原理。
只有用到了异步导入那个模块,浏览器才会去下载、执行当前异步导入模块的js文件。
但是我们的懒加载还是有优化的空间。
假如说我们要进行懒加载的模块,如果逻辑很复杂文件很大,那我们每次使用的时候才去下载并执行,岂不是也有可能花费很多时间。影响用户体验。
如果可以在浏览器空闲的时候,就对我们懒加载的模块的js文件进行下载,等到使用到该模块直接拿来执行就好了。
Webpack考虑到了这些给出了解决方案:使用魔法注释的prefetch或preload。
webpackPrefetch(预获取):告诉浏览器有空闲时先下载我这个异步导入的js文件
webpackPreload(预加载):告诉浏览器在下载当前父chunk时并行下载我这个异步导入的js文件
//魔法注释之webpackPrefetch:(预获取)告诉浏览器有空闲时先下载我这个异步导入的js文件
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
"./element"
).then((res) => {
console.log(res);
})
//魔法注释之webpackPreload:(预加载)告诉浏览器在下载当前父chunk时并行下载我这个异步导入的js文件
import(
/* webpackChunkName: 'element' */
/* webpackPreload: true */
"./element"
).then((res) => {
console.log(res);
})
二选一即可,一般使用Prefetch。
因为preload在下载父chunk时就并行下载懒加载的js文件,有些违背我们使用懒加载的初衷(减少首页的加载速度)。虽然preload是并行下载,但是也会给浏览器增加负担。
runtime分离
什么是runtime?
在运行环境中,对模块进行解析、加载、模块信息等都会生成相关的代码;这些代码就是runtime代码。
默认情况下,runtime代码会被打包到main.bundle.js。
如果我们要对runtime进行额外的代码分离处理,就配置下Optimization.runtimeChunk即可。
//webpack.config.js
optimization{
//runtimeChunk:默认把runtime代码都打包到main.bundle.js里(每个入口分离出来的主文件里)
//multiple(多入口情况下,每个入口对应的runtime各自分离到各自的runtime文件下)
//single(多入口的情况下,所有runtime也是分离到同一个runtime文件)
//object(接受一个对象,可以在对象里设置name)
runtimeChunk: {
//name:可以是字符串,也可以是函数(入口文件作为参数)
//name:'runtime'
name: function(entrypoint) {
return `${entrypoint.name}`
}
}
CSS分离
在最开始的loader的学习中,我们知道了怎么去使用style-loader与css-loader去处理我们项目里的css代码。
这里我们稍微回顾一下,css-loader负责将css代码转化为js代码,而style-loader负责将转化后的代码绑定到js文件上。在js文件里将css代码写在style标签里。
那我们要是不想要把css文件全都放到打包出来的js文件里,有什么好办法吗?
MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中。
(该插件需要在webpack4+才可以使用。)
安装:
npm install mini-css-extract-plugin -D
使用:
//webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');、
module: {
rules: [
{
test: /\.css/i,
// MiniCssExtractPlugin.loader替代style-lodader
use: [
MiniCssExtractPlugin.loader,
"css-loader"
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
fileName: "css/[name].css"
})
],
注意点:使用 MiniCssExtractPlugin.loader替代style-loader