webpack
webpack 原理
1. webpack 模块加载原理
文件信息来源:
webpack 深入理解模块加载原理
webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。
1.1 CommonJS 规范
打包后的代码其实是一个立即执行函数,传入的参数是一个对象。这个对象以文件路径为 key,以文件内容为 value,它包含了所有打包后的模块。
// 简化后的代码
(function(modules){
/*
1. 定义了一个模块缓存对象 `installedModules` ,作用是缓存已经加载过的模块。
2. 定义了一个模块加载函数 `__webpack_require__()`。
3. ... 省略一些其他代码。
4. 使用 `__webpack_require__()` 加载入口模块。
*/
})({
path1: function1,
path2: function2
})
其中的核心就是
__webpack_require__()
函数,它接收的参数是
moduleId
,其实就是文件路径。
它的执行过程如下:
-
判断模块是否有缓存,如果有则返回缓存模块的
export
对象,即
module.exports
。 -
新建一个模块
module
,并放入缓存。 - 执行文件路径对应的模块函数。
- 将这个新建的模块标识为已加载。
-
执行完模块后,返回该模块的
exports
对象。
小结:
__webpack_require__()
加载模块后,会先执行模块对应的函数,然后返回该模块的
exports
对象。而 文件的导出对象
module.exports
就是一个函数。所以入口模块能通过
__webpack_require__()
引入这个函数并执行。
1.2 ES6 module
使用 ES6 module 规范打包后的代码和使用 CommonJS 规范打包后的代码绝大部分都是一样的。
一样的地方是 webpack 自定义模块规范的代码一样,唯一不同的是上面两个文件打包后的代码不同,它们的入参不一样。
// ES6 module
{
"./src/index.js":(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");
}),
}
// CommonJS
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
}),
}
源码里有几个重要的函数需要理解
-
webpack_require.d()
:给
__webpack_exports__
定义导出变量用的。它的作用相当于
__webpack_exports__["default"] = test2
。这个 “
default
” 是因为你使用
export default
来导出函数,如果这样导出函数:// 前 __webpack_require__.d(__webpack_exports__, "default", function() { return test2; }); // 后 __webpack_require__.d(__webpack_exports__, "test2", function() { return test2; });
-
webpack_require.r()
:作用是给
__webpack_exports__
添加一个
__esModule
为
true
的属性,表示这是一个
ES6 module
。 -
__webpack_require__.n()
: 分析该
export
对象是否是
ES6 module
,如果是则返回
module['default']
即
export default
对应的变量。如果不是
ES6 module
则直接返回
export
。
1.3 动态导入
按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。
在 webpack 中可以使用
import
来引入需要动态导入的代码
1.3.1 bundle.js
让我们来看看一个核心文件的执行顺序
bundle.js
-
定义了一个对象
installedChunks
,作用是缓存动态模块。 -
定义了一个辅助函数
jsonpScriptSrc()
,作用是根据模块 ID 生成 URL。 -
定义了两个新的核心函数
__webpack_require__.e()
和
webpackJsonpCallback()
。 -
定义了一个全局变量
window["webpackJsonp"] = []
,它的作用是存储需要动态导入的模块。 -
重写
window["webpackJsonp"]
数组的
push()
方法为
webpackJsonpCallback()
。也就是说
window["webpackJsonp"].push()
其实执行的是
webpackJsonpCallback()
1.3.2 0.bundle.js
0.bundle.js
文件可以发现,它正是使用
window["webpackJsonp"].push()
来放入动态模块的。动态模块数据项有两个值,第一个是它是模块的 ID ;第二个值是模块的路径名和模块内容。
1.3.3 webpack_require.e()
原来模块代码中的
import('./test2')
被翻译成了如下代码
function test() {}
test()
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
//# sourceURL=webpack:///./src/index.js?
他的处理逻辑是
-
先查看该模块 ID 对应缓存的值是否为 0,0 代表已经加载成功了,第一次取值为
undefined
。 -
如果不为 0 并且不是
undefined
代表已经是加载中的状态。然后将这个加载中的
Promise
推入
promises
数组。 -
如果不为 0 并且是
undefined
就新建一个
Promise
,用于加载需要动态导入的模块。 -
生成一个
script
标签,URL 使用
jsonpScriptSrc
(chunkId)` 生成,即需要动态导入模块的 URL。 -
为这个
script
标签设置一个 2 分钟的超时时间,并设置一个
onScriptComplete()
函数,用于处理超时错误。 -
然后添加到页面中
document.head.appendChild(7. script)
,开始加载模块。 - 返回 promises 数组。
1.3.4 小结
总的来说,动态导入的逻辑如下:
-
重写
window["webpackJsonp"].push()
方法。 -
入口模块使用
__webpack_require__.e()
下载动态资源。 -
资源下载完成后执行
window["webpackJsonp"].push()
,即
webpackJsonpCallback()
。 -
将资源标识为 0,代表已经加载完成。由于加载模块使用的是 Promise,所以要执行
resolve()
。 -
再看一下入口模块的加载代码
__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
,下载完成后执行
then()
方法,调用
__webpack_require__()
真正开始加载代码。
2022-08-03 xieliuning