Webpack hash控制

前言

持续开发和部署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也不会改变了。