webpack 的 fullhash、chunkhash 和 contenthash 有什么区别?怎样做长效缓存?
fullhash 是整次编译的哈希,任何文件变了它都变;chunkhash 按 chunk 计算,包含 chunk 里所有类型的内容;contenthash 只算这个文件那一种内容。长效缓存用 [contenthash] 命名,再把运行时拆成单独 chunk、第三方库拆进 vendors,并固定 moduleIds。
简答
上线后,
main.js想让浏览器缓存一年,但代码一改又要立刻拿到新文件。办法是把哈希写进文件名:内容变了文件名就变,HTML 引用新文件名,旧文件继续被缓存也没关系。webpack 提供三种哈希。
[fullhash]是整次编译的哈希,任何一个文件改了,所有用它命名的文件都会变。[chunkhash]按 chunk 算,包含这个 chunk 里的全部内容。[contenthash]也按 chunk 算,但只算这个文件那一种内容类型,比如同一个 chunk 抽出来的 JS 和 CSS 各算各的。所以长效缓存一般用
[name].[contenthash].js。再配合三件事:runtimeChunk: 'single'把运行时拆出去,splitChunks把node_modules拆成 vendors,moduleIds: 'deterministic'让模块 ID 不随引入顺序变化。这样改业务代码时,vendors 的文件名保持不变。要注意,带哈希的文件可以长期缓存,引用它们的 HTML 却不能,HTML 要每次协商,否则用户拿不到新文件名。
三种哈希分别按什么算
webpack 文档的 output.filename 模板字符串表是这样分的:[fullhash] 属于编译级别,是“整次编译的完整哈希”,旧名字 [hash] 已废弃;[chunkhash] 是“chunk 的哈希,包含 chunk 的所有元素”;[contenthash] 是“chunk 的哈希,只包含这一种内容类型的元素”。
先看 [fullhash]。整次编译只有一个值,只要项目里任何一个模块改了,它就变。如果所有文件都用它命名,改一行业务代码,第三方库的文件名也跟着变,用户得重新下载全部文件,缓存等于没做。
[chunkhash] 缩小到了 chunk。改了 about 页面的代码,只影响 about 这个 chunk,main 不受影响。可是一个 chunk 可能同时产出 JS 和 CSS,比如用 mini-css-extract-plugin 把样式抽成单独文件。[chunkhash] 包含 chunk 的所有元素,只改样式时,JS 文件的哈希也可能一起变。
[contenthash] 再细一层,只算这个文件那种内容。只改 CSS,就只有 CSS 文件名变,JS 文件名不动。所以文档的缓存指南直接用 [contenthash]。
哈希长度可以写成 [contenthash:8],默认 20 位,也能用 output.hashDigestLength 统一设置。
只用 contenthash 为什么还不够
缓存指南里有个现象:源码什么都没改,再构建一次,文件名还是可能变。原因是 webpack 会把运行时和 manifest 这些样板代码放进入口 chunk。运行时就是 webpack 加载、连接模块用的那段代码,manifest 记录了模块和 chunk 的对应关系,任何 chunk 变了,这段代码都可能变。
第二个现象更隐蔽:只在业务代码里新增一个模块,vendors 的文件名也变了。文档解释,这是因为模块 ID 按解析顺序分配(moduleIds: 'natural'),新模块插进来,后面模块的 ID 都往后挪,vendors 里引用的 ID 变了,内容也就变了。
所以文档给出的完整配置是这样的:
module.exports = {
output: {
filename: '[name].[contenthash].js',
clean: true,
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
runtimeChunk: 'single' 把运行时拆成一个单独的 runtime 文件,所有 chunk 共用它。第三方库改动少,单独打进 vendors,业务代码频繁发版时它的文件名不变,用户不用重新下载。
moduleIds: 'deterministic' 让模块 ID 不随解析顺序变化。文档说明,production 模式默认就是 'deterministic',development 模式默认是 'named',两者在多次构建之间都稳定,显式写上是为了不依赖 mode。文档按这套配置构建两次,一次引入新模块、一次不引入,vendors 的文件名完全相同。
拆出去以后,改业务代码时 main 和 runtime 变化是正常的:main 内容变了,runtime 里记录的模块也变了,只有 vendors 保持不变。
realContentHash 是做什么的
[contenthash] 受 optimization.realContentHash 影响。文档说明,开启后会在资源处理完以后再算一遍哈希,得到真正按最终内容计算的值;关掉时用内部数据计算,资源内容相同时哈希也可能不同。它在 production 模式默认开启,development 和 none 默认关闭。所以线上构建不用额外配置。
文件名带了哈希,服务器怎么配缓存
文件名带哈希以后,内容一变就是新地址,所以这些文件可以设置很长的强缓存。HTML 不同,它的地址不变,又负责引用最新的文件名,必须每次都回服务器确认。缓存头怎么写,见《Cache-Control 的 immutable、no-store 等指令怎样选?》和《强缓存与协商缓存如何配合?》。
常见追问
开发环境要不要用 contenthash?
一般不用。开发服务器不需要应对浏览器的长期缓存,用
[name].js就够了。realContentHash在development模式默认也是关闭的。
为什么改了一个页面,其他 chunk 的文件名也变了?
先看运行时有没有拆出去,没拆的话运行时在入口 chunk 里,任何 chunk 变化都会带着它变。再看
moduleIds是不是'natural',按解析顺序分配 ID 时,新增模块会让其他模块的 ID 跟着变。
哈希写在文件名里,还是写成查询参数 ?v=xxx?
写在文件名里。webpack 的缓存指南就是用
[name].[contenthash].js让内容变化体现在文件名上,新旧版本是两个不同的文件,可以同时存在于服务器上,发版时旧页面还能继续加载旧文件。