webpack的splitChunk造成hash一致但文件内容不一致的bug

bug描述

上线后发现页面白屏,控制台也没有任何报错。经过定位发现,预发布环境部署了一个vendors_1234.js文件把线上环境的vendors_1234.js覆盖了(预发布和线上的静态文件是一个目录),但是这两个文件的内容竟然不一样。你没看错!!hash相同,内容不同!!。本以为是碰到了万年不遇的撞hash事件,最后研究发现竟然是webpack的splitChunk导致的,而且有很大概率会出现。所以写出来让大家参考!

解决办法

解决办法很简单,为了照顾心急的同学,先把解决办法放上,后面再慢慢看解决过程。
修改webpack配置,让chunkId固定

  • optimization.chunkIds 设置为 ‘named’,让chunkId不会随着chunk数量变化,见下图。
    webpack的splitChunk造成hash一致但文件内容不一致的bug_第1张图片

  • 另推荐同时设置 optimization.moduleIds = ‘hashed’ 让module的id也固定,效果与老版的 HashedModuleIdsPlugin 相同。即不会造成新增了个module就造成所有moduleId都变化,从而使所有chunk的hash也跟着变化,让缓存失效。参考《对Webpack的hash稳定性的初步探索》效果见下图
    未配置moduleIds和chunkIds
    webpack的splitChunk造成hash一致但文件内容不一致的bug_第2张图片
    配置了moduleIds='hashed’和chunkIds='named’
    webpack的splitChunk造成hash一致但文件内容不一致的bug_第3张图片

前置核心知识点

chunkId:

webpack将打包后的内容都挂载在 window.webpackJsonp 这个数组上。每个元素对应一个打包后js文件的内容,称为chunk。每个chunk都有一个chunkId,入口文件通过chunkId从window.webpackJsonp 中读取js代码运行,chunkId默认从0开始累加。如下图中,vendors这个chunk的chunkId是6。可阅读《记一次对webpack打包后代码的失败探究》
帮助理解。
webpack的splitChunk造成hash一致但文件内容不一致的bug_第4张图片

两个文件hash相同但文件内chunkId不同的情况:

经过试验和资料查找,推测chunk及hash生成后才做的splitChunk(见下图),所以的确会产生两个文件hash相同但文件当中显示的chunkId不同的情况。
webpack的splitChunk造成hash一致但文件内容不一致的bug_第5张图片
图片引自《Webpack揭秘——走向高阶前端的必经之路》

解决过程TL;DR

复现步骤

  1. 初始状态:线上vendors_1234的chunkId为19,线上的index19.js可以正常读取到window.webpackJsonp[19](伪代码示意,vendors_1234运行时,把自己的内容放在了这里)的代码并正常运行。
  2. 开发优化减少了打包数量,导致vendors的chunkId变为6,但内容不变,所以hash值没变(例子见下图,原因见上文),文件名还是vendors_1234(但是chunkId=6),开发环境的index6.js可以正常读取vendors_1234并运行。
    webpack的splitChunk造成hash一致但文件内容不一致的bug_第6张图片
  3. 发布预发布环境,由于预发布与线上用的是同一个目录,这时vendors_1234(chunkId=19)会被覆盖,变为vendors_1234(chunkId=6)。
  4. 线上的index19.js尝试读取window.webpackJsonp[19]的内容,但是线上此时是vendors_1234(chunkId=6),其内容放在了window.webpackJsonp[6],所以读取不到正确的代码,无法正常运行。导致白屏bug!

模拟复现步骤build各种版本

修改异步加载模块的数量,经试验,发现下图中箭头指向的 x(当时还不知道这个是chunkId)与打包后js文件总数相差2,推断 x 表示「异步加载的模块数量」或「当前模块的索引」,差的2是vendor自身和runtime.js。案例如下(vendors的hash全部相同):
注:runtime.js是为了解决异步加载模块变更导致index.js变更的问题,优化缓存。可搜索runtimeChunk了解详情
js总数总比 x 大2
js总数21,x = 19;js总数20,x = 18;js总数19,x = 17;js总数8,x = 6……
webpack的splitChunk造成hash一致但文件内容不一致的bug_第7张图片
**去掉runtime,js总数总比 x 大1 **
js总数7,x = 6;js总数20,x = 19……
此时意识到是chunk数量引起了bug,于是尝试固定vendors的chunkId来解决问题。

分析runtime.js尝试固定chunkId

webpack的splitChunk造成hash一致但文件内容不一致的bug_第8张图片
如上图所示,除了vendors,其他异步加载的chunk都在runtime.js里列出来了。于是尝试如何把vendors加入到runtime.js里。经过短暂的努力,宣告失败,换思路。

研究webpack - splitChunk配置

经试验分析如下:

optimization.splitChunks.chunks = 'initial’情况下,vendors会被最后生成,chunkId也会随着打包数量变化。optimization.splitChunks.chunks为其他值时,vendors会被先分析,其chunkId始终为0。

因此猜测,文件的hash生成是在chunk挂在在window.webpackJsonp上之前(此时对webpack打包流程还没有这么清晰,只是隐约有个判断),此时并没有生成chunkId。所以造成了hash值一样,chunk内容不一样。
猜测归猜测,先放一边,继续解决chunkId固定的问题。由于optimization.splitChunks.chunks为其他值时不满足需求,所以此路仍然不通,再换。

更加有效的google

偶然发现dev环境下chunkId不是数字
在这里插入图片描述
这个发现,加上上文放一边的那个猜测,开始各种google,发现了两个较有帮助的资料:
《记一次对webpack打包后代码的失败探究》 这篇文章让我初步了解到了webpack打包的原理,也同时对webpackJsonp、chunkId等各种概念有了认识。
《Webpack原理与实践(一):打包流程》这篇文章让我进一步了解了webpack打包的流程,并且找到了上文猜测的答案,的确与我猜测的吻合。
此时才对于这些概念有了认识,google了几个关键词后,直接在webpack文档中定位到了解决方案:
https://webpack.js.org/configuration/optimization/#optimizationchunkids
同时还优化掉了 HashedModuleIdsPlugin 插件。
至此,问题算是解决了。

总结

  • staging与online一定要充分隔离,不能有共享的情况,包括静态文件。hash也不保准!
  • 还有一个兜底方案,静态文件的文件名也加入环境信息,甚至项目信息,如index_prd_hash.js, performance_vendors_staging_hash.js
  • 解决疑难杂症还是要深入原理层面,至少要弄清楚一些核心概念,好针对性的google。
  • 碰到复杂问题,不要怕,慢慢抽丝剥茧,解决过程中会学到很多新东西,解决问题后会得到很大的成就感,与君共勉~

你可能感兴趣的:(前端工程化)