深入浅出解析阿里成熟的微前端框架 qiankun 源码【图文并茂】

98ab1e8242001d1116d1e13fde8443a1.png

qiankun

我们来解释一下这几个字段

| 字段 | 解释 |

| — | — |

| template | 将脚本文件内容注释后的 html 模板文件 |

| assetPublicPath | 资源地址根路径,可用于加载子应用资源 |

| getExternalScripts | 方法:获取外部引入的脚本文件 |

| getExternalStyleSheets | 方法:获取外部引入的样式表文件 |

| execScripts | 方法:执行该模板文件中所有的 JS 脚本文件,并且可以指定脚本的作用域 - proxy 对象 |

我们先将 template 模板getExternalScripts 和 getExternalStyleSheets 函数的执行结果打印出来,效果如下(见下图):

6cd4636a5e4cf9f9ce37090a48a825ac.png

qiankun

从上图我们可以看到我们外部引入的三个 js 脚本文件,这个模板文件没有外部 css 样式表,对应的样式表数组也为空。

然后我们再来分析 execScripts 方法,该方法的作用就是指定一个 proxy(默认是 window)对象,然后执行该模板文件中所有的 JS,并返回 JS 执行后 proxy 对象的最后一个属性(见下图 1)。在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数(见下图 2),主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。

0032001eedd3667791aa3e704f87819d.png

qiankun

98e2d3f9cdfb2b6b0b28ace94fdd08c5.png

qiankun

在 qiankun 的 importEntry 函数中还传入了配置项 getTemplate,这个其实是对 html 目标文件的二次处理,这里就不作展开了,有兴趣的可以自行去了解一下。

主应用挂载子应用 HTML 模板

我们回到 qiankun 源码部分继续看(见下图)

a66ca5df2d14dfc22c8e058417e42c20.png

qiankun

从上图看出,在 第 85~87 行 处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。

在 第 88 行 中,执行注册子应用时传入的 render 函数,将 HTML Template 和 loading 作为入参,render 函数的内容一般是将 HTML 挂载在指定容器中(见下图)。

13f3b73b02413f1510ed0d07be5f4fbe.png

qiankun

在这个阶段,主应用已经将子应用基础的 HTML 结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount 方法(如 Vue.$mount)对子应用状态进行挂载。

此时页面还可以根据 loading 参数开启一个类似加载的效果,直至子应用全部内容加载完成。

沙箱运行环境 - genSandbox

我们回到 qiankun 源码部分继续看,此时还是子应用激活时的回调函数部分(见下图)

04ca62c03a7c3c358fa21c72848b2ace.png

qiankun

在 第 90~98 行 是 qiankun 比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js 的沙箱运行环境。如果关闭了 useJsSandbox 选项,那么所有子应用的沙箱环境都是 window,就很容易对全局状态产生污染。

我们进入到 genSandbox 内部,看看 qiankun 是如何创建的 (JS)沙箱运行环境。(见下图)

c413950c2dea3bc1fd3a4b9bbdbfedbc.png

qiankun

从上图可以看出 genSandbox 内部的沙箱主要是通过是否支持 window.Proxy 分为 LegacySandbox 和 SnapshotSandbox 两种。

扩展阅读:多实例还有一种 ProxySandbox 沙箱,这种沙箱模式目前看来是最优方案。由于其表现与旧版本略有不同,所以暂时只用于多实例模式。

ProxySandbox 沙箱稳定之后可能会作为单实例沙箱使用。

LegacySandbox

我们先来看看 LegacySandbox 沙箱是怎么进行状态隔离的(见下图)

5d7fee9a6d7583162775f977ad20e750.png

qiankun

我们来分析一下 LegacySandbox 类的几个属性:

| 字段 | 解释 |

| — | — |

| addedPropsMapInSandbox | 记录沙箱运行期间新增的全局变量 |

| modifiedPropsOriginalValueMapInSandbox | 记录沙箱运行期间更新的全局变量 |

| currentUpdatedPropsValueMap | 记录沙箱运行期间操作过的全局变量。上面两个 Map 用于 关闭沙箱 时还原全局状态,而 currentUpdatedPropsValueMap 是在 激活沙箱 时还原沙箱的独立状态 |

