webpack原理(一) 輸出文件

新聞

重磅!滴滴跨端框架Chameleon 1.0正式發佈
前端也有AI代碼補全工具了!
Vue 最黑暗的一天

說明

本文將會介紹wepack實現js模塊化,以及模塊的同步,異步引入的基本原理。主要參考自 webpack原理,部分源碼的分析加入了自己的理解,對webpack感興趣的同學可以閱讀參考文章進一步學習。

基本介紹

概念

在瞭解 Webpack 原理前,需要掌握以下幾個核心概念:

  • Entry:入口,Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。
  • Module:模塊,在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模塊。
  • Chunk:代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。
  • Loader:模塊轉換器,用於把模塊原內容按照需求轉換成新內容。
  • Plugin:擴展插件,在 Webpack 構建流程中的特定時機會廣播出對應的事件,插件可以監聽這些事件的發生,在特定時機做對應的事情。

流程概括

Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行以下流程:

  1. 初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數;
  2. 開始編譯:用上一步得到的參數初始化 Compiler 對象,加載所有配置的插件,執行對象的 run 方法開始執行編譯;
  3. 確定入口:根據配置中的 entry 找出所有的入口文件;
  4. 編譯模塊:從入口文件出發,調用所有配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理;
  5. 完成模塊編譯:在經過第4步使用 Loader 翻譯完所有模塊後,得到了每個模塊被翻譯後的最終內容以及它們之間的依賴關係;
  6. 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內容的最後機會;
  7. 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和文件名,把文件內容寫入到文件系統。

流程細節

Webpack 的構建流程可以分爲以下三大階段:

  1. 初始化:啓動構建,讀取與合併配置參數,加載 Plugin,實例化 Compiler。
  2. 編譯:從 Entry 發出,針對每個 Module 串行調用對應的 Loader 去翻譯文件內容,再找到該 Module 依賴的 Module,遞歸地進行編譯處理。
  3. 輸出:對編譯後的 Module 組合成 Chunk,把 Chunk 轉換成文件,輸出到文件系統。

如果只執行一次構建,以上階段將會按照順序各執行一次。但在開啓監聽模式下,流程將變爲如下:
在這裏插入圖片描述

代碼分析

同步依賴

首先我們構建一個簡單的webpack項目,構建過程可以參照這裏

//main.js
// 通過 CommonJS 規範導入 show 函數
const show = require('./show.js');
// 執行 show 函數
show('Webpack');

可以看到main.js同步的引用了show.js。這種模塊引用是怎麼在瀏覽器端實現的呢?
我們知道在node中,對於一個引用的模塊,通過讀取模塊路徑 -> 編譯模塊代碼 -> 執行模塊來載入模塊。這是因爲node是服務端語言,模塊都是本地文件,可同步阻塞進行模塊文件尋址、讀取、編譯和執行,這些過程在模塊require的時候再“按需”執行即可。
而webpack運行在客戶端(瀏覽器),顯然不能在需要時再通過網絡加載js文件,因爲網絡加載是異步進行的,不能滿足同步的要求。我們通過分析編譯後的bundle.js分析同步依賴的實現。

(
    // webpackBootstrap 啓動函數
    // modules 即爲存放所有模塊的數組,數組中的每一個元素都是一個函數
    function (modules) {
        // 安裝過的模塊都存放在這裏面
        // 作用是把已經加載過的模塊緩存在內存中,提升性能
        var installedModules = {};

        // 去數組中加載一個模塊,moduleId 爲要加載模塊在數組中的 index
        // 作用和 Node.js 中 require 語句相似
        function __webpack_require__(moduleId) {
            // 如果需要加載的模塊已經被加載過,就直接從內存緩存中返回
            if (installedModules[moduleId]) {
                return installedModules[moduleId].exports;
            }

            // 如果緩存中不存在需要加載的模塊,就新建一個模塊,並把它存在緩存中
            var module = installedModules[moduleId] = {
                // 模塊在數組中的 index
                i: moduleId,
                // 該模塊是否已經加載完畢
                l: false,
                // 該模塊的導出值
                exports: {}
            };

            // 從 modules 中獲取 index 爲 moduleId 的模塊對應的函數
            // 再調用這個函數,同時把函數需要的參數傳入
            modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
            // 把這個模塊標記爲已加載
            module.l = true;
            // 返回這個模塊的導出值
            return module.exports;
        }

        // Webpack 配置中的 publicPath,用於加載被分割出去的異步代碼
        __webpack_require__.p = "";

        // 使用 __webpack_require__ 去加載 index 爲 0 的模塊,並且返回該模塊導出的內容
        // index 爲 0 的模塊就是 main.js 對應的文件,也就是執行入口模塊
        // __webpack_require__.s 的含義是啓動模塊對應的 index
        return __webpack_require__(__webpack_require__.s = 0);

    })(

    // 所有的模塊都存放在了一個數組裏,根據每個模塊在數組的 index 來區分和定位模塊
    [
        /* 0 */
        (function (module, exports, __webpack_require__) {
            // 通過 __webpack_require__ 規範導入 show 函數,show.js 對應的模塊 index 爲 1
            const show = __webpack_require__(1);
            // 執行 show 函數
            show('Webpack');
        }),
        /* 1 */
        (function (module, exports) {
            function show(content) {
                window.document.getElementById('app').innerText = 'Hello,' + content;
            }
            // 通過 CommonJS 規範導出 show 函數
            module.exports = show;
        })
    ]
);

以上看上去複雜的代碼其實是一個立即執行函數,可以簡寫爲如下:

(function(modules) {
  
  // 模擬 require 語句
  function __webpack_require__() {
  }
  
  // 執行存放所有模塊數組中的第0個模塊
  __webpack_require__(0);
 
})([/*存放所有模塊的數組*/])

這是一個自執行的函數,參數是所有modules的數組,__webpack_require__是在瀏覽器執行的,模擬node中require的函數,用於加載模塊,加載模塊的來源就是作爲參數的modules數組。這是因爲瀏覽器端不可能每次加載都通過網絡請求,所以將全部模塊通過一次 請求加載到數組中,在需要時,再通過__webpack_require__加載依賴模塊。
如果仔細分析 webpack_require 函數的實現,你還有發現 Webpack 做了緩存優化: 執行加載過的模塊不會再執行第二次,執行結果會緩存在內存中,當某個模塊第二次被訪問時會直接去內存中讀取被緩存的返回值。

異步依賴

當我們將main.js改爲下面的形式,對show.js的依賴變爲異步。

// 異步加載 show.js
import("./show").then(show => {
    // 執行 show 函數
    show("Webpack");
});

重新構建後會輸出兩個文件,分別是執行入口文件 bundle.js 和 異步加載文件 0.bundle.js(以下簡稱bundle0.js)。
其中 bundle0.js 內容如下:

// 加載在本文件(0.bundle.js)中包含的模塊
webpackJsonp(
  // 在其它文件中存放着的模塊的 ID
  [0],
  // 本文件所包含的模塊
  [
    // show.js 所對應的模塊
    (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  ]
);

bundle.js

(function (modules) {
  /***
   * webpackJsonp 用於從異步加載的文件中安裝模塊。
   * 把 webpackJsonp 掛載到全局是爲了方便在其它文件中調用。
   *
   * @param chunkIds 異步加載的文件中存放的需要安裝的模塊對應的 Chunk ID
   * @param moreModules 異步加載的文件中存放的需要安裝的模塊列表
   * @param executeModules 在異步加載的文件中存放的需要安裝的模塊都安裝成功後,需要執行的模塊對應的 index
   */
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // 把 moreModules 添加到 modules 對象中
    // 把所有 chunkIds 對應的模塊都標記成已經加載成功 
    var moduleId, chunkId, i = 0, resolves = [], result;
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    while (resolves.length) {
      resolves.shift()();
    }
  };

  // 緩存已經安裝的模塊
  var installedModules = {};

  // 存儲每個 Chunk 的加載狀態;
  // 鍵爲 Chunk 的 ID,值爲0代表已經加載成功
  var installedChunks = {
    1: 0
  };

  // 模擬 require 語句,和上面介紹的一致
  function __webpack_require__(moduleId) {
    // ... 省略和上面一樣的內容
  }

  /**
   * 用於加載被分割出去的,需要異步加載的 Chunk 對應的文件
   * @param chunkId 需要異步加載的 Chunk 對應的 ID
   * @returns {Promise}
   */
  __webpack_require__.e = function requireEnsure(chunkId) {
    // 從上面定義的 installedChunks 中獲取 chunkId 對應的 Chunk 的加載狀態
    var installedChunkData = installedChunks[chunkId];
    // 如果加載狀態爲0表示該 Chunk 已經加載成功了,直接返回 resolve Promise
    if (installedChunkData === 0) {
      return new Promise(function (resolve) {
        resolve();
      });
    }

    // installedChunkData 不爲空且不爲0表示該 Chunk 正在網絡加載中
    if (installedChunkData) {
      // 返回存放在 installedChunkData 數組中的 Promise 對象
      return installedChunkData[2];
    }

    // installedChunkData 爲空,表示該 Chunk 還沒有加載過,去加載該 Chunk 對應的文件
    var promise = new Promise(function (resolve, reject) {
      installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise;

    // 通過 DOM 操作,往 HTML head 中插入一個 script 標籤去異步加載 Chunk 對應的 JavaScript 文件
    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.async = true;
    script.timeout = 120000;

    // 文件的路徑爲配置的 publicPath、chunkId 拼接而成
    script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";

    // 設置異步加載的最長超時時間
    var timeout = setTimeout(onScriptComplete, 120000);
    script.onerror = script.onload = onScriptComplete;

    // 在 script 加載和執行完成時回調
    function onScriptComplete() {
      // 防止內存泄露
      script.onerror = script.onload = null;
      clearTimeout(timeout);

      // 去檢查 chunkId 對應的 Chunk 是否安裝成功,安裝成功時纔會存在於 installedChunks 中
      var chunk = installedChunks[chunkId];
      if (chunk !== 0) {
        if (chunk) {
          chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
        }
        installedChunks[chunkId] = undefined;
      }
    };
    head.appendChild(script);

    return promise;
  };

  // 加載並執行入口模塊,和上面介紹的一致
  return __webpack_require__(__webpack_require__.s = 0);
})
(
  // 存放所有沒有經過異步加載的,隨着執行入口文件加載的模塊
  [
    // main.js 對應的模塊
    (function (module, exports, __webpack_require__) {
      // 通過 __webpack_require__.e 去異步加載 show.js 對應的 Chunk
      __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
        // 執行 show 函數
        show('Webpack');
      });
    })
  ]
);

