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

作者 | vayne
轉載請註明出處

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

  1. 把穩定的庫代碼(如 ​react​、​antd​ 等)與業務代碼分離,業務代碼的更改不影響用戶本地的庫代碼緩存,同時也把一個大文件拆分成多個文件,充分利用瀏覽器並行加載網絡資源的能力,提高加載性能。
  1. 配合 ​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​ 模板文件:

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

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

bootstrap:

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

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

  • ​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 loaded
    • null:chunk preloaded/prefetched
    • Promise:chunk loading
    • 0: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 代碼塊的基本形式:

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

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

checkDeferredModules:

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

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

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

webpackJsonpCallback:

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

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

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

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

    • 參數 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 方法已經被改寫成了 webpackJsonpCallback
  • parentJsonpFunction 在這種情況下保存的是原生數組的 push 方法,this 指向了 window["webpackJsonp"],目的是集中存在於多個 chunk js 中的 module ,方便多入口文件的其他入口 js 加載。

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

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


相關鏈接:



分享到:


相關文章: