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):

15b9e800846d8848cd67.js文件

我們知道,使用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 issue

webpack團隊人員已經聲明this is a bug,並在4.17.0版本進行了修復,issue傳送門

修復代碼如下:

升級webpack版本到4.20.0,編譯打包後,該問題不在出現。

最後

至此,問題得到了解決,在此過程中出現的問題以及後續的改進都值得我們去深思。在追求Webpack從3.x到4.x的的大版本更新帶來的顯而易見的提升和令人興奮的新特性中,我們也要時刻保持跟進,並針對第三方庫出現的問題進行及時的修復升級。


推薦閱讀:
查看原文 >>
相关文章