探索webpack運行時

前言

本篇文章建議親自動手嘗試.javascript

最近研究了 webpack 運行時源碼, 在這篇文章中記錄了個人探索 webpack 這個複雜的玩具方式, 而且以圖形的形式將 webpack 運行時的流程給記錄的下來.java

咱們討論的是什麼

這篇文章主要記錄的是 webpack 在將文件打包後是如何在瀏覽器中加載以及解析的流程.node

手段

webpack 實在是太複雜的了, 爲了不額外的干擾, 我使用最小化實例的方式來進行探究 webpack 是如何加載和解析模塊文件的. webpack

具體的手段:首先準備幾個很是簡單的 js 文件, 其次準備其對應的 webpack 配置文件, 逐一測試後觀察其輸出, 閱讀其源碼, 而後得出結論.web

簡單總結

webpack 的運行時在解析同步模塊的時候就和 nodejs 規則一模一樣. 數組

什麼意思, 每個模塊在運行前都會被一個函數進行包裹:瀏覽器

(function (module, exports, __webpack_require__) {
    // do something
})

看起來和 nodejs 同樣, 做用起來也一致, 這裏的當前模塊的導出模塊和導入模塊就是:緩存

  • exports
  • _webpack_require_

而當一個模塊被須要的時候, 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 自己就能夠了

建議

我的建議能夠直接上手把玩一番, 文章中源碼很少都是解釋性質的內容, 只有當本身試過了之後才能夠理解透徹.

實踐

同步模塊-不分離runtime

提供一個 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函數, 這裏包含兩部分:

  • runtime自己, 即 IIFE 函數體
  • 模塊內容, 及 IIFE 函數的參數

注意:實際上 IIFE 函數內部有些冗餘代碼, 這些冗餘代碼其實是爲特殊狀況和異步狀況準備的, 因此不用太擔憂看不懂, 某些內容結合後續更多分析就看起來很是簡單了.

在同步的引入中, runtime本體內部會顯示的嵌入入口文件的模塊id, 而當前的配置下 webpack 使用文件路徑來做爲 模塊的惟一id.

在IIFE函數執行到尾部的時候 webpack 會利用這個id做爲起點來進行模塊的解析和執行.

IIFE函數的參數:

{
    "./src/index.js": (function (module, exports) {
        console.log('hello world')
    })
}

圖片:執行流程分析

圖片描述

同步模塊-不分離runtime-導入和執行

如今咱們在單純運行代碼的模塊 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 添加一個屬性不一樣, 使用這個函數定義的屬性都是不可變的.

同步模塊-不分離runtime-多個文件

如今咱們來在代碼中添加導入和導出, 來測試一下 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.jsindex.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 導出一個函數.
  • demo.js 引入 demo1.js 中導出的函數而且再次導出.
  • index.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 主要完成了以下的事情:

  1. ./index.js 是入口模塊 runtime 會首先執行對應的函數, 此時`__webpack_require__.e 被調用
  2. 在內部建立一個Promise, 將這個 Promiseresolve reject 包括這個 Promise 返回的對象都掛載到內部的 chunk緩存上.
  3. 根據提供的名稱, 以及 publicPath runtime在內部拼接出 url 到 script 標籤上, 向服務器發起腳本請求.
  4. 監聽 script 標籤的完成和失敗事件, 作善後處理
  5. chunk 會進行下載->執行, 調用 window['webpackJsonp'].push 方法, push方法內部會讀取 chunk 緩存, 遇到 Promise 會執行它的 resolve .
  6. resolve後 then 被調用, 調用前咱們的 chunk 就已經解析完畢, 此時可使用 __webpack_require__ 來獲取到模塊, 而後在第二個 then 中讀取模塊提供的內容.

圖片:執行流程分析

圖片描述

相關文章
相關標籤/搜索