webpack code split实现原理

动态导入(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

原文地址:https://www.cnblogs.com/joe-yang/p/15518055.html