webpack 4 源碼主流程分析(十二):打包後文件解析

原文首發於 blog.flqin.com。若有錯誤,請聯繫筆者。分析碼字不易,轉載請代表出處,謝謝!javascript

前言及總流程概覽 裏的 demo 爲例, 前七章分析了打包過程,如今來分析它打包後的文件。html

demo

//src/a.js
import { add } from 'Src/b';
import('./c.js').then(m => m.sub(2, 1));
const a = 1;
add(3, 2 + a);
複製代碼
//src/b.js
import { mul } from '@fe_korey/test-loader?number=20!Src/e';
export function add(a, b) {
  return a + b + mul(10, 5);
}
export function addddd(a, b) {
  return a + b * b;
}
複製代碼
//src/c.js
import { mul } from 'Src/d';
import('./b.js').then(m => m.add(200, 100)); //require.ensure() 是 webpack 特有的,已經被 import() 取代。
export function sub(a, b) {
  return a - b + mul(100, 50);
}
複製代碼
//src/d.js
export function mul(a, b) {
  const d = 10000;
  return a * b + d;
}
複製代碼
//webpack.config.js
var path = require('path');

module.exports = {
  entry: {
    bundle: './src/a.js'
  },
  devtool: 'none',
  output: {
    path: __dirname + '/dist',
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:8].js'
  },
  mode: 'development',
  resolve: {
    alias: {
      Src: path.resolve(__dirname, 'src/')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader'
          }
        ]
      }
    ]
  }
};
複製代碼
//babel.config.js
module.exports = {
  presets: ['@babel/env']
};
複製代碼

@fe_korey/test-loader 是一個測試 loader,該 loader 做用爲代碼裏的字符串 10000 替換爲傳入的 numberjava

打包結果文件

根據項目配置及同步異步的關係,打包後一共生成兩個文件:node

  • bundle.xxxx.js

總代碼:見 githubwebpack

入口文件,該文件名根據配置:entryoutput.filename 生成,裏面包含 webpack runtime 代碼和同步模塊代碼。git

如若配置了 html-webpack-plugin,那麼在生成的 html 裏將只會引入此 js 文件。es6

  • 0.xxxxxxxx.js

總代碼:見 githubgithub

非入口文件,本例爲異步 chunk 文件,該文件名根據配置: output.chunkFilename生成,裏面包含異步模塊代碼。web

代碼執行流程

根據代碼執行順序來分析,html 文件只需引入了bundle.xxxx.js文件,則從該文件開始執行,若是有其餘 import 後,會先跳到對應的 module 進行處理,即先序深度優先遍歷算法遞歸該依賴樹。算法

bundle 主體結構

(function(modules) {
  //runtime代碼
})({
  './node_modules/@fe_korey/test-loader/loader.js?number=20!./src/d.js': function(module, __webpack_exports__, __webpack_require__) {
    //...模塊代碼d
  },
  './src/a.js': function(module, __webpack_exports__, __webpack_require__) {
    //...模塊代碼a
  },
  './src/b.js': function(module, __webpack_exports__, __webpack_require__) {
    //...模塊代碼b
  }
});
複製代碼

主體結構爲一個自執行函數,函數體爲 runtime 函數,參數爲 modules 對象,各模塊以 key-value 的形式一塊兒存在該 modules 對象裏。當前 key 爲模塊的路徑,value 爲包裹模塊代碼的一個函數。

runtime 函數

runtime 指的是 webpack 的運行環境(具體做用就是模塊解析, 加載) 和 模塊信息清單(表如今 jsonpScriptSrc 方法裏)。

配置項 optimization.runtimeChunk 能夠設置 webpackruntime 這部分代碼單獨打包。

runtime 函數主體結構

function(modules){
  function webpackJsonpCallback(data){
    //...
  }
  // 設置 script src __webpack_require__.p 即爲 output.publicPath 配置
  function jsonpScriptSrc(chunkId){
    return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0":"d680ffbe"}[chunkId] + ".js"
  }
  function __webpack_require__(moduleId){
    //...
  }
  var installedModules = {};

  var installedChunks = {"bundle": 0};

  // 定義一堆掛載在__webpack_require__上的屬性
  //...

  // jsonp 初始化
  // ...

  return __webpack_require__(__webpack_require__.s = "./src/a.js");
}
複製代碼

