详解 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.jsdemo101.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 行完成了demo101chunk 的 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...
还没有人评论...