前言
前面我们对webpack的基础知识进行了详细的讲解,下面我们进入webpack的进阶教程,代码分割和按需引入。
chunks以及它引入的modules本质上是通过webpack处理module之间的父子关系所做的关联。在webpack4以前,我们使用CommonsChunkPlugin
插件来避免依赖的重复打包,但是这样很难进行更深一步的优化。
从webpack4开始,CommonsChunkPlugin
被移除,新增了optimization.splitChunks
和optimization.runtimeChunk
选项。
译:重心其实就是内置一个配置项,可以把符合指定规则的module提取为chunk,官方说新的方法贼好用(反正在我研究明白之前真没觉得好用,文档实在是不友好,有些概念不好理解)
代码分割
SplitChunksPlugin
对大多数用户来说都更加好用。
默认配置下,它只会影响 按需载入 chunks (实现方式),而不会影响到 initial chunks (html文件同步加载的script tag),因为更改initial chunks将会影响到HTML文件引入的的script tags。
webpack将会按照以下规则进行代码切割:
- 新chunk可被公用,或者是从
node_modules
引入的模块 - 新chunk大于30kb(在 打包压缩 + 服务器压缩 之前)(此条意在平衡新增一次网络请求与公共代码的长缓存的利弊,可配置)
- 对于按需加载的模块,数量不能超出5个
- 对于同步入口chunks,数量不能超出3个(不新增过多的初始网络请求数,可配置)
webpack提供一些可配置项来满足开发者更多定制化的要求。
默认的配置项在我们看来是适合浏览器表现的最好方式,但是对你的项目来说可能会有所差异。如果你尝试覆盖默认项,请确认你的这些配置对于项目来说真的有优化作用。
optimization.splitChunks
以下是SplitChunksPlugin
的默认配置:
webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 仅提取按需载入的module
minSize: 30000, // 提取出的新chunk在两次压缩(打包压缩和服务器压缩)之前要大于30kb
maxSize: 0, // 提取出的新chunk在两次压缩之前要小于多少kb,默认为0,即不做限制
minChunks: 1, // 被提取的chunk最少需要被多少chunks共同引入
maxAsyncRequests: 5, // 最大按需载入chunks提取数
maxInitialRequests: 3, // 最大初始同步chunks提取数
automaticNameDelimiter: '~', // 默认的命名规则(使用~进行连接)
name: true,
cacheGroups: { // 缓存组配置,默认有vendors和default
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
splitChunks.automaticNameDelimiter
string
webpack默认使用 origin~chunkName 的形式生成name(例如:index~detail.js
)。该选项可以自定义连接符。
splitChunks.chunks
function (chunk) | string
该选项决定优化哪些chunks。如果值为string,有三个可选项:all
、async
和initial
。
设置为all
表现会更强大,这样会使公共chunks可能同时被同步和异步引入。
webpack.config.js
module.exports = {
//...
optimization: {
splitChunks: {
// 包含所有chunks
chunks: 'all'
}
}
};
你也可以通过一个function来更精确的进行chunks控制。返回值就是需要优化的chunks。
module.exports = {
//...
optimization: {
splitChunks: {
chunks (chunk) {
// 返回所有不是`my-excluded-chunk`的chunks
return chunk.name !== 'my-excluded-chunk';
}
}
}
};
你可以配合HtmlWebpackPlugin,把提取到的公共chunks注入到html页面中(HtmlWebpackPlugin的 chunks 配置项)。
splitChunks.maxAsyncRequest
number
按需载入的最大请求数。
译注:如果按照着配置规则,最终提取出的按需载入chunks数量大于该配置项,则优先级较低的多余chunks不会被分割成公共chunks(通常是取符合数量的size较大的几个)。
splitChunks.maxIntialRequests
number
单个入口文件的最大请求数。
译注:如果按照着配置规则,最终提取出的初始载入chunks数量大于该配置项,则优先级较低的多余chunks不会被分割成公共chunks(通常是取符合数量的size较大的几个)。 需要注意的是,入口chunk + 默认缓存组的vendors配置项 + 默认缓存组的default配置项,这就已经是3个了,如果自定义的缓存组配置无效,注意查证该配置。
splitChunks.minChunks
number
提取出来的chunk最小共用数量。
splitChunks.minSize
number
提取出来的chunk需要满足的最小size,单位是bytes。
splitChunks.maxSize
number
maxSize
项(无论是全局的optimization.splitChunks.maxSize
还是每个缓存组内的optimization.splitChunks.cacheGroups[x].maxSize
,
或者缓存组回调optimization.splitChunks.fallbackCacheGroup.maxSize
)会尝试对大于该值的chunks分割为更小的几部分。
每部分需要大于minSize
的值。该算法是固定的,并且模块的改变仅会造成局部影响。所以在使用缓存组并且不需要记录时,可以使用该选项。
如果切割结果不符合minSize
要求,maxSize
不会生效。
如果该chunk已经有了一个name,分割出的每个chunk都会有一个新名字。optimization.splitChunks.hidePathInfo
项会使分割项根据原始chunk的name派生出一个hash值。
maxSize
项意在HTTP/2和长缓存中通过增加网络请求数来达到更好的缓存效果。它也可以通过减少文件体积来加快构建速度。
maxSize
比maxInitialRequest/maxAsyncRequests
有更高的优先级。优先级顺序为maxInitialRequest/maxAsyncRequests < maxSize < minSize
。
译:官方解释的比较绕,但是其实仔细读几遍,结合理解他提到的其他配置项,会觉得说的很清楚了。这个配置项默认为0,也就是不做限制。如果做了限制,并且提取出来的chunk大于该限制,那么会把这个chunk按照minSize的规则作二次拆分,并且最终拆分出的chunks数量优先级要大于按需载入/初始载入的最大限制数量。
splitChunks.name
boolean: true | function (module) | string
提取出来的chunk name。设置为true
会根据chunks和缓存组的key来自动命名。设置为string或者function来确定你想要的命名。如果name和入口文件name相同,入口文件将被移除。
module.exports = {
//...
optimization: {
splitChunks: {
name (module) {
// generate a chunk name...
return; //...
}
}
}
};
如果给不同的spilt chunk分配相同的name( 译:按照缓存组内的filename配置来理解比较好 ),所有的依赖项都将被打包进同一个公共chunk。如果你希望提取出不同的chunks,不要这么做。
splitChunks.cacheGroups
缓存组的配置项会继承splitChunks.*
的配置,但是test
,priority
和reuseExistingChunk
只能在缓存组中配置。 如果不需要默认的缓存组,设置为false
(这个很重要) 。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
default: false
}
}
}
};
splitChunks.cacheGroups.priority
number
如果可能出现一个module同时符合多个缓存组的配置规则,你需要设置优先级,该module会被提取进优先级更高的缓存组配置项内。默认有个负数的优先级。
splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk
boolean
如果当前chunk包含的模块已经打包进主bundle,将不再被打包进当前chunk。该配置项这会影响到chunk name。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
reuseExistingChunk: true
}
}
}
}
};
splitChunks.cacheGroups.{cacheGroup}.test
function (module, chunk) | RegExp | string
module的匹配规则。默认会对所有模块进行优化。你可以对module的绝对路径或者chunk name做匹配。如果匹配到一个chunk name,它的所有子模块都会被尝试提取。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test (module, chunks) {
//...
return module.type === 'javascript/auto';
}
}
}
}
}
};
splitChunks.cacheGroups.{cacheGroup}.filename
string
配置提取出的chunk name。output.filename
的命名规则在这里也适用。
该选项也可以通过
splitChunks.filename
全局配置,但是我们不建议这么做,因为如果splitChunks.chunks
没有设置为initial
,有可能引发报错。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
filename: '[name].bundle.js'
}
}
}
}
};
splitChunks.cacheGroups.{cacheGroup}.enforce
boolean: false
强制提取符合该缓存组策略的modules,并且忽略splitChunks.minSize
、splitChunks.minChunks
、splitChunks.maxAsyncRequests
和splitChunks.maxInitialRequests
配置项。
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
enforce: true
}
}
}
}
};
配置详情
了解了splitChunks(公共代码分离)的配置信息,下面我们就来在我们的项目中实践一下。
首先,查看package.json
文件,确保你的dependencies
字段内仅包含vue
,vue-router
,lodash
三个包文件。也就是
我们的项目目前只依赖这三个包文件。如果我们不做任何配置的话,webpack会把所有的文件打包进一个文件中,如下所示:
接下来,让我们配置下将lodash文件从主文件中剥离,同时vue和vue-router打包进同一文件中。
module.exports = {
optimization: {
// 分离公共chunk在vendor文件中
splitChunks: {
chunks: 'all', // 仅提取按需载入的module
minSize: 10, // 提取出的新chunk在两次压缩(打包压缩和服务器压缩)之前要大于30kb
maxSize: 0, // 提取出的新chunk在两次压缩之前要小于多少kb,默认为0,即不做限制
minChunks: 1, // 被提取的chunk最少需要被多少chunks共同引入
maxAsyncRequests: 5, // 最大按需载入chunks提取数
maxInitialRequests: 6, // 最大初始同步chunks提取数
automaticNameDelimiter: '-', // 默认的命名规则(使用~进行连接)
name: true,
cacheGroups: { // 缓存组配置,默认有vendors和default
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
lodash: {
test: /lodash/,
priority: -1,
name: 'lodash'
},
vue: {
test: /vue/,
priority: 10,
name: 'vue'
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
注意这里要修改maxInitialRequests
字段,因为我们配置了4个chunk,而该字段的默认值为3,已经超出了默认范围,所以webpack会
默认将其余的都打包进主文件app.js
中,故这里我们尽可能的加大这个数值,以防止意外发生。
我们可以发现,根据我们的配置正常生成了vue、lodash、vender等文件,符合打包预期。
注意这里的priority
选项,上面说了,这个顺序决定了打包的优先级,同时,这个值也决定了在最终生成的html文件中,
生成的js文件的引用顺序。
将打包后的文件分离,可以有效的减少最终打包文件的体积,同时,也应该考虑,增加一次网络请求与将代码放在一个文件中的 利弊,根据实际情况配置如何分离文件才能达到优化项目加载的目的。
稍等,我们不难发现,我们只用到了lodash的一个工具函数,但是webpack却帮我们把整个lodash库都打包进项目中了,这是非常没有必要的, 要是有一种方法能帮助我们只把我们用到的函数打包进项目,那项目的体积将会进一步减小,有没有这样的方法呢,答案是有的,请继续看下面的内容tree-shaking。
tree-shaking
tree-shaking是由rollup的作者首先提出的,这里有一个比喻:
如果把代码打包比作制作蛋糕。传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去。而 treeshaking 则是一开始就把有用的蛋白蛋黄放入搅拌,最后直接作出蛋糕。
因此,相比于排除不使用的代码,tree shaking 其实是找出使用的代码。
基于 ES6 的静态引用,tree shaking 通过扫描所有 ES6 的 export
,找出被 import
的内容并添加到最终代码中。 webpack 的实现是把所有 import
标记为有使用/无使用两种,在后续压缩时进行区别处理。
因为就如比喻所说,在放入烤箱(压缩混淆)前先剔除蛋壳(无使用的 import
),只放入有用的蛋白蛋黄(有使用的 import
)。
测试未tree-shaking的情况
为了测试,作者添加了测试代码如下:
export const con = () => {
console.log('我是测试分离公共模块的');
};
export const cc = () => {
console.log('测试treecc');
};
export const aa = () => {
console.log('测试treeaa');
};
export const bb = () => {
console.log('测试treebb');
};
export const dd = () => {
console.log('测试treedd');
};
并在detail.vue文件中引入了其中两个方法。
<template>
<div>
detail
</div>
</template>
<script>
import { con, cc } from '../util/dd';
export default {
name: 'detail',
methods: {
go () {
con();
cc();
}
}
};
</script>
为了能清楚的查看打包后的代码,我们暂时关闭代码压缩(即关闭tree shaking)。
module.exports = {
optimization: {
// 关闭代码压缩
minimize: false
}
}
打包查看效果:
我们不难发现,在打包后的文件中,依然存在我们没有用到的函数aa
,bb
,dd
,说明webpack没有做tree shanking,这与我们前面关闭tree shaking的配置是一致的。
我们观察生成的打包文件,会发现如下特征:
-
被使用过的
export
标记为/* harmony export ([type]) */
,其中[type]
和 webpack 内部有关,可能是binding
,immutable
等等。 -
没被使用过的
export
标记为/* unused harmony export [FuncName] */
,其中[FuncName]
为 export 的方法名称。
其实这正是,webpack为tree shaking做的准备工作,如果后续我们启用代码压缩的话,其中被标记/* unused harmony export [FuncName] */
的代码会被丢弃,只有
被标记为/* harmony export ([type]) */
的代码才会被引用并压缩。
webpack4采用terser-webpack-plugin
作为压缩工具,而webpack3采用的是UglifyJSPlugin
。
配置tree-shaking
第一步
首先必须明确的一点是:(重要的事情说三遍)
-
webpack的tree shaking是基于ES6的模块规范
import
和export
-
webpack的tree shaking是基于ES6的模块规范
import
和export
-
webpack的tree shaking是基于ES6的模块规范
import
和export
而babel的默认使用的模块规范是 CommonJS 规范,经过babel转义的代码都会变成require
的形式,所以我们需要告诉babel不要这样转换,在.babelrc中添加"modules": false
配置。
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
经过作者测试,在webpack4和babel7中即使不设置"modules": false
,也可以正常tree shaking,目前还不知道原因。
第二步
webpack 4 在 package.json
新增了一个配置项叫做 sideEffects
, 值为 false
表示整个包都没有副作用;
或者是一个数组列出有副作用的模块
{
"sideEffects": "['./src/util/cc.js']"
}
我们可以根据项目的需求,根据实际情况进行设置,这里我们设置为false
{
"sideEffects": false
}
第三步
接下来就需要配置webpack了,我们已经知道了webpack将使用的和未使用的代码做了不同的标记,接下来就需要挑出有用的代码了,这里就需要压缩工具的帮助了,压缩工具 帮助我们压缩代码的同时,丢弃掉未使用的代码,生成最后的文件,这样便实现了tree shaking。
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
// 配置tree shaking
usedExports: true,
// 开启压缩
minimize: true,
// 压缩工具
minimizer: [new TerserPlugin()]
}
}
好啦,配置完成,打包试试看:
哎呀,怎么没有了css样式,而且css文件也没有生成,这是怎么回事,
原来,webpack认为利用import
导入的文件,如果没有被使用的话,他会认为该引用是无用的,无情丢弃,也就造成了最后打包的文件没有css代码的问题。
解决这个问题,我们只需要告诉webpack样式文件是有副作用的,不能丢掉,需要在webpack的配合文件处添加如下语句:
module.exports = {
module: {
rules: [{
test: /\.css$/,
sideEffects: true,
},{
test: /\.s[ac]ss$/,
sideEffects: true,
}]
}
}
所有涉及样式文件的规则都需要添加sideEffects: true
,表示这类文件是有副作用的。
接下来,再让我们打包看下:
运行一切正常,那tree shaking生效了吗?
很高兴,打包后的项目我们只能找到引入的两个函数的打印语句,其他未使用的方法都没有包含在生产的文件中,这说明,tree shaking生效了。
总结
为了更好的达到tree shaking的效果,我们需要:
-
使用 ES6 模块语法编写代码
-
工具类函数尽量以单独的形式输出,不要集中成一个对象或者类
-
声明 sideEffects
-
注意样式文件是具有副作用的
至此,我们已经完成了完整的vue项目的webpack配置
真正的自己摆脱脚手架工具的帮助,自己配置一个vue项目,你会从中学习到很多东西,对于webpack的认识也会更近一步,妈妈再也不怕我不会自己配置webpack了。