開始執行

代碼開始執行:

var installedModules = {};
複製代碼

初始化 installedModules,保存全部建立過的 module,用於緩存判斷。

// undefined:chunk未加載, null: chunk經過prefetch/preload提早獲取過
// Promise:chunk正在加載, 0:chunk加載完畢
// 數組: 結構爲 [resolve Function, reject Function, Promise] 的數組, 表明 chunk 在處於加載中
var installedChunks = {
  bundle: 0
};
複製代碼

installedChunkskey-value 的形式,用於收集保存全部的 chunk,這裏 bundle 就是指的當前 chunk,天然是已經加載好了的。

__webpack_require__ 屬性

而後定義了一堆 __webpack_require__ 的屬性:

// 異步處理
__webpack_require__.e = function requireEnsure(chunkId) {
  //後文單獨分析
};

// 即爲傳入的modules:各模塊組成的對象
__webpack_require__.m = modules;

// 即爲installedModules:已經緩存的對象
__webpack_require__.c = installedModules;

// 在exports對象上添加屬性,即 增長導出
__webpack_require__.d = function(exports, name, getter) {
  if (!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

// 在exports對象上添加 __esModule 屬性,用於標識 es6 模塊
__webpack_require__.r = function(exports) {
  if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  }
  Object.defineProperty(exports, '__esModule', { value: true });
};

// 建立一個僞命名空間對象
__webpack_require__.t = function(value, mode) {
  //沒用上,解釋暫時略過
};

// 獲得 getDefaultExport,即經過 __esModule 屬性判斷是不是 es6 來肯定對應的默認導出方法
__webpack_require__.n = function(module) {
  var getter =
    module && module.__esModule
      ? function getDefault() {
          return module['default'];
        }
      : function getModuleExports() {
          return module;
        };
  __webpack_require__.d(getter, 'a', getter);
  return getter;
};

// 調用 hasOwnProperty,即判斷對象上是否有某一屬性
__webpack_require__.o = function(object, property) {
  return Object.prototype.hasOwnProperty.call(object, property);
};

// 即爲 publicPath,在output.publicPath配置而來
__webpack_require__.p = '';

// 錯誤處理
__webpack_require__.oe = function(err) {
  console.error(err);
  throw err;
};
複製代碼

每一個屬性的做用已經寫在註釋上面。

jsonp 初始化

而後執行 jsonp 初始化:

var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []); //初始化 window['webpackJsonp']對象
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 暫存 push 方法
jsonpArray.push = webpackJsonpCallback; //重寫 jsonpArray 的 push 方法爲 webpackJsonpCallback
jsonpArray = jsonpArray.slice(); //拷貝 jsonpArray(不帶 push 方法)
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); //若入口文件加載前,chunks文件先加載了,遍歷 jsonpArray 用 webpackJsonpCallback 執行
var parentJsonpFunction = oldJsonpFunction; //舊的 push 方法存入 parentJsonpFunction
複製代碼

jsonp 初始化的主要做用就是給 window['webpackJsonp'] 重寫了 push 方法爲 webpackJsonpCallback 。接着執行:

return __webpack_require__((__webpack_require__.s = './src/a.js'));
複製代碼

由入口文件 a 開始,傳入 moduleID : "./src/a.js",執行方法 __webpack_require__

__webpack_require__

function __webpack_require__(moduleId) {
  // 判斷該module是否已經被緩存到installedModules,若是有,則直接返回它的導出exports
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 定義module並緩存
  var module = (installedModules[moduleId] = {
    i: moduleId,
    l: false,
    exports: {}
  });
  // 執行module代碼
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 標誌module已經讀取完成
  module.l = true;

  return module.exports;
}
複製代碼

__webpack_require__ 方法的主要做用就是建立緩存 module 後,執行該 module 的代碼。其中 modules 即爲上文所解釋的各模塊組成的對象。

執行各同步模塊代碼

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 執行模塊 a 的代碼。

// 模塊 a
'use strict';
__webpack_require__.r(__webpack_exports__);
var Src_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/b.js');