| name | 沙箱名称 |

| proxy | 代理对象,可以理解为子应用的 global/window 对象 |

| sandboxRunning | 当前沙箱是否在运行中 |

| active | 激活沙箱,在子应用挂载时启动 |

| inactive | 关闭沙箱,在子应用卸载时启动 |

| constructor | 构造函数,创建沙箱环境 |

我们现在从 window.Proxy 的 set 和 get 属性来详细讲解 LegacySandbox 是如何实现沙箱运行环境的。(见下图)

daf9cb5935792c1ccc40d3ab15ff4542.png

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 的沙箱隔离是通过激活沙箱时还原子应用状态,卸载时还原主应用状态(子应用挂载前的全局状态)实现的,具体实现如下(见下图)。

8bccaea65f26db4a7167e8bacecca5cb.png

qiankun

从上图可以看出:

  • 第 37 行:在激活沙箱时,沙箱会通过 currentUpdatedPropsValueMap 查询到子应用的独立状态池(沙箱可能会激活多次,这里是沙箱曾经激活期间被修改的全局变量),然后还原子应用状态。

  • 第 44~45 行:在关闭沙箱时,通过 addedPropsMapInSandbox 删除在沙箱运行期间新增的全局变量,通过 modifiedPropsOriginalValueMapInSandbox 还原沙箱运行期间被修改的全局变量,从而还原到子应用挂载前的状态。

从上面的分析可以得知,LegacySandbox 的沙箱隔离机制利用快照模式实现,我们画一张图来帮助理解(见下图)

b64be4476b8449a1aeb11b35d920500b.png

qiankun

多实例沙箱 - ProxySandbox

ProxySandbox 是一种新的沙箱模式,目前用于多实例模式的状态隔离。在稳定后以后可能会成为 单实例沙箱,我们来看看 ProxySandbox 沙箱是怎么进行状态隔离的(见下图)

ebae9c29053e6f3ac525caf4848ebe0f.png

qiankun

我们来分析一下 ProxySandbox 类的几个属性:

| 字段 | 解释 |

| — | — |

| updateValueMap | 记录沙箱中更新的值,也就是每个子应用中独立的状态池 |

| name | 沙箱名称 |

| proxy | 代理对象,可以理解为子应用的 global/window 对象 |

| sandboxRunning | 当前沙箱是否在运行中 |

| active | 激活沙箱,在子应用挂载时启动 |

| inactive | 关闭沙箱,在子应用卸载时启动 |

| constructor | 构造函数,创建沙箱环境 |

我们现在从 window.Proxy 的 set 和 get 属性来详细讲解 ProxySandbox 是如何实现沙箱运行环境的。(见下图)

d3a460caf62e5a4dc619a91b78035737.png

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 沙箱画一张图来加深理解(见下图)

2d50124c3c4105d987732ef35cef8d20.png

qiankun

SnapshotSandbox

在不支持 window.Proxy 属性时,将会使用 SnapshotSandbox 沙箱,我们来看看其内部实现(见下图)

31dc9bf9c9a4f069bb8d3a616f692280.png

qiankun

我们来分析一下 SnapshotSandbox 类的几个属性:

| 字段 | 解释 |

| — | — |

| name | 沙箱名称 |

| proxy | 代理对象,此处为 window 对象 |

| sandboxRunning | 当前沙箱是否激活 |

| windowSnapshot | window 状态快照 |

| modifyPropsMap | 沙箱运行期间被修改过的 window 属性 |

| constructor | 构造函数,激活沙箱 |

| active | 激活沙箱,在子应用挂载时启动 |

| inactive | 关闭沙箱,在子应用卸载时启动 |

SnapshotSandbox 的沙箱环境主要是通过激活时记录 window 状态快照,在关闭时通过快照还原 window 对象来实现的。(见下图)

f548387ba7023ba0059ff83b96f17368.png

qiankun

我们先看 active 函数,在沙箱激活时,会先给当前 window 对象打一个快照,记录沙箱激活前的状态(第 38~40 行)。打完快照后,函数内部将 window 状态通过 modifyPropsMap 记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window 属性。

