Webpack 打包 commonjs 和 esmodule 动态引入模块的产物对比

Webpack 打包 commonjs 和 esmodule 模块的产物对比 我们来继续分析。这篇文章主要来看一下动态引入,允许我们引入的模块名包含变量。

⚠️超长代码预警,需要几个小时的时间去啃,但读懂以后应该会很开心。

commonjs

新建一个 json 文件夹,包含几个 json 文件,和一个 add 方法。

其中 add.js 就是一个简单的加法模块。

1
2
3
4
5
// src/commonjs/json/add.js
console.log("add开始引入");
module.exports.add = (a, b) => {
return a + b;
};

test1.jsontest2.json 都是一个 json 对象。

1
2
3
4
5
6
7
8
9
// src/commonjs/json/test1.json
{
"data": "test1"
}

// src/commonjs/json/test2.json
{
"data": "test2"
}

然后我们提供一个 hello 模块,可以根据用户传入的参数,来引入不同的 json 文件返回给用户。

1
2
3
4
module.exports = function (filename) {
const data = require("./json/" + filename + ".json");
return data;
};

需要注意的上边 require 传入的模块名一定不能是一个纯变量,比如 require(filename) ,不然 webpack 就不知道该打包哪些文件了。

上边我们限定了目录位置 ./json 和文件名后缀 .json 。这样 Webpack 就会把 json 文件夹下所有的 .json 文件进行打包。

主函数 index.js 来调用 hello 方法。

1
2
3
console.log("commonjs开始执行");
const hello = require("./hello");
console.log(hello("test1"));

可以看一下控制台是正常输出:

image-20220503173736921

看一下打包产物:

主要看一下保存所有模块的 __webpack_modules__ 变量,其它的可以看一下上篇 Webpack 打包 commonjs 和 esmodule 模块的产物对比

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
var __webpack_modules__ = {
"./src/commonjs/hello.js": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
module.exports = function (filename) {
const data = __webpack_require__(
"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$"
)("./" + filename + ".json");
return data;
};
},

"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
...
},

"./src/commonjs/json/test1.json": (module) => {
"use strict";
module.exports = { data: "test1" };
},

"./src/commonjs/json/test2.json": (module) => {
"use strict";
module.exports = { data: "test2" };
},
};

主要是四个模块 ./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
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
"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
// 映射 key
var map = {
"./test1.json": "./src/commonjs/json/test1.json",
"./test2.json": "./src/commonjs/json/test2.json",
};

function webpackContext(req) {
var id = webpackContextResolve(req); // 得到映射后的 key
return __webpack_require__(id); // 通过 __webpack_require__ 导入文件
}
// 返回映射后的 key
function webpackContextResolve(req) {
if (!__webpack_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = "MODULE_NOT_FOUND";
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id =
"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$";
},

commonjs 模块整体上就是把匹配 "./json/" + filename + ".json" 这个格式的文件 test1.jsontest2.json 都进行了打包,并且略过了 add.js 文件。

可以再看下整体的产物:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
(() => {
var __webpack_modules__ = {
"./src/commonjs/hello.js": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
module.exports = function (filename) {
const data = __webpack_require__(
"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$"
)("./" + filename + ".json");
return data;
};
},

"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
var map = {
"./test1.json": "./src/commonjs/json/test1.json",
"./test2.json": "./src/commonjs/json/test2.json",
};

function webpackContext(req) {
var id = webpackContextResolve(req);
return __webpack_require__(id);
}
function webpackContextResolve(req) {
if (!__webpack_require__.o(map, req)) {
var e = new Error("Cannot find module '" + req + "'");
e.code = "MODULE_NOT_FOUND";
throw e;
}
return map[req];
}
webpackContext.keys = function webpackContextKeys() {
return Object.keys(map);
};
webpackContext.resolve = webpackContextResolve;
module.exports = webpackContext;
webpackContext.id =
"./src/commonjs/json sync recursive ^\\.\\/.*\\.json$";
},

"./src/commonjs/json/test1.json": (module) => {
"use strict";
module.exports = { data: "test1" };
},

"./src/commonjs/json/test2.json": (module) => {
"use strict";
module.exports = { data: "test2" };
},
};

var __webpack_module_cache__ = {};

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

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

var __webpack_exports__ = {};

(() => {
console.log("commonjs开始执行");
const hello = __webpack_require__("./src/commonjs/hello.js");
console.log(hello("test1"));
})();
})();

esmodule

esmodule 提供了 import() 方法进行动态引入,会返回一个 Promise 对象。

The ES2015 Loader spec defines import() as method to load ES2015 modules dynamically on runtime.

我们来用 esmodule 的形式改写下上边 commonjs 的代码。

首先是 hello.js

1
2
3
4
5
// src/esmodule/hello.js
const hello = (filename) => {
return import("./json/" + filename + ".json");
};
export default hello;

然后是 index.js

1
2
3
4
5
6
// src/esmodule/index.js
console.log("esmodule开始执行");
import hello from "./hello";
hello("test1").then((data) => {
console.log(data);
});

不同于 commonjs ,除了输出 test1.json 原本的数据,还多了一个 default 属性。

image-20220503191214724

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

image-20220503214502914

打包产物中除了 Webpack 打包 commonjs 和 esmodule 模块的产物对比 介绍的 d、o、r 方法,又多了很多奇奇怪怪的方法。

m 属性指向 __webpack_modules__,保存了导出的所有模块。

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
var __webpack_modules__ = {
"./src/esmodule/hello.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
__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$": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
...
},
};
__webpack_require__.m = __webpack_modules__;

g 属性指向全局对象,浏览器中的话就会返回 window

