1. 程式人生 > 實用技巧 >深入瞭解 webpack 模組載入原理

深入瞭解 webpack 模組載入原理

webpack 是一個模組打包器,在它看來,每一個檔案都是一個模組。

無論你開發使用的是 CommonJS 規範還是 ES6 模組規範,打包後的檔案都統一使用 webpack 自定義的模組規範來管理、載入模組。本文將從一個簡單的示例開始,來講解 webpack 模組載入原理。

CommonJS 規範

假設現在有如下兩個檔案:

// index.js
const test2 = require('./test2')

function test() {}

test()
test2()
// test2.js
function test2() {}

module.exports = test2

以上兩個檔案使用 CommonJS 規範來匯入匯出檔案,打包後的程式碼如下(已經刪除了不必要的註釋):

(function(modules) { // webpackBootstrap
    // The module cache
    // 模組快取物件
	var installedModules = {};

    // The require function
    // webpack 實現的 require() 函式
	function __webpack_require__(moduleId) {
        // Check if module is in cache
        // 如果模組已經載入過,直接返回快取
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
        // Create a new module (and put it into the cache)
        // 建立一個新模組,並放入快取
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};

        // Execute the module function
        // 執行模組函式
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // Flag the module as loaded
        // 將模組標識為已載入
		module.l = true;

		// Return the exports of the module
		return module.exports;
	}


    // expose the modules object (__webpack_modules__)
    // 將所有的模組掛載到 require() 函式上
	__webpack_require__.m = modules;

    // expose the module cache
    // 將快取物件掛載到 require() 函式上
	__webpack_require__.c = installedModules;

	// define getter function for harmony exports
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};

	// define __esModule on exports
	__webpack_require__.r = function(exports) {
		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};

	// create a fake namespace object
	// mode & 1: value is a module id, require it
	// mode & 2: merge all properties of value into the ns
	// mode & 4: return value when already ns object
	// mode & 8|1: behave like require
	__webpack_require__.t = function(value, mode) {
		if(mode & 1) value = __webpack_require__(value);
		if(mode & 8) return value;
		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
		var ns = Object.create(null);
		__webpack_require__.r(ns);
		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
		return ns;
	};

	// getDefaultExport function for compatibility with non-harmony modules
	__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;
	};

	// Object.prototype.hasOwnProperty.call
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

	// __webpack_public_path__
	__webpack_require__.p = "";


    // Load entry module and return exports
    // 載入入口模組,並返回模組物件
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js": (function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  
  "./src/test2.js": (function(module, exports) {
    eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
  })
});

可以看到 webpack 實現的模組載入系統非常簡單,僅僅只有一百行程式碼。

打包後的程式碼其實是一個立即執行函式,傳入的引數是一個物件。這個物件以檔案路徑為 key,以檔案內容為 value,它包含了所有打包後的模組。

