前言
如何基于webpack做持久化缓存目前感觉是一直没有一个非常好的方案来实践。网上的文章非常多,但是真的有用的非常少,并没有一些真正深入研究和总结的文章。现在依托于于早教宝线上项目和自己的实践,有了一个完整的方案。
正文
1、webpack的hash的两种计算方式
想要做持久化缓存那么就要依赖 webpack
自身提供的两个 hash
:hash
和chunkhash
。
接着就来看看这两个值之间的具体含义和差别吧:
hash
: webpack
在每一次构建的时候都会产生一个compilation
对象,这个hash
值就是根据compilation
内所有的内容计算而来的值。
chunkhash
:这个值是根据每个chunk
的内容而计算出来的值。
所以单纯根据上面的描述来说,chunkhash
是用来做持久化缓存最有效的。
2、hash和chunkhash的测试
entry | 入口文件 | 入口依赖 |
---|---|---|
pageA | a.js | a.less->a.css, common.js->common.css |
pageB | b.js | b.less->b.css, common.js->common.css |
- 使用hash计算
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: {
pageA: './src/a.js',
pageB: './src/b.js'
},
output: {
filename: '[name]-[hash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?minimize']
})
}
]
},
plugins: [new ExtractTextPlugin('[name]-[hash].css')]
}
构建结果
Hash: 80c922b349f516e79fb5
Version: webpack 3.8.1
Time: 1014ms
Asset Size Chunks Chunk Names
pageB-80c922b349f516e79fb5.js 2.86 kB 0 [emitted] pageB
pageA-80c922b349f516e79fb5.js 2.84 kB 1 [emitted] pageA
pageA-80c922b349f516e79fb5.css 21 bytes 1 [emitted] pageA
pageB-80c922b349f516e79fb5.css 21 bytes 0 [emitted] pageB
结论
可以发现所有文件的hash
全部都是一样的,但是你多构建几次产生的hash
都是不一样的。原因在于我们使用了 ExtractTextPlugin
,ExtractTextPlugin
本身涉及到异步的抽取流程,所以在生成 assets
资源时存在了不确定性(先后顺序),而 updateHash
则对其敏感,所以就出现了如上所说的 hash 异动的情况。另外所有 assets
资源的 hash
值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。
- 使用chunkhash计算
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: {
pageA: './src/a.js',
pageB: './src/b.js'
},
output: {
filename: '[name]-[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?minimize']
})
}
]
},
plugins: [new ExtractTextPlugin('[name]-[chunkhash].css')]
}
构建结果
Hash: 810904f973cc0cf41992
Version: webpack 3.8.1
Time: 1038ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA
pageB-e9ed5150262ba39827d4.css 21 bytes 0 [emitted] pageB
结论
此时可以发现,运行多少次,hash 的变动没有了,每个 entry 拥有了自己独一的 hash 值,细心的你或许会发现此时样式资源的 hash 值和 入口脚本保持了一致,这似乎并不符合我们的想法,冥冥之中告诉我们发生了某些坏事情。
3、探索css文件的hash和入口文件hash之间的关系
在上面的构建结果中,我们发现css的hash值和入口文件的hash值是一样的,这里我们容易产生疑问,是不是这两个文件之间一定会有联系呢?呆着疑问去修改下b.css文件中的内容,产生构建结果:
Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1028ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA
pageB-e9ed5150262ba39827d4.css 41 bytes 0 [emitted] pageB
纳尼???改动css文件内容,为什么css文件的hash没有改变呢?不科学啊,入口文件的hash也没有改变。仔细想了一下 webpack 是将所有的内容都认为是js文件的一部分。在构建的过程中使用 ExtractTextPlugin 将样式抽离出entry chunk 了,而此时的 entry chunk 本身并没有发生改变,改变的是已经被抽离出去的css部分。而chunkunhash 却是根据 chunk 计算出来的,所以不变更应该是正常的。但是这个又不符合我们想要做的持久化缓存的要求,因为又变动就应该改变hash才是。
开心的是 ExtractTextPlugin 插件为我们提供了一个contenthash
来变化:
plugins: [new ExtractTextPlugin('[name]-[contenthash].css')]
修改b.css前后两次构建结果:
Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1091ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-9783744431577cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB-2d03aa12ae45c64dedd7f66bb88dd3db.css 41 bytes 0 [emitted] pageB
Hash: 7a96bcf1ef668a49c9d8
Version: webpack 3.8.1
Time: 1193ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-9783744431577cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB-7e05e00e24f795b674df5701f6a38bd9.css 42 bytes 0 [emitted] pageB
对比发现修改了样式文件后只有样式文件的hash发生了改变,符合我们想要的预期。
4、module id的不可控和修正
经过上面的测试,我们理所当然的认为我完成了持久化缓存的hash稳定。然后我们不小心删除了a.js中的a.less文件,然后前后两次构建:
Hash: 88ab71080c53db9d9f70
Version: webpack 3.8.1
Time: 1279ms
Asset Size Chunks Chunk Names
pageB-a2d1e1d73336f17e2dc4.js 3.82 kB 0 [emitted] pageB
pageA-96c9f5afea30e7e09628.js 3.8 kB 1 [emitted] pageA
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 1 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageB
Hash: 172153ea2b39c2046a92
Version: webpack 3.8.1
Time: 1260ms
Asset Size Chunks Chunk Names
pageB-884da67fe2322246ab28.js 3.81 kB 0 [emitted] pageB
pageA-4c0dfb634722c556ffa0.js 3.68 kB 1 [emitted] pageA
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 1 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageB
奇怪的事产生了,我移除了a.less文件后发现pageB入口文件的hash都改变了。如果只有pageA相关的文件hash变了我还可以理解。但是????为什么都变了???不行我得看看为什么都变了。
通过上面的diff发现我们移除了a.less后整体的id发生了改变了。那么这个地方的id我们可以推测是代表的是具体的引用的模块。
接着我们在看看前后两次构建模块的信息:
[3] ./src/a.js 284 bytes {1} [built]
[4] ./src/a.less 41 bytes {1} [built]
[5] ./src/b.js 284 bytes {0} [built]
[6] ./src/b.less 41 bytes {0} [built]
[3] ./src/a.js 264 bytes {1} [built]
[4] ./src/b.js 284 bytes {0} [built]
[5] ./src/b.less 41 bytes {0} [built]
通过对比发现前面的序号在构建出来的pageB中有隐藏pageA相关的信息,这对于我们来做持久化缓存来说是非常不便的。我们期待的是pageB中只包含和自身相关的信息,不包含其他与自身无关的信息。
5、module id的变化
排除与己不相关的module id或者内容
会用webpack的人大概都之都一个特性:Code Splitting
,本质上是对 chunk 进行拆分再组合的过程。具体要怎么做呢?
The answer is CommonsChunkPlugin
,在plugin中添加:
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]
接下来在看看移除pageA中的a.less的前后变化:
Hash: 697b36118920d991364a
Version: webpack 3.8.1
Time: 1488ms
Asset Size Chunks Chunk Names
pageB-9b2eb6768499c911a728.js 491 bytes 0 [emitted] pageB
pageA-c342383ca09604e8e7b8.js 495 bytes 1 [emitted] pageA
runtime-b6ec3c0d350aef6cbf3e.js 6.8 kB 2 [emitted] runtime
pageA-b812cf5b72744af29181f642fe4dbf38.css 43 bytes 1 [emitted] pageA
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtime
Hash: 7ddaf109d5aa67c43ce2
Version: webpack 3.8.1
Time: 1793ms
Asset Size Chunks Chunk Names
pageB-613cc5a6a90adfb635f4.js 491 bytes 0 [emitted] pageB
pageA-0b72f85fda69a9442076.js 375 bytes 1 [emitted] pageA
runtime-a41b8b8bfe7ec70fd058.js 6.79 kB 2 [emitted] runtime
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtime
接着在看看两次构建中pageB的对比:
经过对比我们发现在pageB中只包含的是自身相关的内容。所以使用CommonsChunkPlugin达到了我们的期望。而抽离出去的代码就是webpack的运行时代码。运行时代码也存储着webpack对module和chunk相关的信息。另外我们发现pageA和pageB的文件大小也发生了变化。导致这个变化的原因是CommonsChunkPlugin会默认的把entry chunk都包含的module抽取到我们取名为runtime的normal chunk中去。
假如我们在开发中每个页面都会用到一些工具库,例如lodash这类的。由于CommonsChunkPlugin的默认行为会抽取公共部分,可能lodash并没有发生改变,但是被抽离在运行时代码中的时候,每次都是会去请求新的。这不能达到我们要求的最小更新原则。所以我们要人工去干预一些代码。
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
在次对边前后两次构建的日志:
Hash: a703a57c828ec32b24e1
Version: webpack 3.8.1
Time: 1493ms
Asset Size Chunks Chunk Names
vendor-f11f58b8150930590a10.js 541 kB 0 [emitted] [big] vendor
pageB-7d065cd319176f44c605.js 938 bytes 1 [emitted] pageB
pageA-2b7e3707314e7ec4d770.js 910 bytes 2 [emitted] pageA
runtime-e68dec8bcad8a5870f0c.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB
Hash: 26fc9ad18554b28cd8e1
Version: webpack 3.8.1
Time: 1806ms
Asset Size Chunks Chunk Names
vendor-d9bad56677b04b803651.js 541 kB 0 [emitted] [big] vendor
pageB-a55dadfbf25a45856d6a.js 929 bytes 1 [emitted] pageB
pageA-7cbd77a502262ddcdd19.js 790 bytes 2 [emitted] pageA
runtime-fa8eba6e81ed41f50d6f.js 5.88 kB 3 [emitted] runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB
到此为止我们解决了:排除与己不相关的module id或者内容问题。
稳定module id,尽可能的保持module id保持不变
一个module id是一个模块的唯一标示,并且该标示会出现在对应的entry chunk构建后的代码中。看个pageB的构建后代码的例子:
__webpack_require__(7)
const sum = __webpack_require__(0)
const _ = __webpack_require__(3)
根据前面的实验,模块的增加或者减少都会引起module id的改变,所以为了不引起module id的改变,那么我们只能找一个东西来代替module id作为标示。我们在构建的过程中就将寻找出来替代标示来替换module id。
所以上面的叙述可以转换成两个步骤来行动。
- 找到替代module id的方式
- 找到时机替换module id
6、稳定 module id 的相关操作
找到替代module id的方式
我们在日常的开发中,经常引用模块,都是通过地址来引用的。从这里我们可以得到启发,我们能不能够把module id全部替换成路径呢?再一个我们了解到在webpack resolve module阶段我们肯定是可以拿到资源路径的。在开始我们担心平台的路径差异性。幸运的是webpack 的源码其中在 ContextModule#74 和 ContextModule#35 中 webpack 对 module 的路径做了差异性修复。也就是说我们可以放心的通过module的libIdent来获取模块的路径了。
在整个webpack的执行过程中涉及到module id有三个钩子:
before-module-ids
-> optimize-module-ids
-> after-optimize-module-ids
所以我们只要在before-module-ids
中做出修改就好了。
编写插件:
'use strict'
class moduleIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin("before-module-ids", (modules) => {
modules.forEach((module) => {
if(module.id === null && module.libIdent) {
module.id = module.libIdent({
context: this.options.context || compiler.options.context
})
}
})
})
})
}
}
module.exports = moduleIDsByFilePath
上面的其实已经被webpack抽成一个插件了:
NamedModulesPlugin
所以只需要在插件那一部分里面添加上
new webpack.NamedModulesPlugin()
接下来对比下两次构建前后文件的变化:
Hash: e5bc78237ca9a3ad31f8
Version: webpack 3.8.1
Time: 1508ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB-432105effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA-158bf2a923c98ab49be2.js 1.09 kB 2 [emitted] pageA
runtime-9ca4cebe90e444e723b9.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB
Hash: 7dce5d9dc88f619522fe
Version: webpack 3.8.1
Time: 1422ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB-432105effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA-dae883ddaeff861761da.js 940 bytes 2 [emitted] pageA
runtime-c874a0c304fa03493296.js 5.88 kB 3 [emitted] runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB
哇,我们对比发现只有相关改动的文件和运行时代码发生了改变,vendor和pageB相关都没有发生改变。美滋滋~~
这下我们达到了我们的目的,我们可以去看看我们构建后的代码了:
__webpack_require__("./src/b.less")
const sum = __webpack_require__("./src/common.js")
const _ = __webpack_require__("./node_modules/lodash/lodash.js")
真的是变成了路径,成功~~。但是新的问题貌似又来了,和之前的文件对比发现我们的文件普遍比之前的变大了。好吧,是我们换成文件路径的时候造成的。这个时候我们能不能用hash来代替文件路径呢?答案是可以,官方也有插件可以供我们使用:
new webpack.HashedModuleIdsPlugin()
官方说 NamedModulesPlugin 适合在开发环境,而在生产环境下请使用 HashedModuleIdsPlugin。
这样我们就达成了使用hash来代替原来的module id使之稳定。而且构建后的代码也不会变化太大。
本以为可以到此为止了。但是细心的人会发现runtime文件每次编译都发生了变化。是什么导致呢的?来看看吧:
我们观察发现,在我们的entry chunk数量没有发生变化的时候,改变一个entry chunk的内容导致runtime内容发生变化的只有chunk id这个时候问题就又来了。根据上面稳定module id的操作一样,数值型的chunk id不稳定性太大,我们要换,方式和上面一样。
- 找到稳定chunk id的方式
- 找到改变chunk id的时机
7、稳定chunk id的相关操作
找到稳定chunk id的方式
因为我们知道webpack在打包的时候入口是具有唯一性的,那么很简单我们能不能够用入口对应的name呢?所以这里就比较简单了我们就用我们的entry name来替换chunk id。
找到改变chunk id的时机
根据经验module 有上面的过程那么 chunk我觉得也是有的。
before-chunk-ids
-> optimize-chunk-ids
-> after-optimize-chunk-ids
所以编写插件:
'use strict'
class chunkIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('before-chunk-ids', chunks => {
chunks.forEach(chunk => {
chunk.id = chunk.name
})
})
})
}
}
module.exports = chunkIDsByFilePath
不巧的是官方也有这个插件所以不用我们写。
NamedChunksPlugin
构建后的代码里面我们可以看到了:
/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"ed00d7222262ac99e510","pageA":"b5b4e2893bce99fd5c57","pageB":"34be879b3374ac9b2072"}[chunkId] + ".js";
原来的chunk id现在全部变成了entry name了,变更的风险又小了一点了。美滋滋~~
我们换成名字后那么问题又和上面module id换成name 又一样的问题,文件会变大。这个时候还是想到和上面的方式一样用hash来处理。这个时候就真的要编写插件了。安利一波我们自己写的
webpack-hashed-chunk-id-plugin。
到此持久化缓存中遇到的核心难题都已经处理完了。
最后
如果你想要快速搭建一个项目,欢迎使用这边的项目架构哦。
webpack-project-seed已经有线上项目用的用这个在跑了哦。顺便star一个吧。
感谢:@pigcan
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。