前言

持续开发和部署 WEB 项目时,对于引入的 script 脚步文件的缓存控制是一个比较重要的话题,通常使用 webpack 打包用于生产环境 js 文件时都在文件名中包含 chunkhash 值,即要求改动准确代表对应文件名变动,这样用户总是会获得最新的代码以获得正确的功能并也能利用浏览器缓存提高体验。

webpack dll

所有 js 代码,其必然包含两个部分:

    1. 引入的第三方包,如 react/jquery 等,这部分代码较少变动,与业务无关
    1. 编写的业务代码,使用大量第三方工具库,这部分经常变动,代表了业务的迭代

通常应该将第一部分单独打包,命名为 venders,第二部分根据实际情况分为一到多个文件。现在我们使用 webpack 提供的 dll 插件来实现这一点。

首先建立一个 webpack.dll.config.js 文件,专门来生成 venders 文件的:

import path from "path";
import webpack from "webpack";

export default {
    entry: {
        vendors: [
            "react",
            "react-dom",
            "react-router",
            "react-router-redux",
            "babel-polyfill",
            "redux",
            "redux-thunk",
            "react-redux",
            "axios",
            "classnames"
        ]
    },
    output: {
        path: path.resolve(__dirname, "./dist"),
        publicPath: "./",
        filename: "[name].[chunkhash:8].js",
        library: "[name]_[chunkhash:8]"
    },
    plugins: [
        new webpack.DllPlugin({
            context: __dirname,
            path: "manifest.json",
            name: "[name]_[chunkhash:8]"
        })
    ]
};

上述配置文件中,我们声明了这个 vendors 文件包含 react 等第三方包,请注意 output.library 要保持和 dllplugin 中的 name 参数一致。

然后在我们正常的 webpack.config.js 文件中,plugins 部分添加一条记录:

new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require("./manifest.json")
}),

mainfest.json 文件是由 webpack.dll.config.js 配置下运行 webpack 生成的配置文件,包含所有引用的第三方包内引入的模块文件,并为他们赋予了唯一的数字 id。

这时通过修改业务代码,生成的 vendors 文件的 hash 后缀是不变的。 webpack-dll

webpack CommonsChunkPlugin

实际上,在我们自己的业务中,也存在一些不同页面复用的模块,如业务相关的一些 utils,如果它们有改动,那将影响所有引用了该模块的 chunk 的 hash 值。那么可以将这些公用的代码继续合并并单独分离为 common 代码,可以在 webpack.config 中 plugins 项添加一条记录:

new webpack.optimize.CommonsChunkPlugin({
    name: "common"
})

但是这时会发现不论修改了哪一部分代码,都会影响 common 文件的 chunkhash 值。

common

这是因为因为 webpack 的 runtime 代码也被作为公共代码引入到 common chunk 中了,而这一部分代码包含了所有 chunk 的 chunkhash 内容在其中。要解决这个问题,我们需要精准控制 common chunk 的内容。现在新添加一条记录:

new webpack.optimize.CommonsChunkPlugin({
    name: "common",
    minChunks: ({ resource } = {}) => {
        return (
            resource && /utils\/([0-9a-zA-Z_-]+)\.js/i.test(resource)
        );
    }
})

并将上一次添加的 common 记录修改为:

new webpack.optimize.CommonsChunkPlugin({
    name: "manifest"
})

添加 manifest chunk 是用于 webpack runtime 代码存放的,否则它仍然会放入最后一个可用的 bundle 代码里面,这里即是生成的 common 文件。

common2

webpack module id

现在我们在代码中引入一个新模块,继续编译代码:

import-new

虽然我只修改了 pageA chunk 的引入,但是 pageB chunk 的 hash 也发生了变化,这个变化不是准确对应 chunk 文件变化的。

打开 pageB.f51b42fd.js 文件,有一部分代码如下:

var _react = __webpack_require__(0);

var _react2 = _interopRequireDefault(_react);

var _modB = __webpack_require__(7);

var modB = _interopRequireWildcard(_modB);

var _utils = __webpack_require__(2);

而在之前,它们是

var _react = __webpack_require__(0);

var _react2 = _interopRequireDefault(_react);

var _modB = __webpack_require__(6);

var modB = _interopRequireWildcard(_modB);

var _utils = __webpack_require__(2);

明显发现_modB 的引入 id 由 6 变成了 7,这个 id 就是 webpack 为每个模块赋予的数字 id,因为我们引入了新的模块,而 webpack 对模块编号是按照编译时引入顺序的,因此新模块可能占用了老模块的 id。要弥补这一点,我们可以使用另一种模块 id 模式,比如 webpack HashedModuleIdsPlugin 插件,基于模块路径 hash 的 id。现在在 webpack.config.js 的 plugins 项中添加一条记录:

new webpack.HashedModuleIdsPlugin()

继续重复上面添加新模块前后对比操作: path-hash-module-id

但是问题依旧,对比了一下 pageB 生成文件前后代码,虽然 HashedModuleIdsPlugin 已经起到作用保证已有模块 id 不变,但是仍然不能阻止 pageB 的 chunkhash 变化,这是因为在 config.entry 中定义的 pageA,pageB 两个 chunk 仍然使用的是数字 id,而且它们的顺序在引用新模块前后发生了变化,从上面的图片中可以看到它们对调了顺序。

虽然 pageB 的实际代码未变,但是因为每个 entry chunk 代码的开头,都会添加自己本身的 chunk id,所以 pageA/B 排序变化了,chunk id 同样变化,那么 chunkhash 还是要变化,下图中数字 0 即是 pageB entry 的 chunk id,而实际按 config.entry 它应该是 1。 entry-chunk-id

为了解决这个问题,研究了一下 webpack 插件机制,写了一个插件WebpackStableChunkId,目的是要求 entry chunks 的排序要保持和 config.entry 中原始定义的顺序一致,这样最终就保持了 chunkhash 的稳定。

再次重复上述引用包前后编译对比: chunk-ordered

已经达到要求,entry chunks 的排序保持了,pageB 在未有代码改动的情况下 chunk hash 也不会改变了。