webpack輸出文件分析

打包原理

  • 簡單講就是生成ast語法樹,根據語法樹生成對應的js代碼
  • 這裏僅分析打包輸出的結果,從結果分析webpack對咱們代碼作了啥

分析

  • 入口文件
// main.js
// 經過CommonJS規範導入
const show = require('./show.js');
// 執行 show 函數
show('Webpack');
  • 依賴文件
// show.js
// 操做 DOM 元素,把 content 顯示到網頁上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 經過 CommonJS 規範導出 show 函數
module.exports = show;
  • 打包結果
// 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是一個自執行函數,入參就是main.jsshow.js改造後的代碼塊所構成的數組
  • 自執行函數裏運行了__webpack_require__這個函數,入參是0,0其實就是代碼塊數組中對應的入參,表示第一個代碼塊
  • 再來看__webpack_require__函數,首先執行的是緩存判斷,經過moduleId判斷以前是否已經加載過,若是加載過,直接返回直接的加載結果exportsmouduleId就是不一樣代碼模塊在入參數組中的index
  • 而若是沒有加載過,則新建一個對象,重要的是這個對象中的exports屬性,裏面存放的就是加載模塊後,對應模塊export出來的東西
  • 而後用這個exports做爲上下文去執行對應的代碼塊,傳遞參數爲剛纔新建的module,module裏的exports,以及__webpack_require__這個方法自己
  • 而後看到main.js中的require被改形成了__webpack_require____webpack_require__(1)表明加載第二個代碼塊
  • 第二個代碼塊中,定義了show這個方法,而後show會做爲module.exports的導出,也就是賦值給了installedModules[0].module.exports,也就是這個導出已經被緩存起來了,下次再有別的地方用到,會直接被導出
  • 這就是webpack大體的打包思路,將各個單獨的模塊改形成數組做爲入參,傳給自執行函數,同時維護一個installedModules記錄加載過的模塊,利用模塊數組中的index做爲key值,exports記錄導出對象

按需加載

  • 因爲單頁應用也會有路由這個概念,在沒有切換到對應路由以前,可能並不但願瀏覽器對這部分頁面的js進行下載,從而提高首頁打開的速度,就涉及到一個懶加載,即按需加載的問題
  • webpack的按需加載是經過import(XXX)實現的,import()是一個提案,而webpack支持了它
// 異步加載 show.js
import(/* webpackChunkName: 'show' */ './show').then((module) => {
  // 執行 show 函數
  const show = module.default;
  show('Webpack');
});
  • 經過這種方式打包,咱們能夠發現最終打包出來的文件分紅了兩個,bundle.jsshow.xxx.js
  • 其中/* webpackChunkName: 'show' */是專門註釋給webpack看的,爲的是指定按需加載的包的名字,同時記得在webpack的配置文件的entry中,配置chunkFilename: '[name].[hash].js',否則這個指定不會生效
  • 先來看入口文件,將暫時沒有用到的函數都隱藏後以下:
(function (modules) {
  // webpackJsonp 用於從異步加載的文件中安裝模塊
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // ... 先省略
  };

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

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

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

  // 用於加載被分割出去的,須要異步加載的 Chunk 對應的文件
  __webpack_require__.e = function requireEnsure(chunkId) {
    // ... 先省略
  };

  // 加載並執行入口模塊,和上面介紹的一致
  return __webpack_require__(__webpack_require__.s = 0);
})
(
  // 存放全部沒有通過異步加載的,隨着執行入口文件加載的模塊
  [
    // main.js 對應的模塊
    (function (module, exports, __webpack_require__) {
      // 經過 __webpack_require__.e 去異步加載 show.js 對應的 Chunk
      __webpack_require__.e('show').then(__webpack_require__.bind(null, 'show')).then((show) => {
        // 執行 show 函數
        show('Webpack');
      });
    })
  ]
);
  • 能夠看到import(xxx).then被替換成了__webpack_require__.e(0).then__webpack_require__.e(0)返回了一個promise
  • 第一個then裏至關於執行了__webpack_require__(1),但很明顯能夠看到自執行函數的入參數組只有一個元素,不存在[1],這個[1]是何時被插入的呢
  • 看一下__webpack_require__.e的實現
__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;
  };
  • 首先判斷這個chunkId是否已經加載過,若是是的話,直接返回一個resolve的promise
  • 若是不爲空又不爲0,說明正在加載中,這裏的installedChunks[chunkId]是一個數組,裏面保存着[resovle, reject],是在發起網絡請求的時候賦值的
  • 若是上面兩個判斷都沒擊中,說明是沒有加載過,下面開始構造加載方法,主要是經過jsonp的形式
  • 首先新建一個promise,並對installedChunks[chunkId]賦值,把這個promise以及他的resolve和reject保存在裏面,這也是上面爲何能夠經過判斷installedChunks[chunkId]不爲空又不爲0即正處於請求當中,直接返回數組第三個值,即新建的promise,讓後續操做能夠在這個promise上進行回調的註冊
  • 而後後面的方法就是經過構造一個script標籤,插入到head中,保證代碼能立刻被下載,同時定義代碼執行完畢時的回調,判斷是已經加載了代碼,若是加載成功清除監聽等,若是加載失敗,拋出異常
  • 最後返回這個promise,供外部註冊回調
  • 而這裏經過jsonp加載的代碼就是打包分離出來的另外一個文件show.xx.js,也就是異步加載的show.js相關的代碼
webpackJsonp(
  // 在其它文件中存放着的模塊的 ID
  ['show'],
  // 本文件所包含的模塊
  {// show.js 所對應的模塊
    show: (function (module, exports) {
      function show(content) {
        window.document.getElementById('app').innerText = 'Hello,' + content;
      }

      module.exports = show;
    })
  }
);
  • 接着看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()();
    }
  };
  • chunkIds表明本身這個文件的id名,由於動態加載的時候,是利用動態加載的文件名構成script標籤進行下載的,這裏傳入這個id是爲了觸發後續promise的resolve以及標記模塊以及被加載
  • moreModules就是對應的代碼模塊集合
  • executeModules 就是加載完成後須要被執行模塊的index
  • 首先遍歷installedChunks,前面提到過installedChunks[chunkId]經過網絡下載的時候,回賦予三個值,表明其對應的promise,這裏取出第一個resolve,保存起來,同時將加載標記置爲0,表示已加載
  • 而後遍歷動態加載的模塊,把代碼塊塞到modules數組裏
  • 最後執行以前保存下來的resolve函數,觸發__webpack_require__.e(0).then的執行
  • 這樣動態加載的代碼經過構造jsonp進行下載,而且將對應代碼傳到bundle.js的modules中進行保存,而後在then函數中經過__webpack_require__執行模塊,緩存輸出
  • 這裏爲了便於理解,有對代碼作必定調整,真實的輸出狀況,能夠經過具體打包輸出查看,這裏僅描述具體打包思路
相關文章
相關標籤/搜索