Webpack 配置优化
一.构建流程
你好!在配置webpack之前先了解一下webpack的构建流程,这有助于你对webpack更好的理解,同时这也是面试中经常问的问题。下面对构建流程先做个简单的介绍。
1.启动(shell 与 config 解析)
每次在命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack 这个 shell 脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js (webpack.js 是 webpack 的启动文件)。
在 webpack.js 这个文件中 webpack 通过 optimist (命令行解析库-实现了node命令行的解析))将用户配置的 webpack.config.js (vue.congfig.js)和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。options对象如下图所示:
Plugins和loader的区别(又一道经典的面试题):
webpack默认只能解析.js的文件,而.vue、.css、.png、.ts等是不能解析的,所以他要借助各种loader帮助自己解析这些文件,所以loader是起到一个辅助性的作用,而plugins是功能性作用,webpack借助各种插件实现代码的打包、封装与抽离,这是二者的主要区别;
2. 编译与构建主流程
在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 lib/webpack.js 中
webpack 的实际入口是 Compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点
1)compile 开始编译;
2)make 从入口点分析模块及其依赖的模块,创建这些模块对象;
3)build-module 构建模块;
4)after-compile 完成构建;
5)seal 封装构建结果;
6)emit 把各个chunk输出到结果文件;
7)after-emit 完成输出;
1)编译-核心对象 Compilation
compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象:
这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可以从图中看到比较关键的步骤,如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息
2)编译与构建主流程
在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法,通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
而构建模块作为最耗时的一步,又可细化为三步:
调用各 loader 处理模块之间的依赖
webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如 url-loader ,jsx-loader , css-loader 等等来让我们可以直接在源文件中引用各类资源。webpack 调用 doBuild() ,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。
loader 处理后的源文件生成抽象语法树 AST;
遍历 AST,构建该模块所依赖的模块
对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。
3)打包输出(重点环节,所有的优化配置都在这一环节实现)
在所有模块及其依赖模块 build 完成后,webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。
最后一步,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。
二.可优化配置(常用)
1.TreeShaking(摇晃树)
作用:当我们在组件中通过import 导入一个方法时,而在组件内部并没有使用这个方法,在打包时也会把该方法打包到文件中,这就增加了代码的体积,而TreeShaking就是解决这个问题的。配置如下:
主要有两点:
1.只针对于import导入的模块
2.在package.json中配置sideEffects属性
{
"name": "testconfig",
"version": "0.1.0",
"private": true,
//sideEffects可以为boolean类型,也可以为数组
"sideEffects":true,// 默认为true,不摇晃,false:全部摇晃掉`在这里插入代码片`
"sideEffects":['@/utils/common.js','*.css'] //指定哪些文件不被摇晃
//当我们在组件内部import './common.css' 时,如果直接设置成false,打包时它会把css文件也摇晃掉,这时就需要把sideEffects配置成数组的格式
.}
2.optimization(最优化配置)
先说一下configureWebpack和chainWebpack的区别:
chainWebpack:配置的内容可以修改webpack的默认配置;
configureWebpack:配置的内容最终会和webpack的默认配置进行合并
...
configureWebpack: (config) => {
...
config.optimization = {
splitChunks: {
chunks: 'all',
maxInitialRequests: 3, // 默认
cacheGroups: {
utils: {
name: 'chunk-utils',
test: /[\\/]utils[\\/]/,
// chunks: "all",//默认
// minChunks: 1,//最少被引用1次,才进行抽离
// minSize: 0,//包超过这个体积才进行抽离
priority: 1,
reuseExistingChunk: true,
enforce: true
},
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 2,
// reuseExistingChunk: true,
enforce: true
}
}
}
};
cacheGroups中默认有两个属性,一个是vendors,一个是default,test: 表示要过滤 node_modules,所有node_modules下的包都打到vendors里,而剩下的打包到默认的app.js中,显然这不符合我们优化的需求,而cacheGroups可以帮助我们对代码做进一步抽离。
1.utils(自定义命名):项目开发中的工具类库,可以抽离出来单独打一个包
2.maxInitialRequests: 3, // 默认,,表示首页面最多请求3次,所以就把app.js分了3个出来, 当我们配置cacheGroups抽离超过3个包时,maxInitialRequests这个属性也得相应的去增大
3.test: 正则匹配路径,
4.priority:权重,数字越大表示优先级越高。
5.reuseExistingChunk:表示是否使用已有的 chunk,true 则表示如果当前的 chunk 包含的模块已经被抽取出去了,那么将不会重新生成新的,即几个chunk复用被拆分出去的一个module;
6.enforce:设置了为true,匹配的模块就会忽略前面提到的那几个属性(注释的属性)
3.externals
作用:从打包的bundle文件中排除依赖。换句话说就是让在项目中通过import引入的依赖在打包的时候不会打包到bundle包中去,而是通过cdn的方式去访问这些依赖。
具体配置如下图
1.vue.config.js中
2.main.js中(注释vue和element-ui主要是为了看是否生效(可以不注释))
3.public/index.html (CDN引入)
4.dll(动态链接库)
通常来说,我们的代码都可以简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码,大大提升了项目的访问速度与打包速度,提升了性能。
1.在根目录下创建dll.config.js(自定义文件名)文件,内容如图所示
entry中配置要独立出来的第三方依赖库
2.在package.json新增一条命令dll,然后在终端中执行npm run dll,它会提示让你安装webpack-cli,安装成功后,再一次执行npm run dll,它会生成两个文件vueSource-manifest.json和vueSource.js
2.然后在vue.config.js中如下图进行配置即可
在打包过程中,它会通过dllReferencePlugin找到vueSource-manifest.json文件(先去动态链接库中去查找),判断里面是否有依赖的包,如果有,就跳过,不进行打包,如果没有才进行打包。
3.在public/index.html文件中手动引入vueSource.js
5.externals与dll对比
共同点:都是对依赖的第三方包进行进一步抽离(分离模块),在本地启用服务时,大大缩短了启用时间,相当于只对业务逻辑代码进行打包
不同点:
externals防止将某些 import 的包(package)打包到 bundle 中,而是在运行时再去从外部获取这些扩展依赖(cdn的方式引入)。简单讲,就是把我们引入的三方模块声明为外部依赖,webpack不会对其进行打包,最后我们在打包的项目中通过script外链引入。
dll:将项目依赖的基础模块(第三方模块)抽离出来,然后打包到一个个单独的动态链接库中。当下一次打包时,通过webpackReferencePlugin,如果打包过程中发现需要导入的模块存在于某个动态链接库中,就不能再次被打包,而是去动态链接库中get到。简单讲,就是将第三依赖单独打包,通过引用 manifest.json来把依赖映射到模块上,相当于把原本的一个文件拆分成多个。这样做有三个优点
1.异步加载文件,减少了页面的白屏时间
2.Dll打包以后是独立存在的,只要其包含的库没有修改,其hash也不会变化,因此线上的dll文件基本不需要随着频繁更新。
3.如果多个项目使用了相同的依赖库,可以共用一个文件。
总结:dll配置繁琐一点,但更智能,externals需要cdn引入。
6.extensions,mainFiles,alias配置
1.extensions:指定extension之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加的扩展名顺序进行匹配
2.alias:配置别名可以加快webpack查找模块的速度,每当引入模块的时候,它会直接引入,而不需要从node_modules文件夹中按模块的查找规则查找或者按照指定路径查找;
3.mainFiles:配置之后它会优先使用文件夹的child.js这个文件,如果没有child.js文件,在查找有没有index.js文件,会根据mainFiles配置的数组顺序依次查找,直到找到为止。