浅析webpack异步模块导入原理

前情提要

随着项目越来越大,性能问题已经成为了困扰业务发展的重要因素。
功能不停地累加后,核心页面已经不堪重负,访问速度愈来愈慢。
业务发展、用户体验都非常迫切的需要释放页面的负载,提高页面加载速度。

目前几种主流异步加载的方式

Webpack2:

  1. require.ensure(dependencies, callback, chunName) 已废弃但保留 链接

    ES提案:

  2. () => import()

    Vue:

  3. component: resolve => require(‘xxx./vue’, resolve)

    React:

  4. react-loadable

希望通过这篇文章能带来以下的帮助?

  1. webpack是如何将异步代码插入到页面上的
  2. 如何检测到异步代码已经查到页面上了?

有关webpack的code Spliting

本章主要是通过一个简单的异步加载的demo解读一下webpack打包后的代码是如何动态加载页面上的

webpack配置相关代码我就不贴了
main.js如下:

1
2
3
4
5
6
(async () => {
const asyncAddWrapper = () => import('./add')

const { default: add } = await asyncAddWrapper()
console.log(add(1, 0))
})()

打包入口文件就是一个简单的通过es提案import动态引入add.js的内容并且打印计算结果

add.js

1
2
3
export default function add (a, b) {
return a + b
}

add.js 默认暴露了一个两个数相加的函数

ok 我们运行打包命令 得到以下几个打包后的文件

  1. 0.js
  2. index.html
  3. main.js

用过异步组件的同学都知道 像这种0.js/1.js之类的文件都是我们的异步代码 只在需要的时候引入到页面上 从而达到减少首屏资源引入的效果
我们看看打包后的main.js是怎么样的 打包后的代码比较难看 我选了重要的片段

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
(function (modules) {
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];


// 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(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]); // installedChunks[chunkId][0] 为 resolve
}
installedChunks[chunkId] = 0; // 设置为已加载
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data); // 将data push到window[webpackJsonp]里

while(resolves.length) {
resolves.shift()(); // 挨个执行resolve
}

}
// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"index": 0
};



// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".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;
}

// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];


// JSONP chunk loading for javascript

var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".

// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise); // installedChunkData = [resolve, reject, promise]

// start chunk loading
var script = document.createElement('script');
var onScriptComplete;

script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);

// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};

// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};

// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// __webpack_public_path__
__webpack_require__.p = "./";

// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;


// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/main.js")
})({
'./src/main.js': (function (module, exports, __webpack_require__) {
(async () => {
const asyncAddWrapper = () => __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./add */ "./src/add.js"))

const { default: add } = await asyncAddWrapper()
console.log(add(1, 0))
})()
})(),
})

可以看到打包之后的main.js文件就是一个立即执行函数 入参modules是一个对象 key值是入口文件 value是打包之前main.js的内容
只不过import被webpack替换成了promise的形式
我们看一下立即执行函数的主函数做了什么事情吧
从上一路向下看 声明了一堆变量和方法 先不管 看到最下面

1
2
3
4
5
6
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

这段是比较重要的一段代码 首先
声明了一个jsonpArray(临时值)把window.webpackJsonp或者空数组赋值给他
然后然后把jsonpArray的push方法赋值给oldJsonpFunction 这个oldJsonpFunction.push此时还是原型上的push
然后将webpackJsonpCallback重载成jsonpArray的push 到这一步window.webpackJsonp.push执行的就是webpackJsonpCallback这个方法
这个webpackJsonpCallback方法在加载异代码的时候会用到
然后执行了jsonpArray = jsonpArray.slice() 将jsonpArray的原型再次指向Array
最后将真正的window[“webpackJsonp”].push赋值给parentJsonpFunction

再看最后的执行代码

1
return __webpack_require__(__webpack_require__.s = "./src/main.js")

这个代码是真正执行main.js的代码
我们先看看__webpack_require__是何方神圣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// The module cache
var installedModules = {};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"index": 0
};
// 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;
}

上述代码生命了两个变量 installedModules和installedChunks
installedModules用于存放已经加载的主入口的模块
installedChunks用于存放已加载异步的模块 key表示当前引入异步模块的编号 也就是文件名 value表示引入的状态 0表示已经加载 如果是个数组表示正在加载

我们看看__webpack_require__做了什么?
先判断了这个主模块是否已经加载过 如果加载过 就返回exports
如果是首次加载 将这个模块插入installedModules

1
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

这一步调用了主模块的代码 并且传入了module/module.exports/__webpack_require__三个参数
ok 到这里估计还是一头雾水 我们再回到立即执行函数的参数modules里 看看执行了什么

1
2
3
4
5
6
7
8
(function (module, exports, __webpack_require__) {
(async () => {
const asyncAddWrapper = () => __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./add */ "./src/add.js"))

const { default: add } = await asyncAddWrapper()
console.log(add(1, 0))
})()
})()

可以看到 import关键字已经被编译成了__webpack_require__.e(0).then(webpack_require.bind(null, ‘./src/add.js’))

由此我们去找找__webpack_require__.e这个方法 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];


// JSONP chunk loading for javascript

var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".

// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise); // installedChunkData = [resolve, reject, promise]

// start chunk loading
var script = document.createElement('script');
var onScriptComplete;

script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);

// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
}

入参chunkId代表的是异步模块的编号 这个方法主要是将插入的过程封装成promise数组
利用promise.all将异步引入的过程变成同步的方法
先通过installedChunks判断该异步模块是否是第一次被引入

  1. 如果是第一次被引入

    1.1 如果是正在引入的状态 直接将该模块插入时的promise放进promises数组

    2.2 如果还没引入过 声明一个promise 并将该promise的fulfilled、reject回调和promise本身赋值给installedChunks[chunkId] 并加入promises数组
    然后通过插入script标签的形式插入到页面上

再回到这步

1
const asyncAddWrapper = () => __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./add */ "./src/add.js"))

webpack_require.e作用是将需要异步引入的模块通过promise封装成同步的形式插入到页面 我们看一下打包异步模块生成的代码 0.js

1
2
3
4
5
6
7
8
9
10
(window.webpackJsonp = window.webpackJsonp || []).push([0], {
'./src/add.js': (function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "default", function() { return add; });
function add (a, b) {
return a + b
}
}),
})

上述讲到window.webpackJsonp.push方法被重写为webpackJsonpCallback 我们看一下这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];


// 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(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]); // installedChunks[chunkId][0] 为 resolve
}
installedChunks[chunkId] = 0; // 设置为已加载
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data); // 将data push到window[webpackJsonp]里

while(resolves.length) {
resolves.shift()(); // 挨个执行resolve
}

}

这个方法接收的参数是一个二维数组 数组第一项是异步模块的编号集合 第二项是异步模块内容
异步代码被插入页面后 会自动执行webpackJsonpCallback方法

  1. 遍历本次插入的异步模块列表 如果该chunkId在installedChunks中 并且installedChunks[chunkId]不为0 说明这个异步模块出于未加载状态
    此时installedChunks[chunkId]的值为[resolve, reject, promise] 并将resolve插入resolves数组 最后通过while循环结束掉所有的promise 此时__webpack_require__.e执行完毕
  2. 遍历异步代码里的模块部分 放到modules做缓存 供热更新使用 parentJsonpFunction此时就是真正的window.webpackJsonp.push方法