{
  "./src/index.js": (function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  
  "./src/test2.js": (function(module, exports) {
    eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
  })
}

將這個立即函式化簡一下,相當於:

(function(modules){
	// ...
})({
	path1: function1,
	path2: function2
})

再看一下這個立即函式做了什麼:

  1. 定義了一個模組快取物件 installedModules,作用是快取已經載入過的模組。
  2. 定義了一個模組載入函式 __webpack_require__()
  3. ... 省略一些其他程式碼。
  4. 使用 __webpack_require__() 載入入口模組。

其中的核心就是 __webpack_require__() 函式,它接收的引數是 moduleId,其實就是檔案路徑。

它的執行過程如下:

  1. 判斷模組是否有快取,如果有則返回快取模組的 export 物件,即 module.exports
  2. 新建一個模組 module,並放入快取。
  3. 執行檔案路徑對應的模組函式。
  4. 將這個新建的模組標識為已載入。
  5. 執行完模組後,返回該模組的 exports 物件。
   // The require function
   // webpack 實現的 require() 函式
function __webpack_require__(moduleId) {
       // Check if module is in cache
       // 如果模組已經載入過,直接返回快取
	if(installedModules[moduleId]) {
		return installedModules[moduleId].exports;
	}
       // Create a new module (and put it into the cache)
       // 建立一個新模組,並放入快取
	var module = installedModules[moduleId] = {
		i: moduleId,
		l: false,
		exports: {}
	};

       // Execute the module function
       // 執行模組函式
	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

       // Flag the module as loaded
       // 將模組標識為已載入
	module.l = true;

	// Return the exports of the module
	return module.exports;
}

從上述程式碼可以看到,在執行模組函式時傳入了三個引數,分別為 modulemodule.exports__webpack_require__

其中 modulemodule.exports 的作用和 CommonJS 中的 modulemodule.exports 的作用是一樣的,而 __webpack_require__ 相當於 CommonJS 中的 require

在立即函式的最後,使用了 __webpack_require__() 載入入口模組。並傳入了入口模組的路徑 ./src/index.js

__webpack_require__(__webpack_require__.s = "./src/index.js");

我們再來分析一下入口模組的內容。

(function(module, exports, __webpack_require__) {
    eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
  })

入口模組函式的引數正好是剛才所說的那三個引數,而 eval 函式的內容美化一下後和下面內容一樣:

const test2 = __webpack_require__("./src/test2.js")
function test() {}
test()
test2()
//# sourceURL=webpack:///./src/index.js?

將打包後的模組程式碼和原模組的程式碼進行對比,可以發現僅有一個地方發生了變化,那就是 require 變成了 __webpack_require__

再看一下 test2.js 的程式碼:

function test2() {}
module.exports = test2
//# sourceURL=webpack:///./src/test2.js?

從剛才的分析可知,__webpack_require__() 載入模組後,會先執行模組對應的函式,然後返回該模組的 exports 物件。而 test2.js 的匯出物件 module.exports 就是 test2() 函式。所以入口模組能通過 __webpack_require__() 引入 test2() 函式並執行。

到目前為止可以發現 webpack 自定義的模組規範完美適配 CommonJS 規範。

ES6 module

將剛才用 CommonJS 規範編寫的兩個檔案換成用 ES6 module 規範來寫,再執行打包。

// index.js
import test2 from './test2'

function test() {}

test()
test2()
// test2.js
export default function test2() {}

使用 ES6 module 規範打包後的程式碼和使用 CommonJS 規範打包後的程式碼絕大部分都是一樣的。

一樣的地方是指 webpack 自定義模組規範的程式碼一樣,唯一不同的是上面兩個檔案打包後的程式碼不同。

{
 	"./src/index.js":(function(module, __webpack_exports__, __webpack_require__) {
		"use strict";
		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");
	}),
	
	"./src/test2.js": (function(module, __webpack_exports__, __webpack_require__) {
		"use strict";
		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
	})
}

可以看到傳入的第二個引數是 __webpack_exports__,而 CommonJS 規範對應的第二個引數是 exports。將這兩個模組程式碼的內容美化一下:

// index.js
__webpack_require__.r(__webpack_exports__);
 var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
 function test() {}
 test()
 Object(_test2__WEBPACK_IMPORTED_MODULE_0__["default"])()
 //# sourceURL=webpack:///./src/index.js?
// test2.js
 __webpack_require__.r(__webpack_exports__);
 __webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
 function test2() {}
 //# sourceURL=webpack:///./src/test2.js?

可以發現,在每個模組的開頭都執行了一個 __webpack_require__.r(__webpack_exports__) 語句。並且 test2.js 還多了一個 __webpack_require__.d() 函式。

我們先來看看 __webpack_require__.r()__webpack_require__.d() 是什麼。

webpack_require.d()

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, { enumerable: true, get: getter });
	}
};

原來 __webpack_require__.d() 是給 __webpack_exports__ 定義匯出變數用的。例如下面這行程式碼:

__webpack_require__.d(__webpack_exports__, "default", function() { return test2; });

它的作用相當於 __webpack_exports__["default"] = test2。這個 "default" 是因為你使用 export default 來匯出函式,如果這樣匯出函式:

export function test2() {}

它就會變成 __webpack_require__.d(__webpack_exports__, "test2", function() { return test2; });

webpack_require.r()

// define __esModule on exports
__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__.r() 函式的作用是給 __webpack_exports__ 新增一個 __esModuletrue 的屬性,表示這是一個 ES6 module。

新增這個屬性有什麼用呢?

主要是為了處理混合使用 ES6 module 和 CommonJS 的情況。

例如匯出使用 CommonJS module.export = test2 匯出函式,匯入使用 ES6 module import test2 from './test2

打包後的程式碼如下:

// index.js
__webpack_require__.r(__webpack_exports__);
var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
var _test2__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_test2__WEBPACK_IMPORTED_MODULE_0__);
function test() {}
test()
_test2__WEBPACK_IMPORTED_MODULE_0___default()()
//# sourceURL=webpack:///./src/index.js?
// test2.js
 function test2() {}
 module.exports = test2
 //# sourceURL=webpack:///./src/test2.js?

從上述程式碼可以發現,又多了一個 __webpack_require__.n() 函式:

__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;
};

