聊聊 webpack 異步加載(一):webpack 如何加載拆包後的代碼

作者 | vayne
轉載請註明出處

前端代碼在使用 ​webpack​ 進行打包時,經常會做兩種優化:

把穩定的庫代碼(如 ​react​、​antd​ 等)與業務代碼分離,業務代碼的更改不影響用戶本地的庫代碼緩存,同時也把一個大文件拆分成多個文件,充分利用瀏覽器並行加載網絡資源的能力,提高加載性能。配合 ​react-router​ 使用 ​import()​ 異步按需加載組件,減少不必要的資源加載,提高首屏性能。

一般而言,常用的 ​CLI​ 工具基本上配置得足夠好,可以開箱即用。​webpack​ 對於這兩種的情況處理有交叉也有區別,這一篇文章先來講解第一種情況。閱讀之前,建議瀏覽這篇文章(https://zhuanlan.zhihu.com/p/52826586)熟悉基本的 ​webpack runtime​ 代碼。

基本代碼結構

首先要熟悉 ​webpack​ 中的 ​chunk​ 和 ​module​。​module​ 可以簡單理解成在 ​js​ 文件中由 ​export​ 導出的模塊,​chunk​ 是由多個 ​module​ 組成的代碼塊(準確來說,​webpack​ 把所有資源都當作 ​module​,​chunk​ 其實也包括了圖片、​css​ 等資源,下面的分析都把 ​module​ 當成代碼,其他資源同理)。先來看下打包出來的 ​html​ 模板文件:

之前一個 ​js​ 文件按照配置被拆分成了三個 ​js​ 文件。看到這裡,應該會有疑問: ​webpack​ 怎麼保證多個 ​js​ 文件的加載順序(​script​ 標籤不加 ​defer​、​async​ 等屬性時,是按順序執行加載的,即使前面的資源因為各種原因被阻塞也會按順序加載,這裡主要討論加上 ​defer​、​async​ 的情況),又是怎麼做到不同 ​js​ 文件中的代碼協作,帶著這些問題一步一步分析。

bootstrap:

首先要熟悉四個緩存變量(截圖中從上到下的順序):

​modules​:緩存 ​module​ 代碼塊,每個 ​module​ 有一個 ​id​,開發環境默認以 ​module​ 所在文件的文件名標識,生產環境默認以一個數字標識。​modules​ 是一個 ​object​, ​key​ 為 ​module id​,​value​ 為對應 ​module​ 的源代碼塊。​installedModules​:緩存已經加載過的 module​,簡單理解就是已經運行了源碼中 ​import somemodule from 'xxx'​ 這樣的語句。​installedModules​ 是一個 ​object​, ​key​ 為 ​module id​,​value​ 為對應 ​module​ 導出的變量。(跟 ​modules​ 的 ​value​ 是不一樣的,這裡的 ​value​ 保存的是 ​module​ 對應的代碼中 ​export​ 的變量)​installedChunks​:緩存已經加載過的 chunk​,簡單理解就是把其他 ​js​ 文件中的 ​chunk​ 包含的 ​modules​ 同步到了當前文件中。每個 ​chunk​ 有一個 ​id​,默認以一個數字標識。​installedChunks​ 也是一個對象,​key​ 為 ​chunk id​,​value​ 有四種情況:undefined:chunk not loadednull:chunk preloaded/prefetchedPromise:chunk loading0:chunk loaded​deferredModules​:緩存運行當前 ​web app​ 需要的 ​chunk id​ 以及入口 ​module id​(截圖中 299 標識入口 ​module​ 的 ​id​,0 和 1 標識運行必需的另外兩個 ​chunk​ 的 ​id​),比如,​react​ 和 ​react-dom​ 被單獨打包到了另外的 ​js​ 中,入口文件需要等待 ​react​ 和 ​react-dom​ 加載成功之後才能運行。

理解這四個變量之後,代碼邏輯看起來就很容易了。

Chunk 代碼塊

首先熟悉下拆分出來的 chunk 代碼塊的基本形式:

簡單理解就是 ​window["webpackJsonp"]​ 下 “push” 了一個二維數組,第一項是當前 ​chunk​ 的 ​id​,第二項就是當前 ​chunk​ 包含的 ​modules​。這一步的主要作用就是通過 ​window["webpackJsonp"]​ ,把在不同 ​js​ 文件中代碼塊聯繫起來。

checkDeferredModules:

入口文件首先填充 ​deferredModules​ 的內容,為運行作準備。之後會調用 ​checkDeferredModules​ 方法。

​checkDeferredModules​ 也很簡單,判斷必需的 ​chunk​ 是否已經加載,如果已經加載,執行入口 ​module​ 代碼,否則啥也不做。之前一個 ​js​ 文件按照配置被拆分成了多個 ​js​ 文件,多個 ​js​ 文件的加載以及執行順序存在著不確定性,所以做了一個檢查,確保必需的的資源在當前環境下已經加載完畢。

webpackJsonpCallback:

入口代碼塊首先執行這幾行代碼:

這幾行代碼看似簡單,蘊含的邏輯其實比較多,設計得也很巧妙。我們分兩種情況分析,第一種情況是 chunk js 資源首先執行,入口 js 資源最後執行:

jsonpArray 初始化為 window["webpackJsonp"],當前情況下 window["webpackJsonp"] 已經包含了必需的 chunk。保存 jsonpArray 的 push 方法(即為數組原生 push 方法),並賦值為 webpackJsonpCallback。實際上就是改寫 window["webpackJsonp"] 的 push 方法,之後把 jsonpArray 還原成普通數組。對 jsonpArray 的每一項執行 webpackJsonpCallback 方法:

參數 data 的形式為 [ chunkId[], modules[], deferredModules[] ]第一個 for 循環標識當前 chunk 已加載(installedChunks[chunkId] === 0 表示 chunk 已加載,resolves 數組是動態 import 需要使用的,此處暫時不涉及)。第二個 for 循環把當前 chunk 包含的 module 保存到入口文件的 modules 變量parentJsonpFunction 在當前情況下為空,resolves 數組暫時不涉及如果 chunk 中還有其他 deferredModule,加入 deferredModules 中(拆分 webpack runtime 代碼時會用到)每次加載完當前 chunk 之後都會調用一次 checkDeferredModules 判斷是否所有 chunk 已經加載完畢,當加載完畢後就會執行入口 module,從而構建整個 web app

第二種情況,入口 js 資源穿插在加載 chunk 的 js 資源當中執行,基本流程是一致的,有兩點不同:

後續的 chunk js 執行時,window["webpackJsonp"] 的 push 方法已經被改寫成了 webpackJsonpCallbackparentJsonpFunction 在這種情況下保存的是原生數組的 push 方法,this 指向了 window["webpackJsonp"],目的是集中存在於多個 chunk js 中的 module ,方便多入口文件的其他入口 js 加載。

至此,chunk 拆分的邏輯已經完結了,可以畫個流程圖簡單總結一下


相關鏈接: