接 Webpack 打包 commonjs 和 esmodule 模块的产物对比 我们来继续分析。这篇文章主要来看一下动态引入,允许我们引入的模块名包含变量。
⚠️超长代码预警,需要几个小时的时间去啃,但读懂以后应该会很开心。
commonjs
新建一个 json 文件夹,包含几个 json 文件,和一个 add 方法。

其中 add.js 就是一个简单的加法模块。
| 1 | // src/commonjs/json/add.js | 
test1.json 和 test2.json 都是一个 json 对象。
| 1 | // src/commonjs/json/test1.json | 
然后我们提供一个 hello 模块,可以根据用户传入的参数,来引入不同的 json 文件返回给用户。
| 1 | module.exports = function (filename) { | 
需要注意的上边 require 传入的模块名一定不能是一个纯变量,比如 require(filename) ,不然 webpack 就不知道该打包哪些文件了。
上边我们限定了目录位置 ./json 和文件名后缀 .json 。这样 Webpack 就会把 json 文件夹下所有的 .json 文件进行打包。
主函数 index.js 来调用 hello 方法。
| 1 | console.log("commonjs开始执行"); | 
可以看一下控制台是正常输出:

看一下打包产物:
主要看一下保存所有模块的 __webpack_modules__ 变量,其它的可以看一下上篇 Webpack 打包 commonjs 和 esmodule 模块的产物对比 。
| 1 | var __webpack_modules__ = { | 
主要是四个模块 ./src/commonjs/hello.js 、./src/commonjs/json sync recursive ^\\.\\/.*\\.json$、./src/commonjs/json/test1.json 和 ./src/commonjs/json/test2.json 。
./src/commonjs/json/test1.json 和 ./src/commonjs/json/test2.json 这两个模块就是把我们的 json 文件用 module.exports 来导出。
./src/commonjs/hello.js 模块中先调用 ./src/commonjs/json sync recursive ^\\.\\/.*\\.json$ 模块的方法,再进行传参。
此外将我们原本的 "./json/" + filename + ".json" 参数转为了 "./" + filename + ".json" 。
重点来看下 ./src/commonjs/json sync recursive ^\\.\\/.*\\.json$ ,详见下边的注释
| 1 | "./src/commonjs/json sync recursive ^\\.\\/.*\\.json$": ( | 
commonjs 模块整体上就是把匹配 "./json/" + filename + ".json" 这个格式的文件 test1.json 和 test2.json 都进行了打包,并且略过了 add.js 文件。

可以再看下整体的产物:
| 1 | (() => { | 
esmodule
esmodule 提供了 import() 方法进行动态引入,会返回一个 Promise 对象。
The ES2015 Loader spec defines
import()as method to load ES2015 modules dynamically on runtime.
我们来用 esmodule 的形式改写下上边 commonjs 的代码。
首先是 hello.js 。
| 1 | // src/esmodule/hello.js | 
然后是 index.js 
| 1 | // src/esmodule/index.js | 
不同于 commonjs ,除了输出 test1.json 原本的数据,还多了一个 default 属性。

打包文件中除了 main.js ,把两个 json 文件也单拎了出来,如下图:

打包产物中除了  Webpack 打包 commonjs 和 esmodule 模块的产物对比  介绍的 d、o、r 方法,又多了很多奇奇怪怪的方法。
m 属性指向 __webpack_modules__,保存了导出的所有模块。
| 1 | var __webpack_modules__ = { | 
g 属性指向全局对象,浏览器中的话就会返回 window 。
| 1 | __webpack_require__.g = (function () { | 
u 方法是将 chunkId 末尾加上 .main.js ,主要是为了和打包出来的文件名吻合。
| 1 | __webpack_require__.u = (chunkId) => { | 
p 属性主要是为了拿到域名,开始执行的时候浏览器会加载我们的 main.js 。

当前请求的地址是 http://127.0.0.1:5501/dist/main.js ,通过这个地址,我们要拿到 http://127.0.0.1:5501/dist/ ,详见下边的代码:
| 1 | var scriptUrl; | 
接下来会比较复杂,会分成 8 个步骤来看一下 esmodule 异步加载的主流程。整体思路是通过 JSONP 的形式发送请求加载我们的 JSON 文件,同时把整个的加载过程会包装为一个 Promise ,加载完成将内容保存到 __webpack_modules__ 中。
- hello方法通过- __webpack_require__调用- "./src/esmodule/json lazy recursive ^\\.\\/.*\\.json$"方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17- "./src/esmodule/hello.js": ( 
 __unused_webpack_module,
 __webpack_exports__,
 __webpack_require__
 ) => {
 ;
 __webpack_require__.r(__webpack_exports__);
 __webpack_require__.d(__webpack_exports__, {
 default: () => __WEBPACK_DEFAULT_EXPORT__,
 });
 const hello = (filename) => {
 return __webpack_require__(
 "./src/esmodule/json lazy recursive ^\\.\\/.*\\.json$"
 )("./" + filename + ".json");
 };
 const __WEBPACK_DEFAULT_EXPORT__ = hello;
 },
- ./src/esmodule/json lazy recursive ^\\.\\/.*\\.json$方法导出的是- webpackAsyncContext方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34- "./src/esmodule/json lazy recursive ^\\.\\/.*\\.json$": ( 
 module,
 __unused_webpack_exports,
 __webpack_require__
 ) => {
 var map = {
 "./test1.json": [
 "./src/esmodule/json/test1.json",
 "src_esmodule_json_test1_json",
 ],
 "./test2.json": [
 "./src/esmodule/json/test2.json",
 "src_esmodule_json_test2_json",
 ],
 };
 function webpackAsyncContext(req) {
 if (!__webpack_require__.o(map, req)) {
 return Promise.resolve().then(() => {
 var e = new Error("Cannot find module '" + req + "'");
 e.code = "MODULE_NOT_FOUND";
 throw e;
 });
 }
 debugger;
 var ids = map[req],
 id = ids[0];
 return __webpack_require__.e(ids[1]).then(() => {
 return __webpack_require__.t(id, 3 | 16);
 });
 }
 webpackAsyncContext.keys = () => Object.keys(map);
 webpackAsyncContext.id =
 "./src/esmodule/json lazy recursive ^\\.\\/.*\\.json$";
 module.exports = webpackAsyncContext;- map中定义了- json文件的映射,- "./src/esmodule/json/test1.json"是原本的文件位置,会作为模块的- key,- "src_esmodule_json_test1_json"对应打包后的文件名。 - 看一下 - webpackAsyncContext方法,先调用- __webpack_require__.e方法来发送请求加载文件并且返回一个- Promise。- __webpack_require__.t方法会将返回的数据加一个- default属性,也就是开头说的一个不同之处。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14- function webpackAsyncContext(req) { 
 if (!__webpack_require__.o(map, req)) {
 return Promise.resolve().then(() => {
 var e = new Error("Cannot find module '" + req + "'");
 e.code = "MODULE_NOT_FOUND";
 throw e;
 });
 }
 var ids = map[req], // ids[0] 是原本路径, id[1] 是打包后的文件名字
 id = ids[0];
 return __webpack_require__.e(ids[1]).then(() => {
 return __webpack_require__.t(id, 3 | 16);
 });
 }
- 详细看一下 - __webpack_require__.e方法,传入了一个参数- chunkId,这里就是- src_esmodule_json_test1_json。- 1 
 2
 3
 4
 5
 6
 7
 8- __webpack_require__.e = (chunkId) => { 
 return Promise.all(
 Object.keys(__webpack_require__.f).reduce((promises, key) => {
 __webpack_require__.f[key](chunkId, promises);
 return promises;
 }, [])
 );
 };- 主要就是执行 - f对象的所有属性函数,- f的属性函数会在传入的- promises中添加当前的- Promise。- 看一下 - f对象的属性函数的定义。
- f对象当前场景下只有一个- j属性函数,所以在上边的- e方法中会执行下边的- j函数。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48- var installedChunks = { // 记录加载的文件 
 main: 0,
 };
 __webpack_require__.f.j = (chunkId, promises) => {
 var installedChunkData = __webpack_require__.o( // o 方法是判断当前对象是否有该属性
 installedChunks,
 chunkId
 )
 ? installedChunks[chunkId]
 : undefined;
 if (installedChunkData !== 0) {
 if (installedChunkData) {
 promises.push(installedChunkData[2]);
 } else {
 if (true) {
 // 第一次加载文件会走到这里
 var promise = new Promise(
 (resolve, reject) =>
 (installedChunkData = installedChunks[chunkId] =
 [resolve, reject]) // 将 resolve 和 reject 保存
 );
 promises.push((installedChunkData[2] = promise)); // 把当前 promise 塞入到传入的 promises 数组
 var url =
 __webpack_require__.p +
 __webpack_require__.u(chunkId); // url 拼成了 http://127.0.0.1:5501/dist/src_esmodule_json_test1_json.main.js
 var error = new Error();
 var loadingEnded = (event) => {
 if (
 __webpack_require__.o(installedChunks, chunkId)
 ) {
 ...
 }
 }
 };
 
 __webpack_require__.l(
 url,
 loadingEnded,
 "chunk-" + chunkId,
 chunkId
 );
 } else installedChunks[chunkId] = 0;
 }
 }
 };- 上边的 - j函数执行完后,会在- installedChunks对象中增加一个- src_esmodule_json_test1_json的- key,值是一个数组,数组的- 0是- promise的- resolve,- 1是- promise的- reject,- 2是当前- promise,如下图所示。 - 最后执行 - l方法,就是我们的主角,通过- JSONP的形式,塞一个- script去加载- http://127.0.0.1:5501/dist/src_esmodule_json_test1_json.main.js文件。- 加载完成或者加载错误会执行上边的 - loadingEnded方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33- var error = new Error(); 
 var loadingEnded = (event) => {
 if (
 __webpack_require__.o(installedChunks, chunkId)
 ) {
 installedChunkData = installedChunks[chunkId];
 if (installedChunkData !== 0)
 installedChunks[chunkId] = undefined;
 if (installedChunkData) { // 走到这里 installedChunkData 应该已经是 0 了(后边会讲到哪里置的 0),不然的话就抛出错误
 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;
 installedChunkData[1](error); // installedChunkData[1] 是之前保存的 reject
 }
 }
 };
- 看一下 - l方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44- var inProgress = {}; 
 var dataWebpackPrefix = "webpack-demo:";
 __webpack_require__.l = (url, done, key, chunkId) => {
 if (inProgress[url]) {
 inProgress[url].push(done);
 return;
 }
 var script, needAttach;
 ...
 // 设置 script
 if (!script) {
 needAttach = true;
 script = document.createElement("script");
 script.charset = "utf-8";
 script.timeout = 120;
 if (__webpack_require__.nc) {
 script.setAttribute("nonce", __webpack_require__.nc);
 }
 script.setAttribute("data-webpack", dataWebpackPrefix + key);
 script.src = url;
 }
 inProgress[url] = [done];
 var onScriptComplete = (prev, event) => {
 script.onerror = script.onload = null;
 clearTimeout(timeout);
 var doneFns = inProgress[url];
 delete inProgress[url];
 script.parentNode && script.parentNode.removeChild(script);
 doneFns && doneFns.forEach((fn) => fn(event));
 if (prev) return prev(event);
 };
 var timeout = setTimeout(
 onScriptComplete.bind(null, undefined, {
 type: "timeout",
 target: script,
 }),
 120000
 );
 script.onerror = onScriptComplete.bind(null, script.onerror);
 script.onload = onScriptComplete.bind(null, script.onload);
 needAttach && document.head.appendChild(script); // 插入当前 script
 };- 主要就是 - scrpit加载完毕后的回调,然后将当前- script插入到- head标签中。 
- 接着浏览器就会发送请求加载我们之前打包后的 - js文件。 - 看一下文件内容: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- ; 
 (self["webpackChunkwebpack_demo"] =
 self["webpackChunkwebpack_demo"] || []).push([
 ["src_esmodule_json_test1_json"],
 {
 "./src/esmodule/json/test1.json": (module) => {
 module.exports = { data: "test1" };
 },
 },
 ]);- 加载完毕后会执行上边的代码, - self["webpackChunkwebpack_demo"]的- push方法之前已经重定义好了,也就是下边的代码。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { 
 ...
 };
 var chunkLoadingGlobal = (self["webpackChunkwebpack_demo"] =
 self["webpackChunkwebpack_demo"] || []);
 chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
 chunkLoadingGlobal.push = webpackJsonpCallback.bind( // 定义 push 方法
 null,
 chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
 );- 执行 - self["webpackChunkwebpack_demo"] || []).push相当于执行- webpackJsonpCallback方法。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26- var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { 
 var [chunkIds, moreModules, runtime] = data;
 var moduleId,
 chunkId,
 i = 0;
 if (chunkIds.some((id) => installedChunks[id] !== 0)) {
 for (moduleId in moreModules) {
 if (__webpack_require__.o(moreModules, moduleId)) {
 __webpack_require__.m[moduleId] = moreModules[moduleId];
 }
 }
 if (runtime) var result = runtime(__webpack_require__);
 }
 if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
 for (; i < chunkIds.length; i++) {
 chunkId = chunkIds[i];
 if (
 __webpack_require__.o(installedChunks, chunkId) &&
 installedChunks[chunkId]
 ) {
 installedChunks[chunkId][0]();
 }
 installedChunks[chunkIds[i]] = 0;
 }
 };- 传入的 - data参数就是加载的文件内容时候传入的,也就是下边的样子。- 1 
 2
 3
 4
 5
 6
 7
 8- [ 
 ["src_esmodule_json_test1_json"],
 {
 "./src/esmodule/json/test1.json": (module) => {
 module.exports = { data: "test1" };
 },
 },
 ]- webpackJsonpCallback拿到上边的- data后主要做了三件事情:- 将 - ./src/esmodule/json/test1.json模块保存到- __webpack_modules__- 1 
 2
 3
 4
 5
 6
 7
 8- if (chunkIds.some((id) => installedChunks[id] !== 0)) { 
 for (moduleId in moreModules) {
 if (__webpack_require__.o(moreModules, moduleId)) {
 __webpack_require__.m[moduleId] = moreModules[moduleId];
 }
 }
 if (runtime) var result = runtime(__webpack_require__);
 }- __webpack_require__.m就是- __webpack_modules__,保存着所有模块的键值对。
- 将 - installedChunks之前保存的- promise执行- resolve。- 1 
 2
 3
 4
 5
 6
 7
 8
 9- for (; i < chunkIds.length; i++) { 
 chunkId = chunkIds[i];
 if (
 __webpack_require__.o(installedChunks, chunkId) &&
 installedChunks[chunkId]
 ) {
 installedChunks[chunkId][0](); // 数组 0 保存的就是 resolve
 }
 }
- 将 - installedChunks相应的对象置为- 0,代表加载完成了,前边讲的- loadingEnded会判断这里是不是- 0。- 1 - installedChunks[chunkIds[i]] = 0; 
 
- 上边一大堆完成了 - JSONP,并且成功将动态加载的模块放到了- __webpack_modules__中,然后我们看一下执行到哪里了:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14- function webpackAsyncContext(req) { 
 if (!__webpack_require__.o(map, req)) {
 return Promise.resolve().then(() => {
 var e = new Error("Cannot find module '" + req + "'");
 e.code = "MODULE_NOT_FOUND";
 throw e;
 });
 }
 var ids = map[req], // ids[0] 是原本路径, id[1] 是打包后的文件名字
 id = ids[0];
 return __webpack_require__.e(ids[1]).then(() => {
 return __webpack_require__.t(id, 3 | 16);
 });
 }- 执行完 - e方法,接下执行- t方法,会有很多不同的- mode进入不同的分支,这里就不细究了,只需要知道最终结果是把数据加了- default属性然后返回。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29- __webpack_require__.t = function (value, mode) { 
 if (mode & 1) value = this(value);
 if (mode & 8) return value;
 if (typeof value === "object" && value) {
 if (mode & 4 && value.__esModule) return value;
 if (mode & 16 && typeof value.then === "function") return value;
 }
 var ns = Object.create(null);
 __webpack_require__.r(ns);
 var def = {};
 leafPrototypes = leafPrototypes || [
 null,
 getProto({}),
 getProto([]),
 getProto(getProto),
 ];
 for (
 var current = mode & 2 && value;
 typeof current == "object" && !~leafPrototypes.indexOf(current);
 current = getProto(current)
 ) {
 Object.getOwnPropertyNames(current).forEach(
 (key) => (def[key] = () => value[key])
 );
 }
 def["default"] = () => value;
 __webpack_require__.d(ns, def);
 return ns;
 };- 拿数据的话就是第一行代码, - if (mode & 1) value = this(value);,这里的- this就是- webpack_require函数,相当于执行- __webpack_require__('./src/esmodule/json/test1.json')。关于- this指向可以参考 JavaScript中this指向详细分析(译)。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18- function __webpack_require__(moduleId) { 
 var cachedModule = __webpack_module_cache__[moduleId];
 if (cachedModule !== undefined) {
 return cachedModule.exports;
 }
 var module = (__webpack_module_cache__[moduleId] = {
 exports: {},
 });
 __webpack_modules__[moduleId](
 module,
 module.exports,
 __webpack_require__
 );
 return module.exports;
 }- './src/esmodule/json/test1.json'之前已经保存到了- __webpack_modules__中,所以就把之前加载的内容返回给了- value。
- 上边讲了 - hello方法的执行,最后返回了一个包含数据的- promise,最终回到了我们的- index函数中。- 1 
 2
 3
 4
 5
 6
 7
 8
 9- var _hello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( 
 "./src/esmodule/hello.js"
 );
 console.log("esmodule开始执行");
 (0, _hello__WEBPACK_IMPORTED_MODULE_0__["default"])("test1").then(
 (data) => {
 console.log(data);
 }
 );
以上就是 esmodule 异步加载模块的全过程了,稍微有些复杂,整体流程如下:
定义 JSOP 的回调函数((self["webpackChunkwebpack_demo"].push) -> 
进入 index 函数 -> 进入 hello 函数 -> 进入 webpackAsyncContext 函数 ->
进入 __webpack_require__.e 函数 -> 
执行 __webpack_require__.f.j 函数,保存 promise ,生成要下载的文件 url  -> 
进入 __webpack_require__.l 函数,运用 JSONP,动态插入 script  ->
加载 script 文件,执行回调函数 (self["webpackChunkwebpack_demo"].push ,将数据保存到 __webpack_modules__  ->
执行 __webpack_require__.t 方法,将数据加上 default 返回 ->
hello 函数执行完毕 ->
回到 index 函数继续执行,输出导入的数据。
可以再看下完整代码:
| 1 | (() => { | 
总
require 引入模块是同步的,因此打包的时候就将数据保存起来了,打包产物也比较简单。
import() 是异步的,需要异步加载的文件提前单独生成文件,然后通过 JSONP 的形式进行加载,加载完毕后通过回调将数据添加到 __webpack_modules__ 对象中,方便后续使用。