可以簡化爲

(function (modules) {
  window["webpackJsonp"] = function (chunkIds, moreModules, executeModules) {
	  //....
  }
	  // 緩存已經安裝的模塊
  var installedModules = {};

  // 存儲每個 Chunk 的加載狀態;
  // 鍵爲 Chunk 的 ID,值爲0代表已經加載成功
  var installedChunks = {
    1: 0
  };

  // 模擬 require 語句,和上面介紹的一致
  function __webpack_require__(moduleId) {
    // ... 省略和上面一樣的內容
  }
   __webpack_require__.e = function requireEnsure(chunkId) {
   // ...
   }
    // 加載並執行入口模塊,和上面介紹的一致
  return __webpack_require__(__webpack_require__.s = 0);
})([
	 (function (module, exports, __webpack_require__) {
      __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
        // 執行 show 函數
        show('Webpack');
      });
    })
])

接下來按照執行順序來講下代碼邏輯。

執行參數模塊

首先通過__webpack_require__(webpack_require.s = 0)方法,會執行參數數組裏第一個元素,即下面的函數。

function (module, exports, __webpack_require__) {
      __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
        // 執行 show 函數
        show('Webpack');
      });
}

這裏通過 webpack_require.e(0) 加載0.bundle.js,加載完畢後,通過__webpack_require__通過加載moduleId是1的模塊,即來自於show.js的模塊。最後通過得到的show參數,執行show(‘Webpack’)。

webpack_require.e

簡單來說__webpack_require__.e 會根據chunkId判斷這個chunk是否已經加載,如果沒有將會在html body裏插入script標籤,加載bundle0,並返回一個promise對象。

webpackJsonp

當bundle0.js被加載到body裏,就開始執行webpackJsonp方法。我們先看下它是做什麼的。

 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // 把 moreModules 添加到 modules 對象中
    // 把所有 chunkIds 對應的模塊都標記成已經加載成功 
    var moduleId, chunkId, i = 0, resolves = [], result;
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    while (resolves.length) {
      resolves.shift()();
    }
  };

簡單來說他做了三件事:

  • 將 installedChunks[chunkId] 置爲0,用於__webpack_require__.e中判斷文件加載狀態
  • 將異步的module加入modules對象裏,key值爲moduleId。
  • 執行promise 的resolve方法。

webpack_require(1)

當執行到

then(__webpack_require__.bind(null, 1))

也就是執行__webpack_require__(1),運行時執行moduleId等於1的module,即運行下面的函數:

function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    }

show函數作爲返回值,傳給下一個then。

show(‘Webpack’)

最後執行到這裏,整個過程結束。

這裏的 bundle.js 和上面所講的同步依賴的bundle.js 非常相似,區別在於:

  • 多了一個 webpack_require.e 用於加載被分割出去的,需要異步加載的 Chunk 對應的文件;
  • 多了一個 webpackJsonp 函數用於從異步加載的文件中安裝模塊。

在配置 splitChunks 去提取公共代碼時輸出的文件和使用了異步加載時輸出的文件是一樣的,都會有 webpack_require.e 和 webpackJsonp。 原因在於提取公共代碼和異步加載本質上都是代碼分割。

參考

webpack原理-輸出文件分析
前端運行時的模塊化設計與實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章