-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2016-10-27 你用 webpack 1.x 输出的 hash 靠谱不? #1
Comments
mark,666 |
在使用
首先执行一次 webpack 构建, 得到: pageB.1536.js
来看下构建后的内容 __webpack_require__(1) // a.js
__webpack_require__(2) // b.js
var a = 'this is pageA';
__webpack_require__(2) // b.js
__webpack_require__(3) // c.js
var b = 'this is pageB'; 修改一下
来看下构建后的内容 // require('./a.js') // a.js
__webpack_require__(1) // b.js
var a = 'this is pageA';
__webpack_require__(1) // b.js
__webpack_require__(2) // c.js
var b = 'this is pageB'; 附上测试用的文件
console.log('a.js');
console.log('b.js');
console.log('c.js');
require('./a.js') // a.js
require('./b.js') // b.js
var a = 'this is pageA';
require('./b.js') // b.js
require('./c.js') // c.js
var b = 'this is pageB';
var webpack = require('webpack');
module.exports = {
entry: {
pageA: './pageA.js',
pageB: './pageB.js',
},
output: {
path: __dirname + '/build',
filename: '[name].[chunkhash:4].js',
chunkFilename: '[id].[chunkhash:4].bundle.js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
minChunks: 2,
chunks: ['pageA', 'pageB'],
})
]
}; |
@ufologist 您说的「没有出现问题」是指重复我的实验,重现不出我的结果? |
@zhenyong 是的, 我就是重复了「模块 ID 的坑」中的实验, 没有出现你提到的问题
我的实验结果是 |
哎呀,不好意思哈!是我文章没写清楚。 在做 「模块 ID 的坑」的实验前已经使用了前面的「自定义 chunkhash」,所以会出现「pageB的 hash 没变」的情况。 |
请问大神有遇到两台机器上编译同一代码hash值不同的问题吗?我试了你写的插件也无法解决。 |
@Double-Lv 两台机器的操作系统一样吗?目前计算 「moduleId」使用了路径,可能是不同操作系统的路径分隔符引起? |
@zhenyong 两台windows,一台mac,各不相同~~!app.0-53-xxxxxxxxx.js格式,但是xxxxxxx各不相同。可能是什么原因吗?或者你有试过两台机器吗?谢谢 |
chunkhash 计算如下:
如果两个系统中的所有依赖的代码都一样,那么结果是一样的,如果不一样则有可能是:
如果还愿意做实验的话,建议完整复制项目文件夹然后在不同电脑进行打包。 |
根据文件路径通过哈希计算映射为数字,只要文件路径不变,那么chunkId也不变,这样也就不需要对webpack runtime单独提取,也不影响静态资源长期缓存。 |
目前我在 [email protected] 中遇到了一个诡异问题,希望能得到 @zhenyong 指教: |
@gongph 如果偶尔加载失败了,我猜可能是资源被劫持之类的,抓包看看吧 具体 webpack 或者 vue-cli 问题,还是去官方提 issue 吧 |
本文只围绕如何保证 webpack 1.x 在 生产发布阶段 输出稳定的 hash 值展开讨论,如果对 webpack 还没了解的,可以戳 webpack。
本文 基于 webpack 1.x 的背景展开讨论,毕竟有些问题在 webpack 2 已经得到解决。为了方便描述问题,文中展示的代码、配置可能很挫,也许不是工程最佳实践,请轻拍。
懒得看文章的可以考虑直接读插件源码 zhenyong/webpack-stable-module-id-and-hash
目标
除了 html 文件以外,其他静态资源文件名都带上哈希值,根据文件本身的内容计算得到,保证文件没变化,则构建后的文件名跟上次一样。
webpack 提供的 hash
[hash]
假设文件目录长这样:
使用 webpack 配置:
首次构建输出:
再次构建输出:
hash 值是稳定的呀,是不是就可以了呢?且慢!
根据 Configuration · webpack/docs Wiki :
意译:
结合 how to write a plugin 提到:
意译:
我们来动一下
pageA.js
,再次构建:发现 hash 变了,并且所有文件的 hash 值总是一样,这似乎就跟文档描述的一致,只要构建过程依赖的任何资源(代码)发生变化,
compilation
的信息就会跟上一次不一样了。那是不是肯定说,源码不变的话,hash 值就一定稳定呢?也不是的,我们改一下 webpack 配置:
再次构建:
compilation
的信息还包括构建上下文,所以,移除入口或者换个loader 都会引起 hash 改变。[hash]
的缺点很明显,不是根据内容来计算哈希,但是 hash 值是"稳定的",用这种方案能保证『每次上线,浏览器访问到的静态资源都是新的(url 变了)』你接受用
[hash]
吗,我是接受不了?于是我们看 webpack 提供的另一种根据内容计算 hash 的配置。[chunkhash]
意译:
我们改下配置:
构建试试:
动下
pageA.js
再构建:发现只有 pageA 的 hash 变了,似乎 [chunkhash] 就能解决问题了?且慢!
我们目前的代码没涉及到 css,先加点 css 文件依赖:
给 webpack 配置 css 文件的 loader,并且抽取所有样式输出到一个文件
构建:
改一下样式,那么样式的 hash 肯定会变的,那 pageA.js 的 hash 变不变呢?
答案是『变了』:
记得之前说 webpack 的
[chunkhash]
是根据 chunk 的内容计算的,而 pageA.js 这个 chunk 的输出在 webpack 看来是包括 css 文件的,只不过被你抽取出来罢了,所以你改 css 也就改了这个 chunk 的内容,这体验很不好吧,怎么让 css 不影响 js 的 hash 呢?自定义 chunkhash
源码 webpack/Compilation.js:
通过这段代码可以发现,通过在 'chunk-hash' "钩子" 中替换掉 chunk 的 digest 方法,就可以自定义
chunk.hash
了。查看文档 how to write a plugin 了解怎么写插件来注册一个钩子方法:
那么这个 hash 值如何计算好呢?
可以将 chunk 所依赖的各个模块 (单个源码文件) 的内容拼接后计算一个 md5 作为 hash 值,当然需要对所有文件排序后再拼接:
此时,pageA.css 修改之后,再也不会影响 pageA.js 的 hash 值。
另外要注意,ExtractTextPlugin 会把 pageA.css 的内容抽取之后,替换该模块的内容
mod._source._value
为:由于每一个 css 模块都对应这段内容,所以不会影响效果。
erm0l0v/webpack-md5-hash 插件也是为了解决类似问题,但是它其中的『排序』算法是基于模块的 id,而模块的 id 理论上是不稳定的,接下来我们就讨论不稳定的模块 ID 带来的坑。
模块 ID 的坑
我们简单的把每个文件理解为一个模块(module),在 webpack 处理模块依赖关系时,会给每个模块定义一个 ID,查看 webpack/Compilation.js 发现,webpack 根据收集 module 的顺序给每个模块分配递增数字作为 ID,至于『收集的 module 顺序』,在你开发生涯里,这玩意绝对是不稳定!不稳定的!
Module ID 不稳定怎么了
我们的文件结构现在长这样:
pageA.js
pageB.js
更新配置,把引用达到 2 次的模块抽取出来:
build build build:
观察 pageB.0752.js,有一段:
从上面看出,webpack 构建时给
b.js
的模块 ID 为 2这时,我们改一下 pageA.js:
build build build :
嗯! 只有 pageA.js 的 hash 变了,挺合理合理,我们进去 pageB.0752.js 看看
看出来了没!这次构建,webpack 给
b.js
的 ID 是 1。我们 pageB.js 的 hash 没变,因为(使用了前面的「自定义 chunkhash」)背后依赖的模块内容 (b.js、c.js) 没有变呀,但是此时 pageB.0752.js 的内容确实变了,如果你用 CDN 上传这个文件,也许会传不上去,因为文件大小和名称一模一样,就是这个不稳定的模块 ID 给坑的!
怎么解决呢?
第一念头:把原来计算 hash 的方式改一下,就那构建输出后的文件内容来计算?
细想: 不要,明明 pageB 这一次就不用重新上传的,浪费。
比较优雅的思路就是:让模块 ID 给我稳定下来!!!
给我稳定的 Module ID
webpack 1 的官方方案
webpack 文档提供了几种方案
OccurrenceOrderPlugin
这个插件根据 module 被引用的次数(被 entry 引用、被 chunk 引用)来排序分配 ID,如果你的整个应用的文件依赖是没太多变化,那么模块 ID 就稳定,但是谁能保证呢?
recordsPath 配置
会记录每一次打包的模块的"文件处理路径"使用的 ID,下次打包同样的模块直接使用记录中的 ID:
这就要求每个人都得提交这份文件了,港真,我觉得体验很差咯。
另外一旦你修改文件名,或者是增减 loader,原来的路径就无效了,从而再次入坑!
DllPlugin 和 DllReferencePlugin
原理就是在你打包源码前,你得新建一个构建配置用 DllPlugin 单独打包生成一份模块文件路径对应的 ID 记录,然后在你的原来配置使用 DllReferencePlugin 引用这份记录,跟 recordsPath 大同小异,但是更高效和稳定,但是这个额外的构建,我觉得不够优雅,至于能快多少呢,我目前还不在意这个速度,另外还是得提交多一份记录文件。
webpack 2 的思路
以上两个插件的思路都是用模块对应的文件路径直接作为模块 ID,而不是 webpack 1 中的默认使用数字,另外 webpack 1 不接受非数字作为 模块 ID。
我们的思路
把模块对应的文件路径通过一个哈希计算映射为数字,用这个全局唯一的数字作为 ID 就解决了,妥妥的!
参考:
给出 webpack 1.x 中的解决方案:
注册钩子的思路跟之前的 content hash 插件差不多,获取到模块文件路径后,通过 md5 计算输出 16 进制的字符串([0-9A-E]),再把字符串的字符逐个转为 ascii 形式的整数,由于 16 进制字符串只会包含
[0-9A-E]
,所以保证单个字符转化的整数是两位就能保证这个算法是有效的。举例:
这个方案还有些小缺点,就是用模块文件路径作为哈希输入还不是百分百完美,如果文件名改了,那么模块 ID 就 "不稳定了"。其实,可以用模块文件内容作为哈希输入,考虑到效率问题,权衡之下还是用路径好了。
总结
为了保证 webpack 1.x 生产阶段的文件 hash 值能够完美跟文件内容一一映射,查阅了大量信息,根据目前 github 上讨论的解决方案算是大体解决了问题,但是还不够优雅和完美,于是借鉴 webpack 2 的思路加上一点小技巧,比较优雅地解决了这个问题。
插件放在 Github: zhenyong/webpack-stable-module-id-and-hash
参考资料
The text was updated successfully, but these errors were encountered: