Webpack4散记(2)dll

595 阅读3分钟

所谓dll:动态链接库。这本是个windows范畴的叫法。

这两天对webpack自带的DllPluginDllReferencePlugin两个插件做了一下研究,感觉在开发阶段的性能优化方面更适用。

这个方案的核心思想是空间换时间:先使用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文件生成命令之后,还需要:

  1. 更改webpack配置,使用DllReferencePlugin指向dll文件;
  2. 要在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即可。

貌似还挺方便的吧。

以上。