Webpack原理-輸出文件分析

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

先來看看由 1-3安裝與使用 中最簡單的項目構建出的 bundle.js 文件內容,代碼以下:html

(
    // 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;
        })
    ]
);
複製代碼

以上看上去複雜的代碼實際上是一個當即執行函數,能夠簡寫爲以下:java

(function(modules) {
  
  // 模擬 require 語句
  function __webpack_require__() {
  }
  
  // 執行存放全部模塊數組中的第0個模塊
  __webpack_require__(0);
  
})([/*存放全部模塊的數組*/])
複製代碼

bundle.js 能直接運行在瀏覽器中的緣由在於輸出的文件中經過 __webpack_require__ 函數定義了一個能夠在瀏覽器中執行的加載函數來模擬 Node.js 中的 require 語句。webpack

原來一個個獨立的模塊文件被合併到了一個單獨的 bundle.js 的緣由在於瀏覽器不能像 Node.js 那樣快速地去本地加載一個個模塊文件,而必須經過網絡請求去加載還未獲得的文件。 若是模塊數量不少,加載時間會很長,所以把全部模塊都存放在了數組中,執行一次網絡加載。web

若是仔細分析 __webpack_require__ 函數的實現,你還有發現 Webpack 作了緩存優化: 執行加載過的模塊不會再執行第二次,執行結果會緩存在內存中,當某個模塊第二次被訪問時會直接去內存中讀取被緩存的返回值。數組

分割代碼時的輸出

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

例如把源碼中的 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》全書在線閱讀連接

閱讀原文

相關文章
相關標籤/搜索