先來分析一下入口模組的處理邏輯:

  1. __webpack_exports__ 匯出物件標識為 ES6 module。
  2. 載入 test2.js 模組,並將該模組的匯出物件作為引數傳入 __webpack_require__.n() 函式。
  3. __webpack_require__.n 分析該 export 物件是否是 ES6 module,如果是則返回 module['default']export default 對應的變數。如果不是 ES6 module 則直接返回 export

按需載入

按需載入,也叫非同步載入、動態匯入,即只在有需要的時候才去下載相應的資原始檔。

在 webpack 中可以使用 importrequire.ensure 來引入需要動態匯入的程式碼,例如下面這個示例:

// index.js
function test() {}

test()
import('./test2')
// test2.js
export default function test2() {}

其中使用 import 匯入的 test2.js 檔案在打包時會被單獨打包成一個檔案,而不是和 index.js 一起打包到 bundle.js


這個 0.bundle.js 對應的程式碼就是動態匯入的 test2.js 的程式碼。

接下來看看這兩個打包檔案的內容:

// bundle.js
(function(modules) { // webpackBootstrap
	// install a JSONP callback for chunk loading
	function webpackJsonpCallback(data) {
		var chunkIds = data[0];
		var moreModules = data[1];

		// add "moreModules" to the modules object,
		// then flag all "chunkIds" as loaded and fire callback
		var moduleId, chunkId, i = 0, resolves = [];
		for(;i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && 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];
			}
		}
		if(parentJsonpFunction) parentJsonpFunction(data);

		while(resolves.length) {
			resolves.shift()();
		}

	};


	// The module cache
	var installedModules = {};

	// object to store loaded and loading chunks
	// undefined = chunk not loaded, null = chunk preloaded/prefetched
	// Promise = chunk loading, 0 = chunk loaded
	var installedChunks = {
		"main": 0
	};

	// script path function
	function jsonpScriptSrc(chunkId) {
		return __webpack_require__.p + "" + chunkId + ".bundle.js"
	}

	// The require function
	function __webpack_require__(moduleId) {

		// Check if module is in cache
		if(installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		// Create a new module (and put it into the cache)
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};

		// Execute the module function
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

		// Flag the module as loaded
		module.l = true;

		// Return the exports of the module
		return module.exports;
	}

	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = function requireEnsure(chunkId) {
		var promises = [];

		// JSONP chunk loading for javascript

		var installedChunkData = installedChunks[chunkId];
		if(installedChunkData !== 0) { // 0 means "already installed".

			// a Promise means "currently loading".
			if(installedChunkData) {
				promises.push(installedChunkData[2]);
			} else {
				// setup Promise in chunk cache
				var promise = new Promise(function(resolve, reject) {
					installedChunkData = installedChunks[chunkId] = [resolve, reject];
				});
				promises.push(installedChunkData[2] = promise);

				// start chunk loading
				var script = document.createElement('script');
				var onScriptComplete;

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

				// create error before stack unwound to get useful stacktrace later
				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.onerror = script.onload = onScriptComplete;
				document.head.appendChild(script);
			}
		}
		return Promise.all(promises);
	};

	// expose the modules object (__webpack_modules__)
	__webpack_require__.m = modules;

	// expose the module cache
	__webpack_require__.c = installedModules;

	// define getter function for harmony exports
	__webpack_require__.d = function(exports, name, getter) {
		if(!__webpack_require__.o(exports, name)) {
			Object.defineProperty(exports, name, { enumerable: true, get: getter });
		}
	};

	// define __esModule on exports
	__webpack_require__.r = function(exports) {
		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
		}
		Object.defineProperty(exports, '__esModule', { value: true });
	};

	// create a fake namespace object
	// mode & 1: value is a module id, require it
	// mode & 2: merge all properties of value into the ns
	// mode & 4: return value when already ns object
	// mode & 8|1: behave like require
	__webpack_require__.t = function(value, mode) {
		if(mode & 1) value = __webpack_require__(value);
		if(mode & 8) return value;
		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
		var ns = Object.create(null);
		__webpack_require__.r(ns);
		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
		return ns;
	};

	// getDefaultExport function for compatibility with non-harmony modules
	__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;
	};

	// Object.prototype.hasOwnProperty.call
	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

	// __webpack_public_path__
	__webpack_require__.p = "";

	// on error function for async loading
	__webpack_require__.oe = function(err) { console.error(err); throw err; };

	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
	jsonpArray.push = webpackJsonpCallback;
	jsonpArray = jsonpArray.slice();
	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
	var parentJsonpFunction = oldJsonpFunction;

	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js":(function(module, exports, __webpack_require__) {
    eval("function test() {}\r\n\r\ntest()\r\n__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./test2 */ \"./src/test2.js\"))\n\n//# sourceURL=webpack:///./src/index.js?");
  })
});
// 0.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push(
  [
    [0],
    {
      "./src/test2.js":(function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
      })
    }
  ]
);

