2018年2月,令人兴奋的是,Webpack迎来了4.x版本的升级,并给这个主版本的更新命名为Legato,意为"Legato means to play each note in sequence without gaps."从3.x到4.x的大版本升级,官方给出了 "Webpack4 is FAST (up to 98% faster)!",支持WebAssembly等等新特性。
在我们的一个后续项目中,Webpack随即升级到了4.8的版本,该项目前端技术栈主要基于Nuxt,分别部署在中国、美国和欧洲三个区,三区跑的是同一套代码。
一个奇怪的现象:
在一个风和日丽的下午,有用户反馈该项目的登录页在国外站点美国区访问出现页面加载不完全,无法进行后续的交互操作,页面无响应。试着去访问该站点的中国区可以进行正常登录操作,大多数用户访问美国区,也未出现上述问题的现象。由于该站点部署在国外,国内访问通常要通过VPN来访问,时常会出现一些国内用户因为网络原因导致资源加载不成功,页面不全的现象,加上最近一周没有线上发布任务,故初步认为是个别用户的网络原因。
初步的定位
越来越多的美国区用户反馈访问不了登录页,无法进行后续的操作,且对该用户现象为必现,事情似乎远没有想象的那么简单。中国区用户在一个VPN下可以正常访问,切换到另外一个VPN下上述问题必现。打开谷歌浏览器控制台网络面板,发现请求的网络资源响应全部200正常。
问题的初步表象是:在部分用户的使用场景下,美国区站点出现请求的资源文件200响应成功,却没有执行到对应的js文件。
打开谷歌浏览器Sources面板,打断点查看为什么会出现文件响应成功却没有执行到js的原因。经过很长一段时间,发现一个令人不解的现象,通过切换两个不同的VPN,其中一个VPN下该站点可以正常访问登录,另一个VPN下会出现上述问题,我们发现,在一个名称为“15b9e800846d8848cd67.js”资源文件中,出现文件内webpackJsonp.push(chunkId)的chunkId值不一样的现象,其中一个chunkId值是11,另一个chunkId值是12。如下图所示(该文件的chunkId值是12):
我们知道,使用webpack打包构建的项目,chunk 代表生成后的 js 文件,一个 chunkId 对应一个打包好的 js 文件。内容如下:
// 加载在本文件中包含的模块
webpackJsonp(
// chuckIds 一般包含该 chunk 文件依赖的 chunkId 以及自身 chunkId
[chunkId],
// 本文件所包含的模块
[(function (module, exports) {...}],
)
执行入口文件通常内容如下:
(function (modules) {
/***
* webpackJsonp 用于从异步加载的文件中安装模块。
* 把 webpackJsonp 挂载到全局是为了方便在其它文件中调用。
*
* @param chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
* @param moreModules 异步加载的文件中存放的需要安装的模块列表
* @param executeModules 在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的 index
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 把 moreModules 添加到 modules 对象中
// 把所有 chunkIds 对应的模块都标记成已经加载成功
var moduleId, chunkId, i = 0, resolves = [], result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
while (resolves.length) {
resolves.shift()();
}
};
// 缓存已经安装的模块
var installedModules = {};
// 存储每个 Chunk 的加载状态;
// 键为 Chunk 的 ID,值为0代表已经加载成功
var installedChunks = {
1: 0
};
...
})
自此,问题的初步原因似乎已经找到了:CDN不同的节点上缓存了两个文件名称相同的资源文件,而这两个相同文件名的文件内容不一致,webpackJsonp.push(chunkId)的chunkId值分别是11和12。所以会存在,在一个VPN环境下请求该CDN节点上的缓存资源文件是可以正常执行,因为加载的chunk是没问题的,页面访问正常,在另一个VPN下请求到最近CDN节点上缓存的文件chunkId值不对,加载了错误的模块导致页面无法正常加载。
于是,通过进入AWS云控制台手动刷新CDN节点上的缓存,这样保证源站内容与CDN的缓存内容保持一致,使得CDN节点上缓存更新到了最新。再次访问无法登录的节点上的VPN环境,发现该问题已经修复,可以进行正常访问操作。
根本原因
通过回溯过去的发布任务,我们发现,最近的一次线上环境发布是一周前,最近的一次预发布是第一次用户反馈无法登录的前一个多小时。查看此次预发布的git commitId,执行git show commitId发现主入口文件新增了一个js-cookie模块。
import Cookies from 'js-cookie'
新增的依赖模块在打包后新生成了一个chunk文件,使得原来"15b9e800846d8848cd67.js"文件chunkId值顺序从11变成了12。
// 旧的
webpackJsonp.push(11)
//新的
webpackJsonp.push(12)
由于我们的预发布是docker镜像的增量更新,会使得预发布后在CDN节点上分别缓存了同一个名称为"15b9e800846d8848cd67.js"的文件但文件内容的chunkId值是不同的情况。
那么根本问题来了,webpack打包后的项目文件名经过contenthash后为什么还会存在文件内容已经不一致文件名称相同的情况?
查看webpack编译配置,输出文件名配置如下:
filenames: {
app: '[name].[contenthash].js',
chunk: '[name].[contenthash].js',
css: '[name].[contenthash].css',
}
可以确认是webpack打包出了问题,查看项目中使用的webpack版本是4.8.0,找到webpack在GitHub上关于contentHash的issues,如图所示:
webpack团队人员已经声明this is a bug,并在4.17.0版本进行了修复,issue传送门
修复代码如下:
升级webpack版本到4.20.0,编译打包后,该问题不在出现。
最后
至此,问题得到了解决,在此过程中出现的问题以及后续的改进都值得我们去深思。在追求Webpack从3.x到4.x的的大版本更新带来的显而易见的提升和令人兴奋的新特性中,我们也要时刻保持跟进,并针对第三方库出现的问题进行及时的修复升级。