webpack动态加载原理简述

为什么需要动态加载

上一篇讲我写webpack针对markdown的loader的时候,提到了希望能动态加载以优化性能。篇幅原因没有展开说,打算单独写一篇文章讲一下。

当我们访问网站的时候,浏览器会向服务器请求页面资源。首先请求的当然是html文件。html文件里会带很多script标签,浏览器解析到这些标签的话,就会去下载这些js资源。当然有些script标签带了async或者defer属性,不会阻塞后续dom的解析。但是很多单页应用,本身渲染就是强依赖js的,js下载的慢,就会推迟页面开始渲染的时机。因此,我们肯定都是希望下载js越快越好的,因此我们就希望缩减js的体积。

缩减js体积的办法有很多,主要思路都是将非首屏必须的js资源拆出来,以减小首屏js的体积,首屏js加载完了就可以渲染出东西来了。所以所以我们可以对组件做动态加载。动态加载会让组件从项目js中拆出来,被打包成一个单独的js文件,被引入。webpack中我们可以通过import()语法,去动态加载组件

动态加载的实现

知其然,也想知其所以然。动态加载是怎么实现的呢?是webpack借助类似jsonp的方式来实现的。

说白了就是,被动态加载的组件会被单独打包,当你写的import()被执行的时候,webpack会去拼一个script标签出来,script标签的src属性就是被单独打包的组件的url。

window对象上的webpackJsonp数组

细致地说,在首屏的js中,webpack会在window上挂一个数组,用来记录已被动态加载完毕的组件。最初这个数组为空。然后被动态加载的组件,所在的js文件的开头,就是向window对象上的那个数组,push自己进去。因此当动态加载的js被下载完毕时,就会立刻执行push方法,后面有讲,这个push方法被动了手脚,在真的push之前,会执行一些必要的操作(详见webpackJsonpCallback)。

1
2
3
4
5
6
7
## 首屏js
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;
1
2
3
4
## 被单独打包的js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/** 被动态引入的组件,被打包后的内容 */
}]);

动态加载的起点,requireEnsure方法

当执行到动态加载的时候,会调用webpack的requireEnsure方法。requireEnsure方法首先检查,这个模块是否被加载过,如果没加载过,就new一个Promise,然后去添加一个script标签,并为这个script标签设置好onerror和onload事件。

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
## 首屏js
__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);

// 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);
};

加载完毕的回调,webpackJsonpCallback

然后还有一个很重要的方法,叫webpackJsonpCallback。window对象上的数组,push方法被劫持了,就是被改为webpackJsonpCallback方法。在webpackJsonpCallback里,会执行之前promise的resolve,然后把自己记录为已加载状态,最后再把自己push到数组里(劫持了原来的push方法,相当于在真的push之前作了上述操作)。这里也就能看出来,import()的本质是一个promise(严谨地说是promise.all)

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
## 首屏js	
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;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);

while(resolves.length) {
resolves.shift()();
}

};

以上就是webpack动态加载的原理


webpack动态加载原理简述
https://miku03090831.github.io/2023/06/17/webpack动态加载原理简述/
作者
qh_meng
发布于
2023年6月17日
许可协议