主要是想搞清楚以下两个问题
- 1,import module 最后是怎么运行的
- 2,动态加载是怎么实现的
- 3,webpack 配置的 output.jsonpFunction 有啥作用
准备以下三个 js
├── index.js
├── local.js
└── dynamic.js
内容如下
index.js
import local from './local';
local();
import( /* webpackChunkName: "dynamic" */ './dynamic').then(dynamic => {
dynamic()
});
local.js
export default function local () {
alert('local')
}
dynamic.js
export default function dynamic () {
alert('dynamic')
}
打包配置
const path = require('path')
module.exports = {
entry: {
app: './src/index.js'
},
mode: 'development',
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
jsonpFunction: 'cd-spin'
}
}
随后我们运行下 webpack ,看看 build 后得到的文件
"rm -rf ./dist && webpack --mode development",
接下来我们一起来搞定前面提到的三个问题
1,import module 最后是怎么运行的
最后打包后,生成的 main.js 主体结果如下
// 整体是一个闭包,参数 modules 是一个结构为[module path]: Function 的对象
(function (modules) {
/** 省略代码 **/
return __webpack_require__("./src/index.js");
})({
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
/** 省略代码 **/
}),
"./src/local.js": (function (module, __webpack_exports__, __webpack_require__) {
/** 省略代码 **/
})
})
从上面的闭包中,我们看到在闭包函数的最后,调用了 __webpack_require__("./src/index.js");
, webpack_require 看名字就知道,就是加载一个 module。整体执行过程如下:
1,会有一个名为 installedModules 的对象,以 module Id 作为 key 来缓存 module
2, module 中 export 的数据,会挂载在 installedModules.exports 中
3,如果 module id 在 installedModules 存在,说明已经加载过了,直接返回对应的 exports,也就是说 import 一次之后,后续都是直接拿到上一次的执行结果
4, 执行前面 modules 挂载进行的 moudle 方法,传入三个参数:module, module.exports, __webpack_require__
5,返回 module.exports;
// The module cache
var installedModules = {};
// 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;
}
加载 local.js 的时候,也就是直接调用 webpack_require
"./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// local.js 直接使用 __webpack_require__ 去加载
var _local__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/local.js");
(_local__WEBPACK_IMPORTED_MODULE_0__["default"])();
__webpack_require__.e("dynamic").then(
__webpack_require__.bind(null, "./src/dynamic.js")).then(dynamic => {
dynamic()
});
})
2,动态加载是怎么实现的
在前面的我们知道了,正常所有模块都是定义在 整个闭包的 modules 参数中,webpack_require 就是根据 module name 去这里读取对应的模块而已
但是对于动态加载的 js ,一开始并不存在于这个 modules 中,但是毫无疑问,为了保证能正常被读取到,动态加载的模块也是要注册到这个 modules 中, 那么它是怎么将其注册进去的呢?
// 调用了 __webpack_require__.e
__webpack_require__.e("dynamic").then(
__webpack_require__.bind(null, "./src/dynamic.js")).then(dynamic => {
dynamic()
});
为了方便理解记忆,我搞了个伪代码,个人感觉这个实现是十分巧妙的
// 劫持 window jsonp 数组的 push 方法
var jsonpArray = (window["jsonp"] = window["jsonp"] || []);
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
// 为什么要挟持,因为 dynamic.js 打包后如下(会调用 window.jsonp.push)
(window["cd-spin"] = window["cd-spin"] || []).push([
["dynamic"],
{
"./src/dynamic.js": function () {},
},
]);
__webpack_require__.e = function () {
// 伪代码
// 保存了一个 Promise 到 installedChunks 中,然后 Promise.all,这样这里的 Promise 状态就是
// pengding, 支持动态加载的 js 加载完毕后来改成这个 Promise 状态
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise));
return Promise.all();
};
/**参数 data 格式如下
* data: [
* ["dynamic"],
* {
* [moduleName]: {
* }
* }
* ]
*
*/
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// 就是 前面 __webpack_require__.e 保存进来的 resolve
// 这里调用一下,表示已经完成了加载,将前面 __webpack_require__.e 的 Promise 状态修改为 resolve
installedChunks(chunkIds)[0];
// 然后注册到全局的 modules 中
modules[moduleId] = moreModules[moduleId];
}
下面再拆开分析下实现
我们来看看 webpack_require.e 的实现
// 主要作用:
// 创建一个 script 标签去动态加载个 chunk js
// 返回一个 Promise, 标示这个动态 chunk js 的加载状态
// 设置 installedChunks = [resolve, reject, promise]
// 这里 Promise 会在 动态 js 加载执行完毕后,才会变成 resolve 状态
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// installedChunks 保存了所有 chunk 的加载状态
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
// 如果在 installedChunks 已存在,但是不为 0 ,表示正在加载中
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 定义一个 Promise 标示加载状态
// installedChunks = [resolve, reject, promise]
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 下面就是创建一个 script 去加载,省略...
// start chunk loading
}
}
return Promise.all(promises);
};
那么,回到我们上面的问题,动态的 chunk 是怎么注册到 modules 中的呢? 我们看看 dynamic.js 中的代码
// 调用了一个全局的 jsonp 数组的 push
(window["jsonp"] = window["jsonp"] || []).push([["dynamic"], {
"./src/dynamic.js": (function (module, __webpack_exports__, __webpack_require__) {
// 省略
})
}])
这个 jsonp 数组,在前面我们提到的整个必包函数中有定义 代码如下,它干了这么一件事情:
1,挟持 jsonp 数组的 push 方法 2,动态记载的 chunk js 调用 push 时,将之前调用 webpack_require.e 生成的 promise 设置为 resolve 状态,然后将 chunk 注入到 modules 中
var jsonpArray = window["jsonp"] = window["jsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 挟持了 push 方法,动态加载的 js,调用的是下面的 webpackJsonpCallback 方法
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {1`
// Promise 的 resolve
resolves.push(installedChunks[chunkId][0]);
}
// 标示该 chunk 已经加载过了
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
// 将 chunk 注册到 modules 中
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
// 执行 将 __webpack_require__.e 的 Promise 置为 resolve
resolves.shift()();
}
};
整个加载过程
3,webpack output.jsonpFunction 的作用
其实就是之前动态加载时,jsonp 这个数组变量的名字
即可这个 ”jsonp". (window["jsonp"] = window["jsonp"] || []).push([["dynamic"], {
当你的应用存在多个 webpack 打包的 js 文件在运行时(例如微前端场景),如果这个数组变量名称一致,那么在动态加载 chunk 的时候,就会出现错乱,例如将自己的 chunk 加载到别的服务上
我们应该保证每个微服务都有独立的 jsonp