Webpack 模块加载原理
webpack 是一个模块打包器,它通过编译打包,将 ESM 和 CJS 语法转换为 webpack 加载模块的规范,这种方式在浏览器上是可以直接使用的,通俗理解就是打包成了通用的普通对象
CommonJS 规范
打包前代码
scss
// index.js
const test = require('./test')
function demo() {}
demo()
test()
// test.js
function test() {}
module.exports = test
打包后代码,较为简单,解释在源码。
javascript
(() => {
// 打包后的原始模块放在这里,用于第一次读取
var __webpack_modules__ = {
'./src/test.js': (module) => {
function test() {}
module.exports = test;
},
};
// 缓存模块,用于第二次读取
// 可能显得多余,但对于大项目来说可以做到利用空间换时间的
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
const test = __webpack_require__('./src/test.js');
function demo() {}
demo();
test();
})();
})();
ESM 规范
打包前代码
javascript
// index.js
import test,{a} from "./test"
function demo() {
console.log('demo',a);
// import("./dynamicImport.js")
}
demo()
test()
// test.js
function test() {
console.log("test");
}
export const a ="hello"
export default test
打包后代码,较为简单,解释在源码。
ESM 其实和 CJS 差不多,但是加载模块的时候,多了一个表明这是一个ESM模块的标志
- 能够与其他模块加载器,或者工具交互,站在库开发者的角度,我觉得有点用的,起码得有。
- 模块识别,一些插件,工具,能够读取到这些配置就能判断是 esm 模块还是 cjs 模块
javascript
(() => {
'use strict';
var __webpack_modules__ = {
'./src/test.js': ( __unused_webpack_module,__webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ a: () => /* binding */ a,
/* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
/* harmony export */
});
function test() {
console.log('test');
}
const a = 'hello';
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = test;
},
};
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module,module.exports,__webpack_require__);
// Return the exports of the module
return module.exports;
}
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__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/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
// 猜测两种好处,能够与其他模块加载器,或者工具交互,站在库开发者的角度,我觉得有点用的,起码得有。
// 模块识别,一些插件,工具,能够读取到这些配置就能判断是 esm 模块还是 cjs 模块
__webpack_require__.r = (exports) => {
// console.log( Object.prototype.toString.call(exports)) ==> "[object Module]"
// Object.getOwnPropertyDescriptors(exports)
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module',
});
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {a:1};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('./src/test.js');
function demo() {
console.log('demo', _test__WEBPACK_IMPORTED_MODULE_0__.a);
}
demo();
(0, _test__WEBPACK_IMPORTED_MODULE_0__['default'])();
})();
})();
按需加载
按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。
在 webpack 中可以使用 import
和 require.ensure
来引入需要动态导入的代码,例如下面这个示例:
打包前代码
scss
function demo() {
console.log('demo');
}
demo()
// webpack 会将 dynamicImport 打包到独立的 chunk 当加载到这个模块的时候,就会自动加载
// 如果这个模块还在其他模块会如用,webpack 会引用缓存
import("./dynamicImport.js")
打包后代码
javascript
(() => {
// 1
var __webpack_modules__ = ({});
// The module cache
// 2
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
debugger
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
// 3
__webpack_require__.m = __webpack_modules__;
/* webpack/runtime/define property getters */
// 4
(() => {
// define getter functions for harmony exports
__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/runtime/ensure chunk */
// 5
(() => {
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
})();
/* webpack/runtime/get javascript chunk filename */
// 6
(() => {
// This function allow to reference async chunks
__webpack_require__.u = (chunkId) => {
// return url for filenames based on template
return "static/js/" + chunkId + "." + "52b72416" + ".chunk.js";
};
})();
/* webpack/runtime/global */
// 7
(() => {
__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/runtime/hasOwnProperty shorthand */
// 8
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* webpack/runtime/load script */
// 9
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack-demo:";
// 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);
debugger
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
})();
/* webpack/runtime/make namespace object */
// 10
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
/* webpack/runtime/publicPath */
// 11
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
// 当前的入口文件的dom <script src="./dist/static/js/main.js"></script>
if (document.currentScript)
scriptUrl = document.currentScript.src;
console.log(document.currentScript.src,"document.currentScript.src");
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if(scripts.length) {
var i = scripts.length - 1;
while (i > -1 && !scriptUrl) scriptUrl = scripts[i--].src;
}
}
}
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/?.*$/, "").replace(//[^/]+$/, "/");
__webpack_require__.p = scriptUrl + "../../";
})();
/* webpack/runtime/jsonp chunk loading */
// 12
(() => {
// no baseURI
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
__webpack_require__.f.j = (chunkId, promises) => {
// JSONP chunk loading for javascript
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
if(true) { // all chunks have JS
// setup Promise in chunk cache
var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
promises.push(installedChunkData[2] = promise);
// start chunk loading
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
// create error before stack unwound to get useful stacktrace later
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);
}
}
}
};
// no prefetching
// no preloaded
// no HMR
// no HMR manifest
// no on chunks loaded
// install a JSONP callback for chunk loading
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
debugger
console.log(parentChunkLoadingFunction,"parentChunkLoadingFunction");
console.log(data,"data");
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
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]) {
console.log(installedChunks[chunkId],chunkId,"installedChunks[chunkId][0]");
debugger
installedChunks[chunkId][0]();
}
installedChunks[chunkId] = 0;
}
}
debugger
// 这里是 jsonp 注意点,当初始化逻辑执行后,会注册这里的回调方法,重写self["webpackChunkwebpack_demo"]的 push 方法
// self["webpackChunkwebpack_demo"] 创建一个[],重写里面的 push 方法,然后留给异步模块去执行这个 push 方法就可以把参数带回来
// push 方法重写为 webpackJsonpCallback,接收到动态模块代码
// script 标签请求 动态模块文件,回来后就会触发这里的方法,它会把 push 执行,并把动态模块代码返回。
// 触发 webpackJsonpCallback 方法,installedChunks[chunkId][0]() 把 resolve 返回,那么就会触发 then方法,接着就会触发 __webpack_require__.e 把模块加载,并缓存起来。
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
})();
// 13
var __webpack_exports__ = {};
function demo() {
console.log('demo');
}
// 14
demo()
// 15
__webpack_require__.e("src_dynamicImport_js").then(__webpack_require__.bind(__webpack_require__,"./src/dynamicImport.js"))
})()
/************************************************************************************/
// 以下为动态模块代码
'use strict';
debugger;
(self['webpackChunkwebpack_demo'] =
self['webpackChunkwebpack_demo'] || []).push([
['src_dynamicImport_js'],
{
'./src/dynamicImport.js': (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
/* harmony export */
});
function dynamicImport() {
console.log('dynamicImport');
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ =
dynamicImport;
},
},
]);
按需加载逻辑还是较为复杂,逻辑比较绕,以下是详细解析。
源码详细分析
-
首先初始化,大概逻辑和 cjs 、esm 大概相似,但是处理异步的逻辑。
-
自执行函数初始化逻辑,都是围绕 webpack_require 添加变量或函数
-
注意一下,push 方法重写
ini// 这里是 jsonp 注意点,当初始化逻辑执行后,会注册这里的回调方法,重写self["webpackChunkwebpack_demo"]的 push 方法 // self["webpackChunkwebpack_demo"] 创建一个[],重写里面的 push 方法,然后留给异步模块去执行这个 push 方法就可以把参数带回来 // push 方法重写为 webpackJsonpCallback,接收到动态模块代码 // script 标签请求 动态模块文件,回来后就会触发这里的方法,它会把 push 执行,并把动态模块代码返回。 // 触发 webpackJsonpCallback 方法,installedChunks[chunkId][0]() 把 resolve 返回,那么就会触发 then方法,接着就会触发 __webpack_require__.e 把模块加载,并缓存起来。 var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []; chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
arduino_webpack_require__.e("src_dynamicImport_js").then(__webpack_require__.bind(__webpack_require__, /*! ./dynamicImport.js */ "./src/dynamicImport.js")) // 同步代码逻辑,then 后面的异步代码后面补充。 _webpack_require__.e("src_dynamicImport_js") 会去到 __webpack_require__.f.j 注册 promise,使模块加载完毕后,异步调用。 接着调用 __webpack_require__.l,这里会去创建 script 标签加载模块,模块加载完 12s 后删除script 等待动态模块加载后,会自动执行,那么就会去到 webpackJsonpCallback 的逻辑,它会把 promise resolve。 回到 _webpack_require__.e("src_dynamicImport_js").then 的方法中,接着触发 __webpack_require__ 方法,就正常的加载模块,把模块放到缓存里面
-
总结
CJS 加载模块,从 __webpack_modules__
把模块代码读取 __webpack_module_cache__
中
ESM 加载模块,从 __webpack_modules__
把模块代码读取 __webpack_module_cache__
中,并对模块类型,原型对象标识上是一个 ESM 模块
按需加载,通过 JSONP
技术实现,本质是通过重写["webpackChunkwebpack_demo"].push
方法,当 script 方法 加载完毕就会执行 push 方法,接着把模块代码传递到 webpackJsonpCallback 中进行处理,把代码传递到 __webpack_require__
中,把模块代码读取 __webpack_module_cache__
中,并对模块类型,原型对象标识上是一个 ESM 模块