在沙箱关闭时,调用 inactive 函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window 对象属性值(第 54 行)记录在 modifyPropsMap 集合中。在记录了 modifyPropsMap 后,将 window 对象通过快照 windowSnapshot 还原到被沙箱激活前的状态(第 55 行),相当于是将子应用运行期间对 window 造成的污染全部清除。

SnapshotSandbox 沙箱就是利用快照实现了对 window 对象状态隔离的管理。相比较 ProxySandbox 而言,在子应用激活期间,SnapshotSandbox 将会对 window 对象造成污染,属于一个对不支持 Proxy 属性的浏览器的向下兼容方案。

我们对 SnapshotSandbox 沙箱画一张图来加深理解(见下图)

2172b6bf72897b3c3ece2a3c289f02c3.png

qiankun

挂载沙箱 - mountSandbox

6c496b487a5235b423b3caec90dd6e75.png

qiankun

我们继续回到这张图,genSandbox 函数不仅返回了一个 sandbox 沙箱,还返回了一个 mount 和 unmount 方法,分别在子应用挂载时和卸载时的时候调用。

我们先看看 mount 函数内部(见下图)

a41353de4fff29545d37363a5f323cd8.png

qiankun

首先,在 mount 内部先激活了子应用沙箱(第 26 行),在沙箱启动后开始劫持各类全局监听(第 27 行),我们这里重点看看 patchAtMounting 内部是怎么实现的。(见下图)

e0861cccb6392a812d41059bc73134d3.png

qiankun

patchAtMounting 内部调用了下面四个函数:

  • patchTimer(计时器劫持)

  • patchWindowListener(window 事件监听劫持)

  • patchHistoryListener(window.history 事件监听劫持)

  • patchDynamicAppend(动态添加 Head 元素事件劫持)

上面四个函数实现了对 window 指定对象的统一劫持,我们可以挑一些解析看看其内部实现。

计时器劫持 - patchTimer

我们先来看看 patchTimer 对计时器的劫持(见下图)

5980686739a143c24f05ba5f0cebb9ca.png

qiankun

从上图可以看出,patchTimer 内部将 setInterval 进行重载,将每个启用的定时器的 intervalId 都收集起来(第 23~24 行),以便在子应用卸载时调用 free 函数将计时器全部清除(见下图)。

276d04412f560e0366708f209d26efb0.png

qiankun

我们来看看在子应用加载时的 setInterval 函数验证即可(见下图)

902b7e3ea316e1dac8529375219d3e4e.png

qiankun

从上图可以看出,在进入子应用时,setInterval 已经被替换成了劫持后的函数,防止全局计时器泄露污染。

动态添加样式表和脚本文件劫持 - patchDynamicAppend

patchWindowListener 和 patchHistoryListener 的实现都与 patchTimer 实现类似,这里就不作复述了。

我们需要重点对 patchDynamicAppend 函数进行解析,这个函数的作用是劫持对 head 元素的操作(见下图)

f96481d60f43ec912b1e0ec7dc715586.png

qiankun

从上图可以看出,patchDynamicAppend 主要是对动态添加的 style 样式表和 script 标签做了处理。

我们先看看对 style 样式表的处理(见下图)

751892f944c6ad7e00c726787fb339c5.png

qiankun

从上图可以看出,主要的处理逻辑在 第 68~74 行,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用head节点中导致出错),那么动态 style 样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements 数组中,在后面还会提到其用处。

e2a68eaac81a159880f7324ec422dcb9.png

qiankun

我们再来看看对 script 脚本文件的处理(见下图)

02d622bc045b47da9538f5f2b56bdb2b.png

qiankun

对动态 script 脚本文件的处理较为复杂一些,我们也来解析一波:

在 第 83~101 行 处对外部引入的 script 脚本文件使用 fetch 获取,然后使用 execScripts 指定 proxy 对象(作为 window 对象)后执行脚本文件内容,同时也触发了 load 和 error 两个事件。

在 第 103~106 行 处将注释后的脚本文件内容以注释的形式添加到子应用容器内。

在 第 109~113 行 是对内嵌脚本文件的执行过程,就不作复述了。

