所谓dll:动态链接库。这本是个windows范畴的叫法。
这两天对webpack自带的DllPlugin和DllReferencePlugin两个插件做了一下研究,感觉在开发阶段的性能优化方面更适用。
这个方案的核心思想是空间换时间:先使用DllPlugin生成dll包(包含1个js和一个对应的json);在使用时,Dll User通过DllReferencePlugin调用这些资源,而不再重新编译该部分代码,从而提升编译效率和开发效率。
生成DLL
// 注意这里是多入口
entry: {
react: ['react', 'react-dom', 'react-router'],
antd: ['antd'],
},
plugins: [
new CleanWebpackPlugin(),
new DllPlugin({
// 注意这里使用了动态文件名
path: path.resolve(__dirname, '../dll/manifest_[name].json'),
name: '[name]_[chunkhash:4]',
context: __dirname,
})
]
运行后,在项目的dll目录中,产生了下面的文件:
staff 1521019 2 25 15:06 antd_bbcd.js
staff 131724 2 25 15:06 manifest_antd.json
staff 3766 2 25 15:06 manifest_react.json
staff 160646 2 25 15:06 react_e0f4.js
注意:网上DllPlugin的例子,都是生成单一vendor;而上面的例子中,通过多入口+动态文件名产生了多个dll文件。后面在引用上也会有所差别。
调用DLL
其实是有点麻烦的。在运行dll文件生成命令之后,还需要:
- 更改webpack配置,使用
DllReferencePlugin指向dll文件; - 要在HTML文件中,写入对这几个dll文件的src引用。
我的dev开发配置,是一个DllUser。以此为例:
1. 更改webpack配置
首先更改dev的webpack配置。多dll文件的情况,则使用多个DllReferencePlugin插件来引入。
注意context条目必须有,且一定要与DllPlugin配置一致。
plugins: [
new HtmlWebpackPlugin({
template: './src/client/index.html',
inject: 'body',
}),
new DllReferencePlugin({
manifest: require(path.resolve(__dirname, '../dll/manifest_react.json')),
context: __dirname,
}),
new DllReferencePlugin({
manifest: require(path.resolve(__dirname, '../dll/manifest_antd.json')),
context: __dirname,
}),
],
这个时候npm run dev是可以正常运行的,但页面控制台会报错
Uncaught ReferenceError: antd_bbcd is not defined
在控制台点开这个external文件,看到如下代码:
module.exports = antd_bbcd;
是不是豁然开朗?
2. 更改HTML模版
下面我们在HTML中引入。由于是开发环境,首先需要先配置一下dev-server,确保dll文件可以被访问到:
devServer: {
contentBase: path.join(__dirname, '../dll'),
port: 9100
}
在HTML的head中添加:
<script type="text/javascript" src="antd_bbcd.js"></script>
<script type="text/javascript" src="react_e0f4.js"></script>
然后重新npm run dev,页面可以正常展示了。
流程优化
由于我们在生成dll的时候,添加了chunkhash后缀,所以文件名很有可能发生变化。而每次变化时,都需要修改webpack配置和html模版,再加上是多个dll,所以每次变化修改都很麻烦。
由于我们生成文件,是独立存放在dll目录中的,所以可以读取这个目录,动态生成配置。
先写个读目录文件的函数:
const fs = require('fs')
const getFiles = (dir) => {
if(!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
return []
}
return fs.readdirSync(dir, 'utf-8').filter(n => !/^\./.test(n))
}
module.exports = getFiles
1. 改造webpack配置
// ...
const getFiles = require('./utils/get-files')
/**
* 多个dll时,plugins对dll加载是有顺序要求的,应当首先加载react。
* 这里使用getFiles获取到文件名称列表后,默认字母排序antd在前,所以antd的dll早于react加载,导致报错。
* 所以简单使用了reverse()来更换加载顺序。多dll还真是有点麻烦。
* @param {string} dir
* @param {string} context
*/
const getDllReferencePlugin = (dir, context = __dirname) => {
return getFiles(dir).filter(n => /\.json$/.test(n)).reverse().map(name => {
return new DllReferencePlugin({
manifest: require(path.join(dir, name)),
context,
})
})
}
module.exports = {
//...
plugins: [
new HtmlWebpackPlugin({
template: './src/client/index.html',
inject: 'body',
}),
...getDllReferencePlugin(path.resolve(__dirname, '../dll'), __dirname),
]
}
2. 使用插件动态改造HTML
在我的上一篇文章中,已经简单介绍了webpack插件的写法,同样也是在html-webpack-plugin这个插件的范畴内,对HTML进行动态改造。这里就不赘述,直接帖插件代码代码:
/*
plugins/jsdir-html-inject.js
*/
const path = require('path')
const { JSDOM } = require('jsdom')
const getFiles = require('../utils/get-files')
const getJsFiles = (dir) => {
return getFiles(dir).filter(n => /\.js$/.test(n))
}
class JSDirHtmlInject {
constructor(options) {
this.options = options || {}
}
apply(compiler) {
compiler.plugin('compilation', (compilation, options) => {
compilation.plugin('html-webpack-plugin-before-html-processing', (data) => {
const { dir, base = '.' } = this.options
if(!dir) {
return
}
const files = getJsFiles(dir)
if(files.length < 1) {
return
}
const dom = new JSDOM(data.html)
const head = dom.window.document.querySelector('head')
files.forEach(src => {
const script = dom.window.document.createElement('script')
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', path.join(base, src))
head.appendChild(script)
})
data.html = dom.serialize()
})
})
}
}
module.exports = JSDirHtmlInject
再改一下webpack配置
const JSDirHtmlInject = require('./plugins/jsdir-html-inject')
// ...
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: './src/client/index.html',
inject: 'body',
}),
new JSDirHtmlInject({
dir: path.resolve(__dirname, '../dll'),
base: '.'
}),
...getDllReferencePlugin(path.resolve(__dirname, '../dll'), __dirname),
]
}
这样,在每次开发开始之前,按需运行npm run dll即可。
从开发层面讲,开发人员对dll产出物不用关心,正常运行npm run dev即可。
貌似还挺方便的吧。
以上。