__webpack_require__
  .e(0)
  .then(__webpack_require__.bind(null, './src/c.js'))
  .then(function(m) {
    return m.sub(2, 1);
  });
var a = 1;
Object(Src_b__WEBPACK_IMPORTED_MODULE_0__['add'])(3, 2 + a);
複製代碼

代碼裏 __webpack_require__('./src/b.js') 會去執行模塊 b 的代碼:

//模塊 b
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'add', function() {
  return add;
});
__webpack_require__.d(__webpack_exports__, 'addddd', function() {
  return addddd;
});
var _fe_korey_test_loader_number_20_Src_d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./node_modules/@fe_korey/test-loader/loader.js?number=20!./src/d.js');

function add(a, b) {
  return a + b + Object(_fe_korey_test_loader_number_20_Src_d__WEBPACK_IMPORTED_MODULE_0__['mul'])(10, 5);
}
function addddd(a, b) {
  return a + b * b;
}
複製代碼

代碼裏在導出了兩個方法後,去執行模塊 d 的代碼:

//模塊 d
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'mul', function() {
  return mul;
});
function mul(a, b) {
  var d = 20;
  return a * b + d;
}
複製代碼

模塊 d 代碼導出了 mul

import() 的處理

各自模塊執行完後,回到模塊 a 裏執行:

__webpack_require__
  .e(0)
  .then(__webpack_require__.bind(null, './src/c.js'))
  .then(function(m) {
    return m.sub(2, 1);
  });
複製代碼

該打包後的代碼爲動態加載,源代碼爲:

import('./c.js').then(m => m.sub(2, 1));
複製代碼

__webpack_require__.e

__webpack_require__.e 實現異步加載模塊,方法爲:

// promise隊列,等待多個異步 chunk都加載完成才執行回調
var promises = [];

// 先判斷是否加載過該 chunk
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
  // 0 means "already installed".

  // a Promise means "currently loading". 目標 chunk 正在加載,則將 promise push到 promises 數組
  if (installedChunkData) {
    promises.push(installedChunkData[2]);
  } else {
    // 新建一個Promise去異步加載目標chunk
    var promise = new Promise(function(resolve, reject) {
      installedChunkData = installedChunks[chunkId] = [resolve, reject]; //這裏設置 installedChunks[chunkId]
    });
    promises.push((installedChunkData[2] = promise)); // installedChunks[chunkId] = [resolve, reject, promise]

    var script = document.createElement('script');
    var onScriptComplete;

    script.charset = 'utf-8';
    script.timeout = 120;
    if (__webpack_require__.nc) {
      script.setAttribute('nonce', __webpack_require__.nc);
    }
    // 設置src
    script.src = jsonpScriptSrc(chunkId);

    var error = new Error();
    // 設置加載完成或錯誤的回調
    onScriptComplete = function(event) {
      // avoid mem leaks in IE.
      script.onerror = script.onload = null;
      clearTimeout(timeout);
      var chunk = installedChunks[chunkId];
      if (chunk !== 0) {
        if (chunk) {
          var errorType = event && (event.type === 'load' ? 'missing' : event.type);
          var realSrc = event && event.target && event.target.src;
          error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
          error.name = 'ChunkLoadError';
          error.type = errorType;
          error.request = realSrc;
          chunk[1](error);
        }
        installedChunks[chunkId] = undefined;
      }
    };
    // 設置超時處理
    var timeout = setTimeout(function() {
      onScriptComplete({ type: 'timeout', target: script });
    }, 120000);
    //script標籤的onload事件都是在外部js文件被加載完成並執行完成後(異步不算)才被觸發
    script.onerror = script.onload = onScriptComplete;
    // script標籤加入文檔
    document.head.appendChild(script);
  }
}
return Promise.all(promises);
複製代碼

參數 0chunkId,在方法 __webpack_require__.e 裏,主要功能就是模擬 jsonp 去異步加載目標 chunk 文件 0,返回一個 promise 對象。

而後加載異步文件 0.e3296d88.js 並執行。

加載非入口文件0.e3296d88.js

非入口文件主體結構

