获取中...

-

Just a minute...

为什么需要动态加载

上一篇讲我写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动态加载的原理

相关文章
评论
分享
  • 基于tapable来模拟webpack插件机制

    前两天看到ByteFE公众号发了篇文章,从源码去讲webpack。我之前也从很多方面去学习webpack,只是源码一直没太看得进去,这一次我想仔细地读一读。 简要介绍tapable说实话,之前看webpack源码一直没看的太懂,...

    基于tapable来模拟webpack插件机制
  • webpack运行时代码简要分析

    开个坑,主要也是怕自己后面忘了 简单写写,有空了补上 __webpack_modules是一个数组,每个元素都是一个函数,函数接受三个参数:第一个参数module好像没用到,第二个是一个对象(会把本模块要导出的属性,赋值给这个对象),...

    webpack运行时代码简要分析
  • 编写一个简单的webpack loader的踩坑

    因为觉得hexo框架搭的博客功能一般,就有了想要自己写一个博客的想法。然后就想着如果把markdown文件也放到项目里的话,会比较方便,发现需要编写一个加载markdown的loader,于是就有了这篇博客,来记录一下踩过的坑和收获...

    编写一个简单的webpack loader的踩坑
  • 动手实现react-ssr 2

    仓库链接:本篇代码 运行方法参考readme,请使用pnpm,因为本次使用的rspack尚未推出1.0版本,预计后续可能会有很多breakchange,所以lock文件至关重要,而我只上传了pnpm的lock,非常抱歉。 环境:no...

    动手实现react-ssr 2
  • 动手实现react ssr

    关于ssr相关的介绍,可以移步之前的一篇博文,本文不再赘述 日常工作中,我们可能都接触过ssr,不过我们可能通常是借助于框架(例如Next.js)的支持来完成ssr。今天我想尝试,摆脱高度封装的前端框架,只依赖react相关的库,来实...

    动手实现react ssr
  • 深入js——作用域,作用域链,执行栈(一)

    最近在补js基础。当初自学js的时候,总是感觉学的不扎实。只能说不去上手,再怎么死读书,也理解不了。积累了经验,然后自己去思考,这个时候再去看理论,就容易理解了。 感觉工作一年以来,确实学到了很多,但是过于专注于应用方面的知识,反倒...

    深入js——作用域,作用域链,执行栈(一)
  • 简单通俗解释HTTP缓存

    开始前的碎碎念 最近加班真的很严重,组里同事们都已经苦不堪言。业务交付压力非常大,根本没时间去磨练本领,学习技术。真的很讨厌这种没有成长的日子,每天被业务需求赶着跑。每天下班之后,本来应该用于提升自我的时间也被压榨用来写业务代码。 ...

    简单通俗解释HTTP缓存
  • 从invalid hook call报错说到npm install相关

    遇到了invalid hook call报错上周,业务下游的同事反馈了一个生产问题,说我们传的值有问题,领导让我帮忙排查一下。我最近一直没做那个项目相关的需求,打开那个项目切到release分支,拉代码,npm i然后启动,结果就白屏...

    从invalid hook call报错说到npm install相关
  • useState为什么获取不到更新后的值,如何解决

    令人困扰的问题是因为异步吗大家都知道,react的useState和useReducer,在set和dispatch之后,马上去获取值,是拿不到最新的值的。这个原因之一是useState和useReducer是异步更新的,也就是说我们...

    useState为什么获取不到更新后的值,如何解决
  • 发布一个npm包的踩坑

    试着自己发布了一个npm包,记录一下自己踩的坑 package.json首先npm init,然后添加type:module字段,将其定义为一个es6模块。然后在files字段设置要发布上去的内容,比如dist文件夹。 接下来将开发用...

    发布一个npm包的踩坑
Please check the parameter of comment in config.yml of hexo-theme-Annie!