在上篇文章中,我们介绍了 webpack 同步加载模块的原理。这篇文章,我们来介绍一下 webpack 异步加载模块。
异步加载模块
还是先做一些准备工作。
首先定义一个依赖模块:math.js,math.js 采用 ES6 module
导出了两个函数 add 和 minus。
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
然后定义一个入口模块:index.js,index.js 通过 import 函数导入了 math.js,然后调用了里面的 add 和 minus 方法。
import("./math").then(math => {
console.log(math.add(2, 1));
console.log(math.minus(2, 1));
});
最后再定义一个配置文件:webpack.config.js,内容和 webpack 同步加载模块中一样。
const path = require("path");
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.join(__dirname, 'index.js'),
output: {
filename: "main.js",
path: path.join(__dirname, 'dist')
}
};
在根目录下执行webpack --config webpack.config.js
,就可以在 dist 目录下看到最终的生成产物了,可以看到 dist 目录下除了 main.js 外,还多了一个 0.main.js。我们通常把 main.js 称为同步 chunk
,0.main.js 称为异步 chunk
。
先来看一下 main.js 文件中的代码。
首先是 webpackBootstrap
的函数体,与同步加载模块相比,它新增了很多内容:
- webpackJsonpCallback 函数:异步加载 chunk 后的 jsonp 回调函数
- installedChunks 对象:缓存 chunk 加载状态,main 代表入口模块,0 表示模块已加载,因为入口模块是同步 chunk,所以默认就是已加载的
- jsonpScriptSrc 函数:拼接 chunk 请求地址
- __webpack__require__.e 函数:异步加载 chunk 函数
- jsonp 初始化代码:初始化 jsonp 相关配置
接下来是 webapackBootstrap
的参数部分,与同步加载模块不同的是,它移除了依赖模块 main.js(main.js 中的内容都移入到了异步 chunk 文件 0.main.js 中)。
最后是入口模块 index.js,它使用 __webpack_require__.e 来异步下载依赖模块 math.js,在 math.js 下载完毕后,再使用 __webpack_require__ 来同步加载 math.js。
看到这里,大家应该有个初步感觉了:webpack 异步加载模块,其实是通过 jsonp
的方式来实现的。
接下来看一下 __webpack_require__.e_ 的函数实现。
__webpack_require__.e 中的代码比较多,先看一头一尾:最前面定义了一个 promise 数组 promises
,最后面用 Promise.all 的方式返回了 promises
。因此 __webpack_require__.e 中间的代码,其实都是用来操作 promises
数组的。具体是如何操作的,我们一步步来看。
首先是缓存查找,在 installedChunks
对象中根据 chunkId 来判断对应的 chunk 是否已加载,chunk 主要有三种状态,分别对应不同的处理:
- 0:表示 chunk 已加载,这种情况下,不做处理,直接返回
- 数组:表示 chunk 正在加载,这种情况下,会将缓存的 promise 对象压入
promises
数组中,等待 chunk 加载完毕 - undefined:表示 chunk 第一次加载,这种情况下的操作包括:
- 创建一个新的 promise 对象
- 将新创建 promise 对象的 reject、resolve 方法以及 promise 对象本身都存放到
installedChunks
对象中 - 将 promise 对象压入
promises
数组中
接下来是模块加载,模块加载使用 jsonp
的方式,首先会动态创建一个 script 标签,src 指向异步 chunk 地址,然后将 script 标签添加到 head 中,实现异步加载 chunk 的功能。
最后是异常处理,给 script 标签添加了 onloade 和 onerror 事件处理函数onScriptComplete
,onScriptComplete
在判断模块加载超时或是加载失败的情况下(缓存的 chunk 不为 0),会调用之前保存的 reject 方法返回模块加载失败的异常,同时还会将 chunk 的缓存标识设置为 undefined,标识未加载。
从前面的流程中我们可以看到,__webpack_require__.e 主要是通过 jsonp
的方式来加载异步 chunk,同时通过 promise 对象来控制异步 chunk 的加载情况:在加载失败的情况下会调用 promise 对象的 reject 方法,在加载成功的情况会执行异步 chunk 文件,也就是 0.main.js。
再来看一下 0.main.js 中的代码。
0.main.js 中的内容比较简单,主要是调用 window 上挂载的全局数组 webpackJsop
的 push 方法,push 的内容包括两部分:chunkId 数组和依赖模块函数,webpackJsonp
全局数组又是在哪里定义的呢?
我们回到 webpackBootStrap
函数,可以看到,webpackJsonp
是在 jsonp
初始化代码中定义的,jsonp
初始化代码主要干了以下几件事:
- 定义了一个全局的
webpackJsonp
数组,用来存储所有的jsonp
回调 - 将
webpackJsonp
的原生 push 方法改写为了webpackJsonpCallback
- 将
webpackJsonp
原生的 push 方法保存为了parentJsonpFunction
所以,在 0.main.js 中调用 webpackJsonp
的 push 方法,最终执行的是 webpackJsonpCallback
中的代码。
最后,看一下 webpackJsonpCallback
的函数实现。
首先是函数参数,data 参数分为两部分:
- chunkIds:一个数组,包含当前 chunk 文件依赖的 chunkId,以及自身的 chunkId
- moreModule:代表当前 chunk 带来的新模块,也就是咱们前面看到的模块执行函数
接下来是执行逻辑:
- 遍历 chunkIds 数组,判断
installedChunks
对象中的 chunk 是否处于加载中状态,如果返回数组,则表示正在加载中,在这种情况下,会取出缓存的 resolve 方法(installedChunks[chunkId][0]),放到 resolves 数组中,等下统一执行,同时,还会将对应的 chuk 设置为已加载 - 将
moreModules
合并到modules
对象中,便于后续同步加载 - 将 data 参数保存到
webpackJsonp
全局数组中 - 遍历 resolves 数组,执行前面缓存的 resolve 方法并清空数组,保证该模块加载开始前所有前置依赖内容包括它自身都已经被加载完毕
总结
总结一下,webpack 采用 jsonp
的方式来实现模块的异步加载:
-
- 通过 __webpack_require__.e 实现动态加载
- 通过 webpackJsonpCallback 实现异步回调
最后,我们用流程图来总结一下 webpack 异步加载模块的流程。