(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
  [0],
  {
    './src/c.js': function(module, __webpack_exports__, __webpack_require__) {
      //模塊 c
    },

    './src/d.js': function(module, __webpack_exports__, __webpack_require__) {
      //模塊 d
    }
  }
]);
複製代碼

在模塊加載後,就會當即執行的 window['webpackJsonp'].push() 。由 jsonp 初始化可知, 即執行 bundle 文件裏的 webpackJsonpCallback 方法。

webpackJsonpCallback

function webpackJsonpCallback(data) {
  var chunkIds = data[0];
  var moreModules = data[1]; //異步 chunk 的各模塊組成的對象

  var moduleId,
    chunkId,
    i = 0,
    resolves = [];
  // 這裏收集 resolve 並將全部 chunkIds 標記爲已加載
  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]); //將 resolve push 到 resolves 數組中
    }
    installedChunks[chunkId] = 0; //標記爲已加載
  }
  // 遍歷各模塊組成的對象,將每一個模塊都加到 modules
  for (moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  // 執行保存的舊 push 方法,多是 array.push (即 push 到 window.webpackJsonp),也多是前一個並行執行了 runtime 的 bundle 的 webpackJsonpCallback,即遞歸執行 webpackJsonpCallback,如多入口同時 import 同一個 module 的狀況。
  if (parentJsonpFunction) parentJsonpFunction(data);

  //循環觸發 resolve 回調
  while (resolves.length) {
    resolves.shift()();
  }
}
複製代碼

webpackJsonpCallback 方法主要將異步的 chunk 裏的全部模塊都加到 modules 後,改變 installedChunks[chunkId] 的狀態爲 0(即已加載),而後執行以前建立的 promiseresolve()

執行 resolve 的回調 then 方法

回到模塊 a 根據 promise 的定義,執行 resolve 後,就會去執行對應的 then 方法:

//...
then(__webpack_require__.bind(null, './src/c.js'));
//...
複製代碼

即執行模塊 c

'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'sub', function() {
  return sub;
});
var Src_d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/d.js');

Promise.resolve()
  .then(__webpack_require__.bind(null, './src/b.js'))
  .then(function(m) {
    return m.add(200, 100);
  });
function sub(a, b) {
  return a - b + Object(Src_d__WEBPACK_IMPORTED_MODULE_0__['mul'])(100, 50);
}
console.log('c');
複製代碼

模塊 c 裏引入了模塊 d,這裏的模塊 d 與前文的模塊 d 雖然是同樣的,但因爲用的 loader 不同,因此會認爲是兩個不一樣的模塊,故會再次加載,互不影響。這裏模塊 d 不在累述。

而後執行:

Promise.resolve()
  .then(__webpack_require__.bind(null, './src/b.js'))
  .then(function(m) {
    return m.add(200, 100);
  });
複製代碼

Promise.resolve 方法容許調用時不帶參數,直接返回一個resolved 狀態的 Promise 對象。即執行 then 方法,即 __webpack_require__.bind(null, './src/b.js')。而後在 __webpack_require__ 方法裏判斷緩存有模塊 b,則直接返回模塊 b 對應的 exports。到此異步加載完成。

根據微任務隊列的前後順序,先執行模塊 a 的第二個 then 回調,而後執行模塊 c 的第二個 then 回調,都執行完成後,執行加載完成回調 onScriptComplete。到此代碼運行完成。

異步加載小結

再次梳理下異步加載的關鍵思路:

  1. 經過 __webpack_require__ 加載運行入口 module
  2. 模塊代碼裏遇到 import()即執行 __webpack_require__.e 加載異步 chunk
  3. __webpack_require__.e 使用模擬 jsonp 的方式及建立 script 標籤來加載異步 chunk,併爲每一個 chunk 建立一個 promise
  4. 等到異步 chunk 被加載後,會執行 window['webpackJsonp'].push,即 webpackJsonpCallback 方法
  5. webpackJsonpCallback 裏將異步 chunk 裏的 module 加入到 modules, 並觸發前面建立 promiseresolve 回調,而後執行其 then 方法即 __webpack_require__ 去加載新的 module

擴展 使用 splitChunks 切割後的文件解析

demo

