一个上了 antd、react 全家桶、moment、lodash 的中台项目,改一行业务代码,npm run build 要跑一分多钟。看构建日志会发现,绝大部分时间花在了那些一个月都不会动一次的第三方库上,每次都要重新解析、转译、压缩一遍。
DllPlugin 要解决的就是这件事:把第三方库提前单独打一次包,之后的每次构建直接引用现成的产物,跳过整个编译过程。这篇把原理、三步接入、踩过的坑讲一遍,最后说清楚 webpack 5 上为什么不再推荐这套做法,以及该换成什么。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 为什么
CommonsChunkPlugin分了 vendor,改业务代码 vendor 的 hash 还是会变 - dll 的思路是从哪来的,
manifest.json里存的到底是什么 - 三步接入 DllPlugin 和 DllReferencePlugin,以及每一步的坑
- happypack 多线程打包怎么配,什么时候是负优化
- webpack 5 用
cache: { type: 'filesystem' }替代 dll 的完整写法和迁移思路
# 一、webpack 的 dll 功能
下面这套配置基于 webpack3 构建,webpack 4 上写法基本一致,webpack 5 的情况放在最后单独讲。
# 1.1 dll 介绍
我们构建前端项目的时候,往往希望第三方库(vendors)和自己写的代码可以分开打包,因为第三方库往往不需要经常打包更新。对此 Webpack 的文档建议用 CommonsChunkPlugin 来单独打包第三方库。
这里要先分清两件事,很多人一开始会混。
我们这里的 dll.js 是提前打包好了的,而不是在每次 build 的时候去打包输出的,这样才能做到依赖包一次构建、无限次使用。另外,webpack 输出的文件名都带有 hash 值,而用 dll 构建后输出的文件名是固定的。
先看 CommonsChunkPlugin 那种做法长什么样:
entry: {
vendor: ["jquery", "other-lib"],
app: "./entry"
}
new CommonsChunkPlugin({
name: "vendor",
// filename: "vendor.js"
// (Give the chunk a different name)
minChunks: Infinity,
// (with more entries, this ensures that no other module
// goes into the vendor chunk)
})
这份配置确实把第三方库单独拆成了一个 vendor chunk,看起来目的达到了。但它有个致命问题。
通常为了对抗缓存,我们会给输出文件的文件名中加入 hash 后缀。问题就出在这里,我们编辑了 app 部分的代码后重新打包,会发现 vendor 的 hash 也跟着变了。

