Webpack原理-輸出檔案分析

浩麟發表於2018-01-03

雖然在前面的章節中你學會了如何使用 Webpack ,也大致知道其工作原理,可是你想過 Webpack 輸出的 bundle.js 是什麼樣子的嗎? 為什麼原來一個個的模組檔案被合併成了一個單獨的檔案?為什麼 bundle.js 能直接執行在瀏覽器中? 本節將解釋清楚以上問題。

先來看看由 1-3安裝與使用 中最簡單的專案構建出的 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);
  
})([/*存放所有模組的陣列*/])
複製程式碼

bundle.js 能直接執行在瀏覽器中的原因在於輸出的檔案中通過 __webpack_require__ 函式定義了一個可以在瀏覽器中執行的載入函式來模擬 Node.js 中的 require 語句。

原來一個個獨立的模組檔案被合併到了一個單獨的 bundle.js 的原因在於瀏覽器不能像 Node.js 那樣快速地去本地載入一個個模組檔案,而必須通過網路請求去載入還未得到的檔案。 如果模組數量很多,載入時間會很長,因此把所有模組都存放在了陣列中,執行一次網路載入。

如果仔細分析 __webpack_require__ 函式的實現,你還有發現 Webpack 做了快取優化: 執行載入過的模組不會再執行第二次,執行結果會快取在記憶體中,當某個模組第二次被訪問時會直接去記憶體中讀取被快取的返回值。

分割程式碼時的輸出

在採用了 4-12 按需載入 中介紹過的優化方法時,Webpack 的輸出檔案會發生變化。

例如把原始碼中的 main.js 修改為如下:

// 非同步載入 show.js
import('./show').then((show) => {
  // 執行 show 函式
  show('Webpack');
});
複製程式碼

重新構建後會輸出兩個檔案,分別是執行入口檔案 bundle.js 和 非同步載入檔案 0.bundle.js

其中 0.bundle.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');
      });
    })
  ]
);
複製程式碼

這裡的 bundle.js 和上面所講的 bundle.js 非常相似,區別在於:

  • 多了一個 __webpack_require__.e 用於載入被分割出去的,需要非同步載入的 Chunk 對應的檔案;
  • 多了一個 webpackJsonp 函式用於從非同步載入的檔案中安裝模組。

在使用了 CommonsChunkPlugin 去提取公共程式碼時輸出的檔案和使用了非同步載入時輸出的檔案是一樣的,都會有 __webpack_require__.ewebpackJsonp。 原因在於提取公共程式碼和非同步載入本質上都是程式碼分割。

本例項提供專案完整程式碼

Webpack原理-輸出檔案分析

《深入淺出Webpack》全書線上閱讀連結

閱讀原文

相關文章