详解 webpackBootstrap
// run deferred modules from other chunks checkDeferredModules(); ```
这一段首先检查了 window 下有无之前定义的 jsonpArray
,若无则定义为一个空集合。同时将 jsonpArray
的 push
方法作为 oldJsonpFunction
同时指定其上下文 this::window["webpackJsonp"]
为自己。接着使用闭包的 webpackJsonpCallback
覆盖其 push
方法(此时同时覆盖了 window["webpackJsonp"]
的 push
方法,但 oldJsonpFunction
指向原生的 push
方法)。
接下来 jsonpArray = jsonpArray.slice();
这一行比较重要!调用了原生的 slice
方法,使得 jsonpArray
变成一个普通的 Array。此时 jsonpArray.push === Array.prototype.push
而之前暴露在 window 下的 window["webpackJsonp"] === webpackJsonpCallback
。
接着往后,依次对 jsonpArray
内的对象调用 webpackJsonpCallback
,然后用闭包的 parentJsonpFunction
指向之前的 oldJsonpFunction
,最后执行 checkDeferredModules
。
在 runtime
自执行时,我们讲到了入参 modules
=== []
,所以初始化时 webpackJsonpCallback
实际不会被执行,因此我们先看 checkDeferredModules
。
闭包函数 checkDeferredModules
js
function checkDeferredModules() {
var result;
for(var i = 0; i < deferredModules.length; i++) {
var deferredModule = deferredModules[i];
var fulfilled = true;
for(var j = 1; j < deferredModule.length; j++) {
var depId = deferredModule[j];
if(installedChunks[depId] !== 0) fulfilled = false;
}
if(fulfilled) {
deferredModules.splice(i--, 1);
result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
}
}
return result;
}
checkDeferredModules
并没有入参,因此它能取到的值都是当前 runtime
内闭包的变量。它做的事情也很简单,即对闭包集合 deferredModules
内的所有 modules 进行遍历,如果其满足 fulfilled
条件(即任意 module 依赖的所有 chunks 都已经在 installedChunks
内,注意检查是从 j = 1
开始的),则将其从 deferredModules
删除,使用 __webpack_require__
调用该 module 内的 j = 0
对应的模块(即入口模块)。
闭包函数 webpackJsonpCallback
```js function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2];
// add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = []; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) { resolves.shift()(); }
// add entry modules from loaded chunk to deferred list deferredModules.push.apply(deferredModules, executeModules || []);
// run deferred modules when all chunks ready return checkDeferredModules(); } ```
回头再看 webpackJsonpCallback
,它的入参是一个有 3 个对象的集合。第一个是当前 chunk 的 IDs (通常为单个),类型为 Array<string>
;第二个是当前 chunk 包含的模块,类型为 {[string]: function}
;第三个则是该 chunk 的执行依赖 chunks,类型为 Array<Array<string>>
。前两个参数无需多说,第三个参数是 webpack 4 新引入的,其作用就是交给前面提到的 checkDeferredModules
去检查 module 是否具备执行条件 fulfilled
。
Tips: 这里的第一和第三入参之所以是
Array
实际上对应的是多入口的情况chunkIds: Array<string>(n)
和executeModules: Array<Array<string>>(n)
,原则上讲两个 n 应该是相等的。还有一点就是executeModules
的准确类型应该是[[moduleId, ...chunkIds]]
,moduleId
是该模块的入坑,对应上方j = 0
的情况,而chunkIds
才是该模块能够执行的条件。
webpackJsonpCallback
的内部,首先遍历当前 chunk 的 chunkIds
,将其在 installedChunks
内的状态标识为 0;如果标识之前其已经在 installedChunks
内,则将其放入 resolves
集合内。接下来遍历该 chunk 所包含的所有模块,将其放在 modules
内对应的位置上。注意,这里的 modules
是 runtime
自执行函数的入参,即那个空集合。接着用入参,调用当前 runtime
的 parentJsonpFunction
方法;然后遍历 resolves
集合让其自执行(这两部分主要是有多个 runtime
时才会有影响,主要还是为了让之前的模块有机会拿到依赖的 chunks 从而触发执行,后面再详细分析)。最后,当前 chunk 安装完毕,将当前 chunk 依赖的 chunks 放入 deferredModules
,检查是否具备执行条件 checkDeferredModules()
。
webpackJsonpCallback
的运行过程其实就是将当前 chunk 的模块添加到 modules
内,并没有执行。执行前 modules
类似 []
,执行后变为 [ƒ, ƒ, ƒ, ƒ, ƒ]
,而其中的每一个 ƒ
正是所谓的模块。
闭包函数 __webpack_require__
webpack 的核心 magic 应该就是 __webpack_require__
方法。正是通过封装的 __webpack_require__
实现了前端的模块化。
```js // The require function function webpack_require(moduleId) {
// Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} };
// Execute the module function modules[moduleId].call(module.exports, module, module.exports, webpack_require);
// Flag the module as loaded module.l = true;
// Return the exports of the module return module.exports; } ```
主方法很简单,无非做了下面几件事情:
- 检查
require
的moduleId
是否在installedModules
内部,若在,则返回其exports
属性。 - 若不在,则声明一个新的
module
对象,并赋值在installedModules
内部。 - 从(由
webpackJsonpCallback
安装的)modules
内部找到对应模块(ƒ
),将当前module
和__webpack_require__
方法传入,调用执行。 - 返回执行完(即已经写入)的
exports
属性。
Tips: 这里有个小坑,注意第 2 步的时候,此时该
moduleId
已经在installedModules
内部了,而module.exports
还是个空对象{}
。当多个构建共享runtime
而又相互依赖时,有可能出现:A 依赖 B,而 B 通过 C 暴露出去(类似DllReferencePlugin
的情形),这个时候 A 去__webpack_require__
B,实际是找的 C,而 C 去__webpack_require__
B 时发现 B 已经在installedModules
内了,所以输出了空对象{}
。
__webpack_require__
的一些扩展
最后便是 __webpack_require__
的一些扩展,主要包括两类:
暴露对象
__webpack_require__.m
: 暴露modules
__webpack_require__.c
: 暴露installedModules
__webpack_require__.p
: 暴露publicPath
工具类
__webpack_require__.o = function(object, property)
__webpack_require__.d = function(exports, name, getter)
__webpack_require__.n = function(module)
__webpack_require__.r = function(exports)
__webpack_require__.t = function(value, mode)
let's debugger
webpackBootstrap
的代码涵盖的情景非常多,上面读完可能比较晕菜。下面结合实际例子,看一下 runtime
的调用栈。
demo101
demo101 的源码已经在前面 准备环境 做了介绍。模块 foo.js
导入模块 bar.js
输出的值(42)。使用 npm run wb101
构建完成后,启动 HTTP 服务(个人常用 http-server),e.g. http-server dist -p 3000
。
runtime.js
```diff /**/ (function(modules) { // webpackBootstrap
- debugger; /*/ // install a JSONP callback for chunk loading /*/ function webpackJsonpCallback(data) { ```
访问 http://localhost:3000/,
runtime
的执行非常简单,定义了前面讨论的闭包的方法和变量。一直按 F10 到 140 行,我们可以看到所有变量完成了初始化,以及其初始值:deferredModules
:[]
installedChunks
:{runtime: 0}
installedModules
:{}
modules
:[]
接着 142 - 147 行定义了
window["webpackJsonp"] = []
并覆盖其push
方法为webpackJsonpCallback
,jsonpArray
通过赋值调用slice
方法,使得webpackJsonpCallback
仅指向window["webpackJsonp"].push
,至此:window["webpackJsonp"]
:[push: ƒ]
(push === webpackJsonpCallback
)jsonpArray
:[]
oldJsonpFunction
:-> Array.prototype.push
(此处this
指向window["webpackJsonp"]
)parentJsonpFunction
:=== oldJsonpFunction
最后 151 行执行
checkDeferredModules()
,由于此时deferredModules = []
,所以直接返回undefined
。到此
runtime.js
执行结束,准备进入demo101.js
demo101.js
接着按 F10 进入
demo101.js
(删掉多余注释)。```js (window["webpackJsonp"] = window["webpackJsonp"] || []).push([["demo101"],{
"./demo101/bar.js":
(function(module, webpackexports, webpackrequire) {
"use strict"; webpackrequire.r(webpackexports); console.log("i'm in bar~");
webpack_exports["default"] = (42);
}),
"./demo101/foo.js":
(function(module, webpackexports, webpackrequire) {
"use strict"; webpackrequire.r(webpackexports); var barWEBPACKIMPORTEDMODULE0 = webpack_require(/*! ./bar */ "./demo101/bar.js");
console.log("i'm in foo~"); console.log(
got ${_bar__WEBPACK_IMPORTED_MODULE_0__["default"]} fron bar
);})
},[["./demo101/foo.js","runtime"]]]); ```
window["webpackJsonp"].psuh
指向前面的webpackJsonpCallback
,所以这里就相当于直接调用webpackJsonpCallback
方法,且入参为:["demo101"]
{./demo101/bar.js: ƒ, ./demo101/foo.js: ƒ}
[["./demo101/foo.js","runtime"]]
按 F11 进入方法,在完成了入参的解析和一些变量的声明后
webpackJsonpCallback
开始工作。在 23 行完成了demo101
chunk 的 install 和./demo101/bar.js
+./demo101/foo.js
模块的注入。此时:installedChunks
:{runtime: 0}
->{runtime: 0, demo101: 0}
modules
:[]
->[./demo101/bar.js: ƒ, ./demo101/foo.js: ƒ]
接着在 24 行调用
parentJsonpFunction
方法,将入参放入window["webpackJsonp"]
内;在 31 行将第三入参push
至deferredModules
内。此时:window["webpackJsonp"]
:[push: ƒ]
->[Array(3), push: ƒ]
deferredModules
:[]
->[["./demo101/foo.js","runtime"]]
接着进入
checkDeferredModules
方法:由于
deferredModules[0] = ["./demo101/foo.js","runtime"]
满足fulfilled
条件(注意检查时,默认从 1 开始,0 为该 chunk 的入口文件,在满足fulfilled
条件后作为入口执行)。将其从deferredModules
内删除,然后从./demo101/foo.js
开始调用__webpack_require__
。此时:deferredModules
:[["./demo101/foo.js","runtime"]]
->[]
从此开始进入
__webpack_require__
方法,执行模块内部方法。模块
./demo101/foo.js
不在installedModules
内,因此声明了一个新的module
并放入installedModules
,接着从modules
内找到./demo101/foo.js
模块(ƒ
),将module
,module.exports
,__webpack_require__
作为入参传入,调用模块方法。此时:installedModules
:{}
->{./demo101/foo.js: {…}}
到这里正式开始执行业务代码,因为离开了
runtime
的闭包区域,在业务代码里看不到之前的Closure
变量,只能访问到传入的module
和__webpack_require__
。在第 20 行的调用
__webpack_require__.r
方法,为module.exports
写入了一些 meta 信息;在第 21 行,遇到了源代码内部的import
,所以这里使用__webpack_require__
查找模块./demo101/bar.js
。__webpack_require__
遵循相同的机制,先在installedModules
内查找,结果没找到./demo101/bar.js
于是新声明了一个module
,接着传入__webpack_require__
执行进入./demo101/bar.js
模块内部。在第 9 行输出
console.log
,在第 11 行为module.exports
赋值 42,此时:installedModules
:{./demo101/foo.js: {…}, ./demo101/bar.js: {…}}
installedModules["./demo101/bar.js"].exports
:Module {default: 42, __esModule: true, Symbol(Symbol.toStringTag): "Module"}
到此
./demo101/bar.js
内部代码执行完毕,调用栈开始弹出:
| Call Stack | Continue |
| :-------------------------------------: | :----------------------------------------------------------------: |
| ./demo101/bar.js (demo101.js:11) | - |
| __webpackrequire__ (runtime.js:80) | 标记 module
+ 返回 ./demo101/bar.js
模块输出(42) |
| ./demo101/foo.js (demo101.js:21) | 拿到 ./demo101/bar.js
模块输出后执行完剩余代码 |
| __webpackrequire__ (runtime.js:80) | 标记 module
+ 返回 ./demo101/foo.js
模块输出 |
| checkDeferredModules (runtime.js:47) | 跳出循环,返回 deferredModule[0]
即 ./demo101/foo.js
模块输出 |
| webpackJsonpCallback (runtime.js:34) | 返回 checkDeferredModules()
输出,即 ./demo101/foo.js
模块输出 |
| (anonymous) (demo101.js:1) | END |
what's next
这里只是通读了 webpackBootstrap
的代码,let's debugger 部分也只列举了最简单的情况。还有多 chunk、启用 DllPlugin
、启用 library
、代码拆分、动态引入等情况 runtime
的执行分析可对比参照。
loading...
还没有人评论...