动态导入(dynamic import)
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。
- 第一种,也是推荐选择的方式是,使用符合
[ECMAScript 提案](https://github.com/tc39/proposal-dynamic-import)
的 import() 语法 来实现动态导入。 - 第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。
通过webpack打包后(mode=development也一样,只不过文件内容在内存中),会将
import() 语法
和webpack 特定的require.ensure
方法转换为__webpack_require__.e
函数
当执行代码时,源代码中的import()或require.ensure方法调用已变成__webpack_require__.e函数调用。通过这个__webpack_require__.e => webpack_require.f.j => webpack_require.l调用链,最终使用script标签加载相应的js文件。
加载流程
1. 加载html页面,页面内容包含
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack Code Split</title>
<meta name="viewport" content="width=device-width, initial-scale=1"><script defer src="runtime.bundle.js"></script><script defer src="index.bundle.js"></script></head>
<body>
</body>
</html>
2. 加载runtime.bundle.js并执行代码
// 文件底部代码
// - chunkLoadingGlobal为常规数组
var chunkLoadingGlobal = self["webpackChunkgetting_started_using_a_configuration"] = self["webpackChunkgetting_started_using_a_configuration"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 1. 为chunkLoadingGlobal数组的push方法绑定执行上下文为chunkLoadingGlobal
// 2. 为webpackJsonpCallback函数绑定第一个参数为上一步的push方法
// 3. 将上一步的webpackJsonpCallback函数赋值给chunkLoadingGlobal.push
// 这样,webpackJsonpCallback的第一个参数已经绑定为常规数组的push方法,而chunkLoadingGlobal.push函数就变成了webpackJsonpCallback方法
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
3. 加载index.bundle.js
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
// 这里实际上是webpackJsonpCallback的函数调用,即push === webpackJsonpCallback
// 在webpackJsonpCallback函数内部,参数为该push的3个参数。
// runtime为第三个参数,在函数的底部调用这个这个函数,参数为moduleId="./src/index.js"即第二个参数的键,
// 然后通过__webpack_require__的内部代码__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__)去调用
// 第二个参数的moduleId所对应值的函数。
// 遇到__webpack_require__.e这个函数,它会去动态加载通过import导入的文件。即所谓的code split。
(self["webpackChunkgetting_started_using_a_configuration"] = self["webpackChunkgetting_started_using_a_configuration"] || []).push([["index"],{
/***/ "./src/index.js":
/*!**********************!*
!*** ./src/index.js ***!
**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("async function getComponent() {
const element = document.createElement('div');
const { default: _ } = await __webpack_require__.e(/*! import() */ "vendors-node_modules_lodash_lodash_js").then(__webpack_require__.t.bind(__webpack_require__, /*! lodash */ "./node_modules/lodash/lodash.js", 23));
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
getComponent().then((component) => {
document.body.appendChild(component);
});
//# sourceURL=webpack://getting-started-using-a-configuration/./src/index.js?");
/***/ })
},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ var __webpack_exports__ = (__webpack_exec__("./src/index.js"));
/******/ }
]);
4. 加载js文件
webpack_require.e => webpack_require.f.j => webpack_require.l通过这个调用链,然后script标签去加载而外的js文件。
通过结合Promise实现异步回调。
/* webpack/runtime/load script */
/******/ (() => {
/******/ var inProgress = {};
/******/ var dataWebpackPrefix = "getting-started-using-a-configuration:";
/******/ // loadScript function to load a script via script tag
/******/ __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) => {
/******/ // avoid mem leaks in IE.
/******/ 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);
/******/ };
/******/ })();
参考
[-] https://webpack.docschina.org/guides/code-splitting/
[-] https://github.com/YYvanYang/webpack-code-split
[-] https://github.com/webpack/webpack/blob/d28592e9daf1f483c621708451534fc1ec7240c6/examples/code-splitting/README.md