我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window 对象替换成 proxy 代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。

HTMLHeadElement.prototype.removeChild 的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。

最后我们来看看 free 函数(见下图)

94bccb19a610eaefae1e146eeb5bd1c1.png

qiankun

这个 free 函数与其他的 patches(劫持函数) 实现不太一样,这里缓存了一份 cssRules,在重新挂载的时候会执行 rebuild 函数将其还原。这是因为样式元素 DOM 从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style 标签,但是没有渲染样式的问题。

卸载沙箱 - unmountSandbox

我们再回到 mount 函数本身(见下图)

389f327d985e2c353f04d66bbe27634f.png

qiankun

从上图可以看出,在 patchAtMounting 函数中劫持了各类全局监听,并返回了解除劫持的 free 函数。在卸载应用时调用 free 函数解除这些全局监听的劫持行为(见下图)

db0123b921f1930b80d00eba00267fb7.png

qiankun

从上图可以看到 sideEffectsRebuilders 在 free 后被返回,在 mount 的时候又将被调用 rebuild 重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。

到这里,qiankun 的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。

在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)

bed0b45fb43724f7aba3ab6a6b8dbc03.png

qiankun

注册内部生命周期函数

在创建好了沙箱环境后,在 第 100~106 行 注册了一些内部生命周期函数(见下图)

ab79e99207d538a306736e235cbb2300.png

qiankun

在上图中,第 106 行 的 mergeWith 方法的作用是将内置的生命周期函数与传入的 lifeCycles 生命周期函数。

这里的 lifeCycles 生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如 加载效果 之类的。(见下图)

8daed96c0e51072606071a69fcb3ae09.png

qiankun

除了外部传入的生命周期函数外,我们还需要关注 qiankun 内置的生命周期函数做了些什么(见下图)

9c601ab3811e9c2348dbfa99c6f7421e.png

qiankun

我们对上图的代码进行逐一解析:

  • 第 13~15 行:在加载子应用前 beforeLoad(只会执行一次)时注入一个环境变量,指示了子应用的 public 路径。

  • 第 17~19 行:在挂载子应用前 beforeMount(可能会多次执行)时可能也会注入该环境变量。

  • 第 23~30 行:在卸载子应用前 beforeUnmount 时将环境变量还原到原始状态。

通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__ 的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)

a1ad2c26d04b8bbfeb0b15f5d9897a2e.png

qiankun

触发 beforeLoad 生命周期钩子函数

在注册完了生命周期函数后,立即触发了 beforeLoad 生命周期钩子函数(见下图)

59bf9a546e9d76b22a7b0844b19b743a.png

qiankun

从上图可以看出,在 第 108 行 中,触发了 beforeLoad 生命周期钩子函数。

随后,在 第 110 行 执行了 import-html-entry 的 execScripts 方法。指定了脚本文件的运行沙箱(jsSandbox),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。

07f86afc5ac7daa6d8046a5155754922.png

qiankun

在 第 112~121 行 对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount 这三个生命周期钩子函数才能被 qiankun 正确嵌入到主应用中。

这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)

0f8c1da79bb67c9ec02e2a6910ebd77f.png

qiankun

进入到 mount 挂载流程

在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等)准备就绪后,qiankun 内部将其组装在一起,返回了三个函数作为 single-spa 内部的生命周期函数(见下图)

cf11377cf5f5659453cf928cf56b19fc.png

qiankun

single-spa 内部的逻辑我们后面再展开说,这里我们可以简单理解为 single-spa 内部的三个生命周期钩子函数:

  • bootstrap:子应用初始化时调用,只会调用一次;

  • mount:子应用挂载时调用,可能会调用多次;

  • unmount:子应用卸载时调用,可能会调用多次;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!

  • mount:子应用挂载时调用,可能会调用多次;

  • unmount:子应用卸载时调用,可能会调用多次;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-TQ4jGEP3-1712375833515)]

[外链图片转存中…(img-fsyVPwAz-1712375833516)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-GfaZR84R-1712375833516)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!

[外链图片转存中…(img-jxLMe6aG-1712375833516)]

[外链图片转存中…(img-nEOUeq7d-1712375833516)]

  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值