qiankun
我们来解释一下这几个字段
| 字段 | 解释 |
| — | — |
| template
| 将脚本文件内容注释后的 html
模板文件 |
| assetPublicPath
| 资源地址根路径,可用于加载子应用资源 |
| getExternalScripts
| 方法:获取外部引入的脚本文件 |
| getExternalStyleSheets
| 方法:获取外部引入的样式表文件 |
| execScripts
| 方法:执行该模板文件中所有的 JS
脚本文件,并且可以指定脚本的作用域 - proxy
对象 |
我们先将 template 模板
、getExternalScripts
和 getExternalStyleSheets
函数的执行结果打印出来,效果如下(见下图):
qiankun
从上图我们可以看到我们外部引入的三个 js
脚本文件,这个模板文件没有外部 css
样式表,对应的样式表数组也为空。
然后我们再来分析 execScripts
方法,该方法的作用就是指定一个 proxy
(默认是 window
)对象,然后执行该模板文件中所有的 JS
,并返回 JS
执行后 proxy
对象的最后一个属性(见下图 1)。在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数(见下图 2),主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。
qiankun
qiankun
在 qiankun
的 importEntry
函数中还传入了配置项 getTemplate
,这个其实是对 html
目标文件的二次处理,这里就不作展开了,有兴趣的可以自行去了解一下。
主应用挂载子应用 HTML 模板
我们回到 qiankun
源码部分继续看(见下图)
qiankun
从上图看出,在 第 85~87 行
处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。
在 第 88 行
中,执行注册子应用时传入的 render
函数,将 HTML Template
和 loading
作为入参,render
函数的内容一般是将 HTML
挂载在指定容器中(见下图)。
qiankun
在这个阶段,主应用已经将子应用基础的 HTML
结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount
方法(如 Vue.$mount
)对子应用状态进行挂载。
此时页面还可以根据 loading
参数开启一个类似加载的效果,直至子应用全部内容加载完成。
沙箱运行环境 - genSandbox
我们回到 qiankun
源码部分继续看,此时还是子应用激活时的回调函数部分(见下图)
qiankun
在 第 90~98 行
是 qiankun
比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js
的沙箱运行环境。如果关闭了 useJsSandbox
选项,那么所有子应用的沙箱环境都是 window
,就很容易对全局状态产生污染。
我们进入到 genSandbox
内部,看看 qiankun
是如何创建的 (JS)沙箱运行环境
。(见下图)
qiankun
从上图可以看出 genSandbox
内部的沙箱主要是通过是否支持 window.Proxy
分为 LegacySandbox
和 SnapshotSandbox
两种。
扩展阅读:多实例还有一种
ProxySandbox
沙箱,这种沙箱模式目前看来是最优方案。由于其表现与旧版本略有不同,所以暂时只用于多实例模式。
ProxySandbox
沙箱稳定之后可能会作为单实例沙箱使用。
LegacySandbox
我们先来看看 LegacySandbox
沙箱是怎么进行状态隔离的(见下图)
qiankun
我们来分析一下 LegacySandbox
类的几个属性:
| 字段 | 解释 |
| — | — |
| addedPropsMapInSandbox
| 记录沙箱运行期间新增的全局变量 |
| modifiedPropsOriginalValueMapInSandbox
| 记录沙箱运行期间更新的全局变量 |
| currentUpdatedPropsValueMap
| 记录沙箱运行期间操作过的全局变量。上面两个 Map
用于 关闭沙箱
时还原全局状态,而 currentUpdatedPropsValueMap
是在 激活沙箱
时还原沙箱的独立状态 |
| name
| 沙箱名称 |
| proxy
| 代理对象,可以理解为子应用的 global/window
对象 |
| sandboxRunning
| 当前沙箱是否在运行中 |
| active
| 激活沙箱,在子应用挂载时启动 |
| inactive
| 关闭沙箱,在子应用卸载时启动 |
| constructor
| 构造函数,创建沙箱环境 |
我们现在从 window.Proxy
的 set
和 get
属性来详细讲解 LegacySandbox
是如何实现沙箱运行环境的。(见下图)
qiankun
注意:子应用沙箱中的
proxy
对象(第 62 行
)可以简单理解为子应用的window
全局对象(代码如下),子应用对全局属性的操作就是对该proxy
对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
// 这里将 proxy 作为 window 参数传入
// 子应用的全局对象就是该子应用沙箱的 proxy 对象
(function(window) {
/* 子应用脚本文件内容 */
})(proxy)
);
在 第 65~72 行中
,当调用 set
向子应用 proxy/window
对象设置属性时,所有的属性设置和更新都会先记录在 addedPropsMapInSandbox
或 modifiedPropsOriginalValueMapInSandbox
中,然后统一记录到currentUpdatedPropsValueMap
中。
在 第 73 行
中修改全局 window
的属性,完成值的设置。
当调用 get
从子应用 proxy/window
对象取值时,会直接从 window
对象中取值。对于非构造函数的取值将会对 this
指针绑定到 window
对象后,再返回函数。
LegacySandbox
的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的,具体实现如下(见下图)。
qiankun
从上图可以看出:
-
第 37 行
:在激活沙箱时,沙箱会通过currentUpdatedPropsValueMap
查询到子应用的独立状态池(沙箱可能会激活多次,这里是沙箱曾经激活期间被修改的全局变量),然后还原子应用状态。 -
第 44~45 行
:在关闭沙箱时,通过addedPropsMapInSandbox
删除在沙箱运行期间新增的全局变量,通过modifiedPropsOriginalValueMapInSandbox
还原沙箱运行期间被修改的全局变量,从而还原到子应用挂载前的状态。
从上面的分析可以得知,LegacySandbox
的沙箱隔离机制利用快照模式实现,我们画一张图来帮助理解(见下图)
qiankun
多实例沙箱 - ProxySandbox
ProxySandbox
是一种新的沙箱模式,目前用于多实例模式的状态隔离。在稳定后以后可能会成为 单实例沙箱
,我们来看看 ProxySandbox
沙箱是怎么进行状态隔离的(见下图)
qiankun
我们来分析一下 ProxySandbox
类的几个属性:
| 字段 | 解释 |
| — | — |
| updateValueMap
| 记录沙箱中更新的值,也就是每个子应用中独立的状态池 |
| name
| 沙箱名称 |
| proxy
| 代理对象,可以理解为子应用的 global/window
对象 |
| sandboxRunning
| 当前沙箱是否在运行中 |
| active
| 激活沙箱,在子应用挂载时启动 |
| inactive
| 关闭沙箱,在子应用卸载时启动 |
| constructor
| 构造函数,创建沙箱环境 |
我们现在从 window.Proxy
的 set
和 get
属性来详细讲解 ProxySandbox
是如何实现沙箱运行环境的。(见下图)
qiankun
注意:子应用沙箱中的
proxy
对象可以简单理解为子应用的window
全局对象(代码如下),子应用对全局属性的操作就是对该proxy
对象属性的操作,带着这份理解继续往下看吧。
// 子应用脚本文件的执行过程:
eval(
// 这里将 proxy 作为 window 参数传入
// 子应用的全局对象就是该子应用沙箱的 proxy 对象
(function(window) {
/* 子应用脚本文件内容 */
})(proxy)
);
当调用 set
向子应用 proxy/window
对象设置属性时,所有的属性设置和更新都会命中 updateValueMap
,存储在 updateValueMap
集合中(第 38 行
),从而避免对 window
对象产生影响(旧版本则是通过 diff
算法还原 window
对象状态快照,子应用之间的状态是隔离的,而父子应用之间 window
对象会有污染)。
当调用 get
从子应用 proxy/window
对象取值时,会优先从子应用的沙箱状态池 updateValueMap
中取值,如果没有命中才从主应用的 window
对象中取值(第 49 行
)。对于非构造函数的取值将会对 this
指针绑定到 window
对象后,再返回函数。
如此一来,ProxySandbox
沙箱应用之间的隔离就完成了,所有子应用对 proxy/window
对象值的存取都受到了控制。设置值只会作用在沙箱内部的 updateValueMap
集合上,取值也是优先取子应用独立状态池(updateValueMap
)中的值,没有找到的话,再从 proxy/window
对象中取值。
相比较而言,ProxySandbox
是最完备的沙箱模式,完全隔离了对 window
对象的操作,也解决了快照模式中子应用运行期间仍然会对 window
造成污染的问题。
我们对 ProxySandbox
沙箱画一张图来加深理解(见下图)
qiankun
SnapshotSandbox
在不支持 window.Proxy
属性时,将会使用 SnapshotSandbox
沙箱,我们来看看其内部实现(见下图)
qiankun
我们来分析一下 SnapshotSandbox
类的几个属性:
| 字段 | 解释 |
| — | — |
| name
| 沙箱名称 |
| proxy
| 代理对象,此处为 window
对象 |
| sandboxRunning
| 当前沙箱是否激活 |
| windowSnapshot
| window
状态快照 |
| modifyPropsMap
| 沙箱运行期间被修改过的 window
属性 |
| constructor
| 构造函数,激活沙箱 |
| active
| 激活沙箱,在子应用挂载时启动 |
| inactive
| 关闭沙箱,在子应用卸载时启动 |
SnapshotSandbox
的沙箱环境主要是通过激活时记录 window
状态快照,在关闭时通过快照还原 window
对象来实现的。(见下图)
qiankun
我们先看 active
函数,在沙箱激活时,会先给当前 window
对象打一个快照,记录沙箱激活前的状态(第 38~40 行
)。打完快照后,函数内部将 window
状态通过 modifyPropsMap
记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window
属性。
在沙箱关闭时,调用 inactive
函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window
对象属性值(第 54 行
)记录在 modifyPropsMap
集合中。在记录了 modifyPropsMap
后,将 window
对象通过快照 windowSnapshot
还原到被沙箱激活前的状态(第 55 行
),相当于是将子应用运行期间对 window
造成的污染全部清除。
SnapshotSandbox
沙箱就是利用快照实现了对 window
对象状态隔离的管理。相比较 ProxySandbox
而言,在子应用激活期间,SnapshotSandbox
将会对 window
对象造成污染,属于一个对不支持 Proxy
属性的浏览器的向下兼容方案。
我们对 SnapshotSandbox
沙箱画一张图来加深理解(见下图)
qiankun
挂载沙箱 - mountSandbox
qiankun
我们继续回到这张图,genSandbox
函数不仅返回了一个 sandbox
沙箱,还返回了一个 mount
和 unmount
方法,分别在子应用挂载时和卸载时的时候调用。
我们先看看 mount
函数内部(见下图)
qiankun
首先,在 mount
内部先激活了子应用沙箱(第 26 行
),在沙箱启动后开始劫持各类全局监听(第 27 行
),我们这里重点看看 patchAtMounting
内部是怎么实现的。(见下图)
qiankun
patchAtMounting
内部调用了下面四个函数:
-
patchTimer(计时器劫持)
-
patchWindowListener(window 事件监听劫持)
-
patchHistoryListener(window.history 事件监听劫持)
-
patchDynamicAppend(动态添加 Head 元素事件劫持)
上面四个函数实现了对 window
指定对象的统一劫持,我们可以挑一些解析看看其内部实现。
计时器劫持 - patchTimer
我们先来看看 patchTimer
对计时器的劫持(见下图)
qiankun
从上图可以看出,patchTimer
内部将 setInterval
进行重载,将每个启用的定时器的 intervalId
都收集起来(第 23~24 行
),以便在子应用卸载时调用 free
函数将计时器全部清除(见下图)。
qiankun
我们来看看在子应用加载时的 setInterval
函数验证即可(见下图)
qiankun
从上图可以看出,在进入子应用时,setInterval
已经被替换成了劫持后的函数,防止全局计时器泄露污染。
动态添加样式表和脚本文件劫持 - patchDynamicAppend
patchWindowListener
和 patchHistoryListener
的实现都与 patchTimer
实现类似,这里就不作复述了。
我们需要重点对 patchDynamicAppend
函数进行解析,这个函数的作用是劫持对 head
元素的操作(见下图)
qiankun
从上图可以看出,patchDynamicAppend
主要是对动态添加的 style
样式表和 script
标签做了处理。
我们先看看对 style
样式表的处理(见下图)
qiankun
从上图可以看出,主要的处理逻辑在 第 68~74 行
,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用
head节点中导致出错
),那么动态 style
样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements
数组中,在后面还会提到其用处。
qiankun
我们再来看看对 script
脚本文件的处理(见下图)
qiankun
对动态 script
脚本文件的处理较为复杂一些,我们也来解析一波:
在 第 83~101 行
处对外部引入的 script
脚本文件使用 fetch
获取,然后使用 execScripts
指定 proxy
对象(作为 window
对象)后执行脚本文件内容,同时也触发了 load
和 error
两个事件。
在 第 103~106 行
处将注释后的脚本文件内容以注释的形式添加到子应用容器内。
在 第 109~113 行
是对内嵌脚本文件的执行过程,就不作复述了。
我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window
对象替换成 proxy
代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。
HTMLHeadElement.prototype.removeChild
的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。
最后我们来看看 free
函数(见下图)
qiankun
这个 free
函数与其他的 patches(劫持函数)
实现不太一样,这里缓存了一份 cssRules
,在重新挂载的时候会执行 rebuild
函数将其还原。这是因为样式元素 DOM
从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style
标签,但是没有渲染样式的问题。
卸载沙箱 - unmountSandbox
我们再回到 mount
函数本身(见下图)
qiankun
从上图可以看出,在 patchAtMounting
函数中劫持了各类全局监听,并返回了解除劫持的 free
函数。在卸载应用时调用 free
函数解除这些全局监听的劫持行为(见下图)
qiankun
从上图可以看到 sideEffectsRebuilders
在 free
后被返回,在 mount
的时候又将被调用 rebuild
重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。
到这里,qiankun
的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。
在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)
qiankun
注册内部生命周期函数
在创建好了沙箱环境后,在 第 100~106 行
注册了一些内部生命周期函数(见下图)
qiankun
在上图中,第 106 行
的 mergeWith
方法的作用是将内置的生命周期函数与传入的 lifeCycles
生命周期函数。
这里的
lifeCycles
生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如加载效果
之类的。(见下图)
qiankun
除了外部传入的生命周期函数外,我们还需要关注 qiankun
内置的生命周期函数做了些什么(见下图)
qiankun
我们对上图的代码进行逐一解析:
-
第 13~15 行
:在加载子应用前beforeLoad
(只会执行一次)时注入一个环境变量,指示了子应用的public
路径。 -
第 17~19 行
:在挂载子应用前beforeMount
(可能会多次执行)时可能也会注入该环境变量。 -
第 23~30 行
:在卸载子应用前beforeUnmount
时将环境变量还原到原始状态。
通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__
的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)
qiankun
触发 beforeLoad
生命周期钩子函数
在注册完了生命周期函数后,立即触发了 beforeLoad
生命周期钩子函数(见下图)
qiankun
从上图可以看出,在 第 108 行
中,触发了 beforeLoad
生命周期钩子函数。
随后,在 第 110 行
执行了 import-html-entry
的 execScripts
方法。指定了脚本文件的运行沙箱(jsSandbox
),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。
qiankun
在 第 112~121 行
对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount
这三个生命周期钩子函数才能被 qiankun
正确嵌入到主应用中。
这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)
qiankun
进入到 mount
挂载流程
在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等
)准备就绪后,qiankun
内部将其组装在一起,返回了三个函数作为 single-spa
内部的生命周期函数(见下图)
qiankun
single-spa
内部的逻辑我们后面再展开说,这里我们可以简单理解为 single-spa
内部的三个生命周期钩子函数:
-
bootstrap
:子应用初始化时调用,只会调用一次; -
mount
:子应用挂载时调用,可能会调用多次; -
unmount
:子应用卸载时调用,可能会调用多次;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
总结一下这三次面试下来我的经验是:
-
一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!
-
代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!
-
尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!
-
多刷面经!
我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!
篇幅有限,仅展示部分内容
了95%以上前端开发知识点,真正体系化!**
[外链图片转存中…(img-7eXil80H-1713352917519)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
总结一下这三次面试下来我的经验是:
-
一定不要死记硬背,要理解原理,否则面试官一深入就会露馅!
-
代码能力一定要注重,尤其是很多原理性的代码(之前两次让我写过Node中间件,Promise.all,双向绑定原理,被虐的怀疑人生)!
-
尽量从面试官的问题中表现自己知识的深度与广度,让面试官发现你的闪光点!
-
多刷面经!
我把所有遇到的面试题都做了一个整理,并且阅读了很多大牛的博客之后写了解析,免费分享给大家,算是一个感恩回馈吧,有需要的朋友【点击我】获取。祝大家早日拿到自己心怡的工作!
篇幅有限,仅展示部分内容