// a.js
import { mul } from './d';
// b.js
import { mul } from './d';
// d.js爲普通同步文件
複製代碼
//webpack.config.js
{
  "entry": {
    "bundle1": "./src/e.js",
    "bundle2": "./src/f.js"
  },
  "plugins": [new HtmlWebpackPlugin()],
  //...
  "optimization": {
    "splitChunks": {
      "chunks": "all",
      "minSize": 0, //當模塊小於這個值時,就不拆
      "maxSize": 0, //當模塊大於這個值時,嘗試拆分
      "minChunks": 1, //重複一次就打包
      "name": true, //是否以cacheGroups中的filename做爲文件名
      "automaticNameDelimiter": "~", //打包的chunk名字鏈接符
      "cacheGroups": {
        "default": {
          "chunks": "all",
          "minChunks": 2,
          "priority": -10
        }
      }
    }
  }
}
複製代碼

引入插件 HtmlWebpackPlugin 輔助分析。

打包後代碼見 github,如下只作關鍵點記錄:

  • html 會引入每一個入口 bundle 生成的 js 和公共部分的 js
<script type="text/javascript" src="default~bundle1~bundle2.183bf5f4.js"></script><script type="text/javascript" src="bundle1.10ad.js"></script><script type="text/javascript" src="bundle2.c333.js"></script></body>
複製代碼
  • jsonp 初始化階段,執行:
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
複製代碼

和在 webpackJsonpCallback 方法裏執行:

if (parentJsonpFunction) parentJsonpFunction(data);
複製代碼

能夠保證不管頁面先加載入口文件仍是非入口文件,均可以將依賴 module 同步到各自的 chunk 裏。

  • 兩個入口文件 bundle1.xxxx.js bundle2.xxxx.jsruntime 代碼裏會多出一個新的變量 deferredModules
var deferredModules = [];
//...
deferredModules.push(['./src/e.js', 'default~bundle1~bundle2']);
return checkDeferredModules();
複製代碼

該變量爲一個數組,第一個變量是須要加載的 module,後面的變量就是要加載本 module 所需的其餘依賴 module。而後在 runtime 的末尾執行:return checkDeferredModules();

  • checkDeferredModules
function checkDeferredModules() {
  var result;
  for (var i = 0; i < deferredModules.length; i++) {
    var deferredModule = deferredModules[i];
    var fulfilled = true;
    for (var j = 1; j < deferredModule.length; j++) {
      var depId = deferredModule[j];
      if (installedChunks[depId] !== 0) fulfilled = false; //判斷依賴模塊有沒有加載過
    if (fulfilled) {
      deferredModules.splice(i--, 1);
      result = __webpack_require__((__webpack_require__.s = deferredModule[0])); //若是全部依賴模塊都加載了(即modules裏有依賴模塊),則就能夠讀取目標的module了
    }
  }

  return result;
}
複製代碼

該方法主要檢查依賴的 module 是否加載過,若都加載了則加載目標 module

  • webpackJsonpCallback 格外代碼
//...
deferredModules.push.apply(deferredModules, executeModules || []);
return checkDeferredModules();
複製代碼

該方法增長了這兩句代碼,用於在調用 webpackJsonpCallback 時(即 window["webpackJsonp"].pushwebpackJsonpCallback(jsonpArray[i])),有其餘依賴的時候能夠再去調用 checkDeferredModules 進行依賴檢查。

splitChunks 切割後加載小結

再次梳理下 splitChunks 切割後的關鍵思路:

  1. 根據 script 標籤前後順序,html 先加載公共依賴 default~bundle1~bundle2.xx.js,即在 window["webpackJsonp"]push 了該 module
  2. html 加載 bundle1.js,在 jsonp 初始化裏調用 webpackJsonpCallback(jsonpArray[i]) 將公共依賴模塊加到 modules 裏並改變其狀態爲已加載後,調用 checkDeferredModules(),但 deferredModules 爲空,因此沒有任何操做。
  3. 而後回到 runtime 裏繼續執行,將當前 module 和依賴 module pushdeferredModules 裏,再次調用 checkDeferredModules,此時判斷各依賴模塊狀態均爲已加載後,加載當期 module
  4. html 加載 bundle2 文件,此後邏輯跟 bundle1 一致。
相關文章
相關標籤/搜索