从上图可以看出,在子应用激活后,首先在 第 81~84 行
处使用了 import-html-entry
库从 entry
进入加载子应用,加载完成后将返回一个对象(见下图)
我们来解释一下这几个字段
|
| |
我们先将 template 模板
、getExternalScripts
和 getExternalStyleSheets
函数的执行结果打印出来,效果如下(见下图):
从上图我们可以看到我们外部引入的三个 js
脚本文件,这个模板文件没有外部 css
样式表,对应的样式表数组也为空。
然后我们再来分析 execScripts
方法,该方法的作用就是指定一个 proxy
(默认是 window
)对象,然后执行该模板文件中所有的 JS
,并返回 JS
执行后 proxy
对象的最后一个属性(见下图 1)。在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数(见下图 2),主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。
在 qiankun
的 importEntry
函数中还传入了配置项 getTemplate
,这个其实是对 html
目标文件的二次处理,这里就不作展开了,有兴趣的可以自行去了解一下。
主应用挂载子应用 HTML 模板
我们回到 qiankun
源码部分继续看(见下图)
从上图看出,在 第 85~87 行
处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载之后才开始。
在 第 88 行
中,执行注册子应用时传入的 render
函数,将 HTML Template
和 loading
作为入参,render
函数的内容一般是将 HTML
挂载在指定容器中(见下图)。
在这个阶段,主应用已经将子应用基础的 HTML
结构挂载在了主应用的某个容器内,接下来还需要执行子应用对应的 mount
方法(如 Vue.$mount
)对子应用状态进行挂载。
此时页面还可以根据 loading
参数开启一个类似加载的效果,直至子应用全部内容加载完成。
沙箱运行环境 - genSandbox
我们回到 qiankun
源码部分继续看,此时还是子应用激活时的回调函数部分(见下图)
在 第 90~98 行
是 qiankun
比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js
的沙箱运行环境。如果关闭了 useJsSandbox
选项,那么所有子应用的沙箱环境都是 window
,就很容易对全局状态产生污染。
我们进入到 genSandbox
内部,看看 qiankun
是如何创建的 (JS)沙箱运行环境
。(见下图)
从上图可以看出 genSandbox
内部的沙箱主要是通过是否支持 window.Proxy
分为 ProxySandbox
和 SnapshotSandbox
两种(多实例还有一种 LegacySandbox
沙箱,这里我们不作讲解)。
ProxySandbox
我们先来看看 ProxySandbox
沙箱是怎么进行状态隔离的(见下图)
我们来分析一下 ProxySandbox
类的几个属性:
|
| |
我们现在从 window.Proxy
的 set
和 get
属性来详细讲解 ProxySandbox
是如何实现沙箱运行环境的。(见下图)
注意:子应用沙箱中的 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
沙箱画一张图来加深理解(见下图)
SnapshotSandbox
在不支持 window.Proxy
属性时,将会使用 SnapshotSandbox
沙箱,我们来看看其内部实现(见下图)
我们来分析一下 SnapshotSandbox
类的几个属性:
|
| |
SnapshotSandbox
的沙箱环境主要是通过激活时记录 window
状态快照,在关闭时通过快照还原 window
对象来实现的。(见下图)
我们先看 active
函数,在沙箱激活时,会先给当前 window
对象打一个快照,记录沙箱激活前的状态(第 38~40 行
)。打完快照后,函数内部将 window
状态通过 modifyPropsMap
记录还原到上次的沙箱运行环境,也就是还原沙箱激活期间(历史记录)修改过的 window
属性。
在沙箱关闭时,调用 inactive
函数,在沙箱关闭前通过遍历比较每一个属性,将被改变的 window
对象属性值(第 54 行
)记录在 modifyPropsMap
集合中。在记录了 modifyPropsMap
后,将 window
对象通过快照 windowSnapshot
还原到被沙箱激活前的状态(第 55 行
),相当于是将子应用运行期间对 window
造成的污染全部清除。
SnapshotSandbox
沙箱就是利用快照实现了对 window
对象状态隔离的管理。相比较 ProxySandbox
而言,在子应用激活期间,SnapshotSandbox
将会对 window
对象造成污染,属于一个对不支持 Proxy
属性的浏览器的向下兼容方案。
我们对 SnapshotSandbox
沙箱画一张图来加深理解(见下图)
挂载沙箱 - mountSandbox
我们继续回到这张图,genSandbox
函数不仅返回了一个 sandbox
沙箱,还返回了一个 mount
和 unmount
方法,分别在子应用挂载时和卸载时的时候调用。
我们先看看 mount
函数内部(见下图)
首先,在 mount
内部先激活了子应用沙箱(第 26 行
),在沙箱启动后开始劫持各类全局监听(第 27 行
),我们这里重点看看 patchAtMounting
内部是怎么实现的。(见下图)
patchAtMounting
内部调用了下面四个函数:
patchTimer(计时器劫持)
patchWindowListener(window 事件监听劫持)
patchHistoryListener(window.history 事件监听劫持)
patchDynamicAppend(动态添加 Head 元素事件劫持)
上面四个函数实现了对 window
指定对象的统一劫持,我们可以挑一些解析看看其内部实现。
计时器劫持 - patchTimer
我们先来看看 patchTimer
对计时器的劫持(见下图)
从上图可以看出,patchTimer
内部将 setInterval
进行重载,将每个启用的定时器的 intervalId
都收集起来(第 23~24 行
),以便在子应用卸载时调用 free
函数将计时器全部清除(见下图)。
我们来看看在子应用加载时的 setInterval
函数验证即可(见下图)
从上图可以看出,在进入子应用时,setInterval
已经被替换成了劫持后的函数,防止全局计时器泄露污染。
动态添加样式表和脚本文件劫持 - patchDynamicAppend
patchWindowListener
和 patchHistoryListener
的实现都与 patchTimer
实现类似,这里就不作复述了。
我们需要重点对 patchDynamicAppend
函数进行解析,这个函数的作用是劫持对 head
元素的操作(见下图)
从上图可以看出,patchDynamicAppend
主要是对动态添加的 style
样式表和 script
标签做了处理。
我们先看看对 style
样式表的处理(见下图)
从上图可以看出,主要的处理逻辑在 第 68~74 行
,如果当前子应用处于激活状态(判断子应用的激活状态主要是因为:当主应用切换路由时可能会自动添加动态样式表,此时需要避免主应用的样式表被添加到子应用
head节点中导致出错
),那么动态 style
样式表就会被添加到子应用容器内(见下图),在子应用卸载时样式表也可以和子应用一起被卸载,从而避免样式污染。同时,动态样式表也会存储在 dynamicStyleSheetElements
数组中,在后面还会提到其用处。
我们再来看看对 script
脚本文件的处理(见下图)
对动态 script
脚本文件的处理较为复杂一些,我们也来解析一波:
在 第 83~101 行
处对外部引入的 script
脚本文件使用 fetch
获取,然后使用 execScripts
指定 proxy
对象(作为 window
对象)后执行脚本文件内容,同时也触发了 load
和 error
两个事件。
在 第 103~106 行
处将注释后的脚本文件内容以注释的形式添加到子应用容器内。
在 第 109~113 行
是对内嵌脚本文件的执行过程,就不作复述了。
我们可以看出,对动态添加的脚本进行劫持的主要目的就是为了将动态脚本运行时的 window
对象替换成 proxy
代理对象,使子应用动态添加的脚本文件的运行上下文也替换成子应用自身。
HTMLHeadElement.prototype.removeChild
的逻辑就是多加了个子应用容器判断,其他无异,就不展开说了。
最后我们来看看 free
函数(见下图)
这个 free
函数与其他的 patches(劫持函数)
实现不太一样,这里缓存了一份 cssRules
,在重新挂载的时候会执行 rebuild
函数将其还原。这是因为样式元素 DOM
从文档中删除后,浏览器会自动清除样式元素表。如果不这么做的话,在重新挂载时会出现存在 style
标签,但是没有渲染样式的问题。
卸载沙箱 - unmountSandbox
我们再回到 mount
函数本身(见下图)
从上图可以看出,在 patchAtMounting
函数中劫持了各类全局监听,并返回了解除劫持的 free
函数。在卸载应用时调用 free
函数解除这些全局监听的劫持行为(见下图)
从上图可以看到 sideEffectsRebuilders
在 free
后被返回,在 mount
的时候又将被调用 rebuild
重建动态样式表。这块环环相扣,是稍微有点绕,没太看明白的同学可以翻上去再看一遍。
到这里,qiankun
的最核心部分-沙箱机制,我们就已经解析完毕了,接下来我们继续剖析别的部分。
在这里我们画一张图,对沙箱的创建过程进行一个总梳理(见下图)
注册内部生命周期函数
在创建好了沙箱环境后,在 第 100~106 行
注册了一些内部生命周期函数(见下图)
在上图中,第 106 行
的 mergeWith
方法的作用是将内置的生命周期函数与传入的 lifeCycles
生命周期函数。
这里的 lifeCycles
生命周期函数指的是全子应用共享的生命周期函数,可用于执行多个子应用间相同的逻辑操作,例如 加载效果
之类的。(见下图)
除了外部传入的生命周期函数外,我们还需要关注 qiankun
内置的生命周期函数做了些什么(见下图)
我们对上图的代码进行逐一解析:
第 13~15 行
:在加载子应用前beforeLoad
(只会执行一次)时注入一个环境变量,指示了子应用的public
路径。第 17~19 行
:在挂载子应用前beforeMount
(可能会多次执行)时可能也会注入该环境变量。第 23~30 行
:在卸载子应用前beforeUnmount
时将环境变量还原到原始状态。
通过上面的分析我们可以得出一个结论,我们可以在子应用中获取该环境变量,将其设置为 __webpack_public_path__
的值,从而使子应用在主应用中运行时,可以匹配正确的资源路径。(见下图)
触发 beforeLoad
生命周期钩子函数
在注册完了生命周期函数后,立即触发了 beforeLoad
生命周期钩子函数(见下图)
从上图可以看出,在 第 108 行
中,触发了 beforeLoad
生命周期钩子函数。
随后,在 第 110 行
执行了 import-html-entry
的 execScripts
方法。指定了脚本文件的运行沙箱(jsSandbox
),执行完子应用的脚本文件后,返回了一个对象,对象包含了子应用的生命周期钩子函数(见下图)。
在 第 112~121 行
对子应用的生命周期钩子函数做了个检测,如果在子应用的导出对象中没有发现生命周期钩子函数,会在沙箱对象中继续查找生命周期钩子函数。如果最后没有找到生命周期钩子函数则会抛出一个错误,所以我们的子应用一定要有 bootstrap, mount, unmount
这三个生命周期钩子函数才能被 qiankun
正确嵌入到主应用中。
这里我们画一张图,对子应用挂载前的初始化过程做一个总梳理(见下图)
进入到 mount
挂载流程
最后
全网独播-价值千万金融项目前端架构实战
从两道网易面试题-分析JavaScript底层机制
RESTful架构在Nodejs下的最佳实践
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
一线互联网企业如何初始化项目-做一个自己的vue-cli
思维无价,看我用Nodejs实现MVC
代码优雅的秘诀-用观察者模式深度解耦模块
前端高级实战,如何封装属于自己的JS库
VUE组件库级组件封装-高复用弹窗组件