这么一来,每次发布版本的时候 vendor 代码都要刷新,即使我并没有修改其中的代码。这样并不符合我们分开打包的初衷。
那为什么改业务代码会让 vendor 改名呢?两个原因叠在一起。一是 [hash] 是整次构建的哈希,只要有任何文件变了,所有产物一起改名,得换成 [chunkhash] 才是按 chunk 内容算;二是 webpack 的运行时代码里存着模块 id 到 chunk 的映射表,业务代码一变这张表就变,而这段 runtime 默认是塞在 vendor 里的。所以就算换了 [chunkhash],不把 runtime 单独提出去,vendor 照样会变。
dll 走的是另一条路,它干脆让 vendor 不参与每次构建。
Dll 这个概念应该是借鉴了 Windows 系统的 dll。一个 dll 包,就是一个纯纯的依赖库,它本身不能运行,是用来给你的 app 引用的。打包 dll 的时候,Webpack 会将所有包含的库做一个索引,写在一个 manifest 文件中,而引用 dll 的代码(dll user)在打包的时候,只需要读取这个 manifest 文件就可以了。
优势
Dll打包以后是独立存在的,只要其包含的库没有增减、升级,hash也不会变化,因此线上的dll代码不需要随着版本发布频繁更新App部分代码修改后,只需要编译app部分的代码,dll部分,只要包含的库没有增减、升级,就不需要重新打包。这样也大大提高了每次编译的速度- 假设你有多个项目,使用了相同的一些依赖库,它们就可以共用一个
dll
第三条在多项目的团队里特别香。几个中台系统用同一套技术栈,dll 打一份放 CDN,所有项目共用,用户从第二个系统开始就是直接命中缓存。
# 1.2 dll 使用
整个流程分三步,先建一个 dll 的配置文件(entry 只包含第三方库),再加一条构建命令,最后在业务配置里把 dll 关联进来。
第一步:新建 webpack.dll.conf.js
webpack.DllPlugin 的选项中,path 是 manifest 文件的输出路径,name 是 dll 暴露的对象名,要跟 output.library 保持一致。这两个对不上的话,运行时会直接抛 undefined,而且报错信息完全看不出原因,这个坑我踩过。
// build/webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 把这些资源打包成dll,提高编译速度
react: ['react','react-router-dom','redux','redux-immutable','immutable','react-redux','react-router','redux-logger','redux-thunk','styled-components'],
ui: ['antd-mobile','antd'],
others: ['react-icons','axios','clipboard','humps','lodash','md5','moment','normalizr']
},
output: {
path: path.resolve(__dirname, "../dist/static/js"),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '../dist/static/js/[name].manifest.json'),
name: '[name]_library'
}),
new webpack.optimize.UglifyJsPlugin()
]
}
这里说明一下,我原来的笔记在 plugins 里写的是 DllReferencePlugin 加一段 Object.keys(['react','ui','others']).map(...),那是错的。DllReferencePlugin 是给业务配置用的,dll 配置里该用 DllPlugin;而 Object.keys 作用在数组上返回的是 ['0','1','2'] 这样的下标,不是包名。上面已经改成正确写法了。
entry 按「更新频率」分组是个好习惯。React 全家桶、UI 库、工具库各一组,某个库升级时只需要重打对应那一组,不用全量重来。
第二步:加一个命令
// package.json
"scripts": {
"dll": "webpack --config config/webpack.dll.conf.js"
}
执行 npm run dll。
运行 Webpack 之后会输出两类文件,一类是打包好的 [name].dll.js,一类是对应的 manifest.json,长这样:
{
"name": "vendor_ac51ba426d4f259b8b18",
"content": {
"./node_modules/antd/dist/antd.js": 1,
"./node_modules/react/react.js": 2,
"./node_modules/react/lib/React.js": 3,
"./node_modules/react/node_modules/object-assign/index.js": 4,
"./node_modules/react/lib/ReactChildren.js": 5,
"./node_modules/react/lib/PooledClass.js": 6,
"./node_modules/react/lib/reactProdInvariant.js": 7,
"./node_modules/fbjs/lib/invariant.js": 8,
"./node_modules/react/lib/ReactElement.js": 9,
............
Webpack 将每个库都进行了编号索引,之后的 dll user 可以读取这个文件,直接用 id 来引用。

理解这张表是理解 dll 的关键。业务代码构建时遇到 import React from 'react',webpack 会先去 manifest 里查这个路径在不在,在的话就不再解析这个模块,直接生成一句「去全局变量 react_library 里取 id 为 2 的那个模块」。整个解析、转译、压缩的过程都跳过了,省下来的就是这部分时间。
第三步:在 plugins 中增加配置
// build/webpack.prod.conf.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('../dll/react-manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: require('../dll/ui-manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: require('../dll/others-manifest.json')
})
]
}
有几个 dll 分组就 new 几个 DllReferencePlugin,一个都不能漏,漏了那一组就会被重新编译进业务包。
还有一步文章里容易漏掉:dll 产出的 [name].dll.js 不会自动出现在页面上,得自己在 HTML 里用 <script> 引入,而且要排在业务脚本前面。用了 html-webpack-plugin 的话,可以配合 add-asset-html-webpack-plugin 自动注入。忘了这一步的表现是构建成功但页面一片空白,控制台报某个全局变量 undefined。
再次执行 npm run build。
之前

之后

时间降下来了,降幅取决于你的第三方依赖有多重,依赖越重收益越大。