1
2
3
4
5
6
7
8
__webpack_require__.g = (function () {
if (typeof globalThis === "object") return globalThis; // 这句就直接返回
try {
return this || new Function("return this")();
} catch (e) {
if (typeof window === "object") return window;
}
})();

u 方法是将 chunkId 末尾加上 .main.js ,主要是为了和打包出来的文件名吻合。

1
2
3
__webpack_require__.u = (chunkId) => {
return "" + chunkId + ".main.js";
};

p 属性主要是为了拿到域名,开始执行的时候浏览器会加载我们的 main.js

image-20220503214502914

当前请求的地址是 http://127.0.0.1:5501/dist/main.js ,通过这个地址,我们要拿到 http://127.0.0.1:5501/dist/ ,详见下边的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var scriptUrl;
if (__webpack_require__.g.importScripts) // 这里不执行
scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document; // 这里拿到 window.document
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src; // 这里得到 http://127.0.0.1:5501/dist/main.js
if (!scriptUrl) { // 这里不执行
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}

if (!scriptUrl)
throw new Error(
"Automatic publicPath is not supported in this browser"
);
scriptUrl = scriptUrl
.replace(/#.*$/, "")
.replace(/\?.*$/, "")
.replace(/\/[^\/]+$/, "/"); // 这里得到 http://127.0.0.1:5501/dist/
__webpack_require__.p = scriptUrl;

接下来会比较复杂,会分成 8 个步骤来看一下 esmodule 异步加载的主流程。整体思路是通过 JSONP 的形式发送请求加载我们的 JSON 文件,同时把整个的加载过程会包装为一个 Promise ,加载完成将内容保存到 __webpack_modules__ 中。

  1. 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__
    ) => {
    "use strict";
    __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;
    },
  2. ./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" 对应打包后的文件名。

    image-20220503214502914

    看一下 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);
    });
    }
  3. 详细看一下 __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 对象的属性函数的定义。

  4. 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_jsonkey ,值是一个数组,数组的 0promiseresolve1promisereject2 是当前 promise ,如下图所示。

    image-20220504181410631

    最后执行 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
    }
    }
    };
  5. 看一下 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 标签中。

    image-20220504183052534

  6. 接着浏览器就会发送请求加载我们之前打包后的 js 文件。

    image-20220504183143512

    看一下文件内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    "use strict";
    (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 后主要做了三件事情:

    1. ./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__ ,保存着所有模块的键值对。

    2. 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
      }
      }
    3. installedChunks 相应的对象置为 0 ,代表加载完成了,前边讲的 loadingEnded 会判断这里是不是 0

      1
      installedChunks[chunkIds[i]] = 0;
  7. 上边一大堆完成了 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

  8. 上边讲了 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
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
(() => {
var __webpack_modules__ = {
"./src/esmodule/hello.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
__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$": (
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;
},
};

var __webpack_module_cache__ = {};

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

__webpack_require__.m = __webpack_modules__;

(() => {
var getProto = Object.getPrototypeOf
? (obj) => Object.getPrototypeOf(obj)
: (obj) => obj.__proto__;
var leafPrototypes;

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

(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();

(() => {
__webpack_require__.f = {};

__webpack_require__.e = (chunkId) => {
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
);
};
})();

(() => {
__webpack_require__.u = (chunkId) => {
return "" + chunkId + ".main.js";
};
})();

(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === "object") return globalThis;
try {
return this || new Function("return this")();
} catch (e) {
if (typeof window === "object") return window;
}
})();
})();

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack-demo:";

__webpack_require__.l = (url, done, key, chunkId) => {
if (inProgress[url]) {
inProgress[url].push(done);
return;
}
var script, needAttach;
if (key !== undefined) {
var scripts = document.getElementsByTagName("script");
for (var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if (
s.getAttribute("src") == url ||
s.getAttribute("data-webpack") ==
dataWebpackPrefix + key
) {
script = s;
break;
}
}
}
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);
};
})();

(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts)
scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}

if (!scriptUrl)
throw new Error(
"Automatic publicPath is not supported in this browser"
);
scriptUrl = scriptUrl
.replace(/#.*$/, "")
.replace(/\?.*$/, "")
.replace(/\/[^\/]+$/, "/");
__webpack_require__.p = scriptUrl;
})();

(() => {
var installedChunks = {
main: 0,
};

__webpack_require__.f.j = (chunkId, promises) => {
var installedChunkData = __webpack_require__.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])
);
promises.push((installedChunkData[2] = promise));

var url =
__webpack_require__.p +
__webpack_require__.u(chunkId);

var error = new Error();
var loadingEnded = (event) => {
if (
__webpack_require__.o(installedChunks, chunkId)
) {
installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0)
installedChunks[chunkId] = undefined;
if (installedChunkData) {
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);
}
}
};
__webpack_require__.l(
url,
loadingEnded,
"chunk-" + chunkId,
chunkId
);
} else installedChunks[chunkId] = 0;
}
}
};

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

var chunkLoadingGlobal = (self["webpackChunkwebpack_demo"] =
self["webpackChunkwebpack_demo"] || []);
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(
null,
chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
);
})();

var __webpack_exports__ = {};

(() => {
"use strict";

__webpack_require__.r(__webpack_exports__);
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);
}
);
})();
})();

require 引入模块是同步的,因此打包的时候就将数据保存起来了,打包产物也比较简单。

import() 是异步的,需要异步加载的文件提前单独生成文件,然后通过 JSONP 的形式进行加载,加载完毕后通过回调将数据添加到 __webpack_modules__ 对象中,方便后续使用。

windliang wechat