本篇文章建議親自動手嘗試.javascript
最近研究了 webpack 運行時源碼, 在這篇文章中記錄了個人探索 webpack 這個複雜的玩具方式, 而且以圖形的形式將 webpack 運行時的流程給記錄的下來.java
這篇文章主要記錄的是 webpack 在將文件打包後是如何在瀏覽器中加載以及解析的流程.node
webpack 實在是太複雜的了, 爲了不額外的干擾, 我使用最小化實例的方式來進行探究 webpack 是如何加載和解析模塊文件的. webpack
具體的手段:首先準備幾個很是簡單的 js 文件, 其次準備其對應的 webpack 配置文件, 逐一測試後觀察其輸出, 閱讀其源碼, 而後得出結論.web
webpack 的運行時在解析同步模塊的時候就和 nodejs 規則一模一樣. 數組
什麼意思, 每個模塊在運行前都會被一個函數進行包裹:瀏覽器
(function (module, exports, __webpack_require__) { // do something })
看起來和 nodejs 同樣, 做用起來也一致, 這裏的當前模塊的導出模塊和導入模塊就是:緩存
而當一個模塊被須要的時候, webpack 就會執行這個函數獲取其對應的 exports對象.服務器
注意:咱們須要的是模塊執行完成後的 exports 對象而不是這個模塊函數. app
在 webpack 中咱們可使用 ESM 規範和 commonjs 規範來加載模塊, 不管使用哪一種規範, 實際上均可以被這種方式來包裹起來, 由於這兩種常見的方式中都存在着相同的導入導出的概念, 因此 webpack 將這兩種形式進行了統一包裝消除了差別.
對於異步模塊這裏有兩種狀況都是屬於異步的狀況:
import()
語法, 引入的模塊.如今你須要知道的是, webpack 運行時徹底不依賴文件加載順序, 不管文件加載順序是何種方式, webpack 均可以輕鬆應對.
import()
語法經常使用於代碼切割或者叫作懶加載, 這種狀況下 webpack 使用script引入打包後的文件, 而後使用Promise語法來進行異步處理(後續會有進一步的討論).
"webpack": "^4.31.0", "webpack-cli": "^3.3.2"
只須要 webpack 自己就能夠了
我的建議能夠直接上手把玩一番, 文章中源碼很少都是解釋性質的內容, 只有當本身試過了之後才能夠理解透徹.
提供一個 index.js
內部就一個console.log('hello world')
, 而後進行打包來檢測
webpack.config.js:
{ // 省略了導出 mode: 'development', entry: { app: './src/index.js', }, output: { filename: '[name].js' }, devtool:'hidden-source-map', // 這樣作生成的代碼中註釋更加少一些, 不是爲了sourceMap }
webpack 默認狀況下會輸出一個 app.js
並且只會有100行(這仍是在有沒用的註釋狀況下), 打開文件後會發現一個IIFE函數, 這裏包含兩部分:
注意:實際上 IIFE 函數內部有些冗餘代碼, 這些冗餘代碼其實是爲特殊狀況和異步狀況準備的, 因此不用太擔憂看不懂, 某些內容結合後續更多分析就看起來很是簡單了.
在同步的引入中, runtime本體內部會顯示的嵌入入口文件的模塊id, 而當前的配置下 webpack 使用文件路徑來做爲 模塊的惟一id.
在IIFE函數執行到尾部的時候 webpack 會利用這個id做爲起點來進行模塊的解析和執行.
IIFE函數的參數:
{ "./src/index.js": (function (module, exports) { console.log('hello world') }) }
圖片:執行流程分析
如今咱們在單純運行代碼的模塊 index.js
中添加一個導出,而後觀察在打包後的文件中會有什麼樣的改變:
{ "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "echo", function () { return echo; }); const echo = () => { console.log('hello world'); } echo(); }) }
果真打包後的內容只有IIFE函數的參數有變化, 咱們定睛一看多出了兩個函數調用, 這是什麼鬼:
__webpack_require__.r
用來給 exports
添加一個描述標識這個模塊是ESM模塊__webpack_require__.d
用於定義模塊導出, 和對於直接向 exports
添加一個屬性不一樣, 使用這個函數定義的屬性都是不可變的.如今咱們來在代碼中添加導入和導出, 來測試一下 webpack 多個同步模塊之間引用是何種狀況.
我創建一個另一個文件 demo.js
這個文件負責導出一個函數, 而原來的 index.js
導入這個函數後執行這個函數.
構建後明顯的變化就是在IIFE函數的參數中, 多了些內容這裏多出去的內容就是新增了一個 demo.js
所引發的.
{ "./src/demo.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "demo", function () { return demo; }); const demo = () => { console.log('hello world') } }), "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _demo__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./demo */ "./src/demo.js"); Object(_demo__WEBPACK_IMPORTED_MODULE_0__["demo"])(); }) }
這裏提示一下 webpack runtime 提供的不一樣函數的功能:
__webpack_require__
字如其名用於引入其餘模塊__webpack_require__.r
用來給 exports
添加一個描述標識這個模塊是ESM模塊__webpack_require__.d
用於定義模塊導出, 和對於直接向 exports
添加一個屬性不一樣, 使用這個函數定義的屬性都是不可變的.那麼在模塊id爲 ./src/index.js
中使用了導入模塊也就是 __webpack_require__
而在 ./src/demo.js
中導出模塊也就是 __webpack_require__.d
.
圖片:執行流程分析
這裏咱們將運行時和 demo.js
and index.js
進行分離, 這裏相較於上一步咱們須要修改一下配置文件:
{ mode: 'development', entry: { app: './src/index.js', }, output: { filename: '[name].js' }, devtool:'hidden-source-map', // 這樣作生成的代碼中註釋更加少一些, 不是爲了sourceMap optimization: { runtimeChunk: { name: 'runtime' // 將runtime分離 }, } }
這個時候runtime被分離爲一個單獨的文件, 而 demo.js
和 index.js
組成一個一個 chunk 叫作 app.js
.
咱們知道瀏覽器在加載一個文件的時候, 默認的狀況下是解析 HTML 中全部的script標籤中的內容後才執行的.
也就是說 javascript
在瀏覽器中的執行會受到 script 標籤順序的影響.
顯然 app.js
的執行是依賴 runtime.js
的, 那麼違反了加載順序是否能夠正常執行呢?
答案:能夠 webpack 運行時徹底做爲最後一個文件加載, 換句話說就是不會受加載順序的影響, 以及是否同步加載的影響.
此時輸出的 runtime.js
代碼向較於同步的版本, 多出了一半的代碼, 這些代碼就是用於處理多個文件直接加載處理的.
長話短說以前的多文件同步加載的版本, 只有模塊的概念(模塊全局惟一). 當有多個文件的時候咱們會將文件進行拆分爲 chunk 此時模塊就屬於 chunk 中的內容.
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([ ["app"], // chunk 名稱 { // 模塊 "./src/demo.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, "demo", function () { return demo; }); const demo = () => { console.log('hello world') } }), "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _demo__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./demo */ "./src/demo.js"); Object(_demo__WEBPACK_IMPORTED_MODULE_0__["demo"])(); }) }, [["./src/index.js", "runtime"]] // 第二列表項後的內容是這個chunk所依賴的chunk ]);
那麼 webpack 運行時是如何作到文件順序加載亂掉也能夠正常執行內容呢?
webpack 運行時內部維護了一個數組變量, 這個變量被掛載在 window 對象上:
window["webpackJsonp"] = []
不管是 runtime 仍是普通的 chunk 都會在IIFE函數中試圖去讀取這個屬性, 若是沒有讀取到就爲其賦值一個數組.
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(xxx) // 處了runtime,每個chunk都有哦
runtime 若是讀取到了數組, 也就意味着 runtime 加載以前有其餘 chunk 加載了, 此時 runtime 只要讀取這個數組中的內容而後在進行解析上以前加載完成的 chunk 就OK了(在實際操做中他會修改這個數組對象改變其行爲,使得後續的 chunk 調用的其實是 runtime 上的 chunk 解析函數).
圖片:執行流程分析
import()
語法import()
語法纔是 webpack 中實打實的異步模塊. 當你使用 import()
的語法來懶加載模塊的時候 runtime 又會提供一些包裝, 沒錯咱們的 runtime 中的代碼又變多了.
爲了增長點難度咱們多增長了一個文件, 不過這是最後一節了, 放心吧難度不會再次提升了.
demo1.js
中導出的函數而且再次導出.import()
語法來引入 demo.js
中的導出內容.// demo1.js export const echo = ()=>{ console.log('hello world') }
// demo.js export { echo } from "./demo1";
// index.js import(/* webpackChunkName: "demo" */'./demo').then(({echo})=>{ echo(); });
咱們來看一下 app.js
作了什麼:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["app"], { "./src/index.js": (function (module, exports, __webpack_require__) { __webpack_require__.e(/*! import() | demo */ "demo") .then(__webpack_require__.bind(null, /*! ./demo */ "./src/demo.js")) .then(({ echo }) => { echo(); }) }) }, [["./src/index.js", "runtime"]]]);
這裏的引入模塊使用了 __webpack_require__.e
再次以前這個 API 是不存在的.
並且它後面還鏈接了兩個 then 第一個是獲取依賴模塊的導出, 第二個是用於依賴的執行.
__webpack_require__.e
主要完成了以下的事情:
./index.js
是入口模塊 runtime 會首先執行對應的函數, 此時`__webpack_require__.e
被調用Promise
的 resolve
和 reject
包括這個 Promise
返回的對象都掛載到內部的 chunk緩存上.publicPath
runtime在內部拼接出 url 到 script 標籤上, 向服務器發起腳本請求.window['webpackJsonp'].push
方法, push方法內部會讀取 chunk 緩存, 遇到 Promise 會執行它的 resolve
.then
被調用, 調用前咱們的 chunk 就已經解析完畢, 此時可使用 __webpack_require__
來獲取到模塊, 而後在第二個 then
中讀取模塊提供的內容.圖片:執行流程分析