這次打包的程式碼量有點膨脹,bundle.js 程式碼居然有 200 行。我們來看看相比於同步載入的 webpack 模組規範,它有哪些不同:

  1. 定義了一個物件 installedChunks,作用是快取動態模組。
  2. 定義了一個輔助函式 jsonpScriptSrc(),作用是根據模組 ID 生成 URL。
  3. 定義了兩個新的核心函式 __webpack_require__.e()webpackJsonpCallback()
  4. 定義了一個全域性變數 window["webpackJsonp"] = [],它的作用是儲存需要動態匯入的模組。
  5. 重寫 window["webpackJsonp"] 陣列的 push() 方法為 webpackJsonpCallback()。也就是說 window["webpackJsonp"].push() 其實執行的是 webpackJsonpCallback()

而從 0.bundle.js 檔案可以發現,它正是使用 window["webpackJsonp"].push() 來放入動態模組的。動態模組資料項有兩個值,第一個是 [0],它是模組的 ID;第二個值是模組的路徑名和模組內容。

然後我們再看一下打包後的入口模組的程式碼,經過美化後:

function test() {}
test()
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
//# sourceURL=webpack:///./src/index.js?

原來模組程式碼中的 import('./test2') 被翻譯成了 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))

__webpack_require__.e() 的作用是什麼呢?

webpack_require.e()

__webpack_require__.e = function requireEnsure(chunkId) {
	var promises = [];
	// JSONP chunk loading for javascript
	var installedChunkData = installedChunks[chunkId];
	if(installedChunkData !== 0) { // 0 means "already installed".
		// a Promise means "currently loading".
		if(installedChunkData) {
			promises.push(installedChunkData[2]);
		} else {
			// setup Promise in chunk cache
			var promise = new Promise(function(resolve, reject) {
				installedChunkData = installedChunks[chunkId] = [resolve, reject];
			});
			promises.push(installedChunkData[2] = promise);

			// start chunk loading
			var script = document.createElement('script');
			var onScriptComplete;

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

			// create error before stack unwound to get useful stacktrace later
			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.onerror = script.onload = onScriptComplete;
			document.head.appendChild(script);
		}
	}
	return Promise.all(promises);
};

它的處理邏輯如下:

  1. 先檢視該模組 ID 對應快取的值是否為 0,0 代表已經載入成功了,第一次取值為 undefined
  2. 如果不為 0 並且不是 undefined 代表已經是載入中的狀態。然後將這個載入中的 Promise 推入 promises 陣列。
  3. 如果不為 0 並且是 undefined 就新建一個 Promise,用於載入需要動態匯入的模組。
  4. 生成一個 script 標籤,URL 使用 jsonpScriptSrc(chunkId) 生成,即需要動態匯入模組的 URL。
  5. 為這個 script 標籤設定一個 2 分鐘的超時時間,並設定一個 onScriptComplete() 函式,用於處理超時錯誤。
  6. 然後新增到頁面中 document.head.appendChild(script),開始載入模組。
  7. 返回 promises 陣列。

當 JS 檔案下載完成後,會自動執行檔案內容。也就是說下載完 0.bundle.js 後,會執行 window["webpackJsonp"].push()

由於 window["webpackJsonp"].push() 已被重置為 webpackJsonpCallback() 函式。所以這一操作就是執行 webpackJsonpCallback() ,接下來我們看看 webpackJsonpCallback() 做了哪些事情。

webpackJsonpCallback()

對這個模組 ID 對應的 Promise 執行 resolve(),同時將快取物件中的值置為 0,表示已經載入完成了。相比於 __webpack_require__.e(),這個函式還是挺好理解的。

小結

總的來說,動態匯入的邏輯如下:

  1. 重寫 window["webpackJsonp"].push() 方法。
  2. 入口模組使用 __webpack_require__.e() 下載動態資源。
  3. 資源下載完成後執行 window["webpackJsonp"].push(),即 webpackJsonpCallback()
  4. 將資源標識為 0,代表已經載入完成。由於載入模組使用的是 Promise,所以要執行 resolve()
  5. 再看一下入口模組的載入程式碼 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js")),下載完成後執行 then() 方法,呼叫 __webpack_require__() 真正開始載入程式碼,__webpack_require__() 在上文已經講解過,如果不瞭解,建議再閱讀一遍。

更多文章,敬請關注