作者 | 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 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 代碼塊的基本形式:
簡單理解就是 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 方法已經被改寫成了 webpackJsonpCallback
- parentJsonpFunction 在這種情況下保存的是原生數組的 push 方法,this 指向了 window["webpackJsonp"],目的是集中存在於多個 chunk js 中的 module ,方便多入口文件的其他入口 js 加載。
至此,chunk 拆分的邏輯已經完結了,可以畫個流程圖簡單總結一下
相關鏈接:
閱讀更多 頭號前端 的文章