打开全栈工匠技能包-1小时轻松掌握SSR
两小时精通jq+bs插件开发
生产环境下如歌部署Node.js
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
网易内部VUE自定义插件库NPM集成
谁说前端不用懂安全,XSS跨站脚本的危害
webpack的loader到底是什么样的?两小时带你写一个自己loader
本文将针对微前端框架 qiankun
的源码进行深入解析,在源码讲解之前,我们先来了解一下什么是 微前端
。
微前端
是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立开发、独立部署。同时,它们也可以在共享组件的同时进行并行开发——这些组件可以通过 NPM
或者 Git Tag、Git Submodule
来管理。
qiankun(乾坤)
就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa
进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。(见下图)
那么,话不多说,我们的源码解析正式开始。
初始化全局配置 - start(opts)
我们从两个基础 API - registerMicroApps(apps, lifeCycles?) - 注册子应用
和 start(opts?) - 启动主应用
开始,由于 registerMicroApps
函数中设置的回调函数较多,并且读取了 start
函数中设置的初始配置项,所以我们从 start
函数开始解析。
我们从 start
函数开始解析(见下图):
我们对 start
函数进行逐行解析:
第 196 行
:设置window
的__POWERED_BY_QIANKUN__
属性为true
,在子应用中使用window.__POWERED_BY_QIANKUN__
值判断是否运行在主应用容器中。第 198~199 行
:设置配置参数(有默认值),将配置参数存储在importLoaderConfiguration
对象中;第 201~203 行
:检查prefetch
属性,如果需要预加载,则添加全局事件single-spa:first-mount
监听,在第一个子应用挂载后预加载其他子应用资源,优化后续其他子应用的加载速度。第 205 行
:根据singularMode
参数设置是否为单实例模式。第 209~217 行
:根据jsSandbox
参数设置是否启用沙箱运行环境,旧版本需要关闭该选项以兼容 IE。(新版本在单实例模式下默认支持 IE,多实例模式依然不支持 IE)。第 222 行
:调用了single-spa
的startSingleSpa
方法启动应用,这个在single-spa
篇我们会单独剖析,这里可以简单理解为启动主应用。
从上面可以看出,start
函数负责初始化一些全局设置,然后启动应用。这些初始化的配置参数有一部分将在 registerMicroApps
注册子应用的回调函数中使用,我们继续往下看。
注册子应用 - registerMicroApps(apps, lifeCycles?)
registerMicroApps
函数的作用是注册子应用,并且在子应用激活时,创建运行沙箱,在不同阶段调用不同的生命周期钩子函数。(见下图)
从上面可以看出,在 第 70~71 行
处 registerMicroApps
函数做了个处理,防止重复注册相同的子应用。
在 第 74 行
调用了 single-spa
的 registerApplication
方法注册了子应用。
我们直接来看 registerApplication
方法,registerApplication
方法是 single-spa
中注册子应用的核心函数。该函数有四个参数,分别是
name(子应用的名称)
回调函数(activeRule 激活时调用)
activeRule(子应用的激活规则)
props(主应用需要传递给子应用的数据)
这些参数都是由 single-spa
直接实现,这里可以先简单理解为注册子应用(这个我们会在 single-spa
篇展开说)。在符合 activeRule
激活规则时将会激活子应用,执行回调函数,返回一些生命周期钩子函数(见下图)。
注意,这些生命周期钩子函数属于 single-spa
,由 single-spa
决定在何时调用,这里我们从函数名来简单理解。(bootstrap
- 初始化子应用,mount
- 挂载子应用,unmount
- 卸载子应用)
如果你还是觉得有点懵,没关系,我们通过一张图来帮助理解。(见下图)
获取子应用资源 - import-html-entry
我们从上面分析可以看出,qiankun
的 registerMicroApps
方法中第一个入参 apps - Array<RegistrableApp<T>>
有三个参数 name、activeRule、props
都是交给 single-spa
使用,还有 entry
和 render
参数还没有用到。
我们这里需要关注 entry(子应用的 entry 地址)
和 render(子应用被激活时触发的渲染规则)
这两个还没有用到的参数,这两个参数延迟到 single-spa
子应用激活后的回调函数中执行。
那我们假设此时我们的子应用已激活,我们来看看这里做了什么。(见下图)
从上图可以看出,在子应用激活后,首先在 第 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
函数(见下图)
最后
全网独播-价值千万金融项目前端架构实战
从两道网易面试题-分析JavaScript底层机制
RESTful架构在Nodejs下的最佳实践
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
一线互联网企业如何初始化项目-做一个自己的vue-cli
思维无价,看我用Nodejs实现MVC
代码优雅的秘诀-用观察者模式深度解耦模块
前端高级实战,如何封装属于自己的JS库
VUE组件库级组件封装-高复用弹窗组件