本文将针对微前端框架 qiankun
的源码进行深刻解析,在源码讲解以前,咱们先来了解一下什么是 微前端
。css
微前端
是一种相似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还能够独立开发、独立部署。同时,它们也能够在共享组件的同时进行并行开发——这些组件能够经过 NPM
或者 Git Tag、Git Submodule
来管理。html
qiankun(乾坤)
就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa
进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。(见下图)前端
那么,话很少说,咱们的源码解析正式开始。vue
start(opts)
咱们从两个基础 API - registerMicroApps(apps, lifeCycles?) - 注册子应用
和 start(opts?) - 启动主应用
开始,因为 registerMicroApps
函数中设置的回调函数较多,而且读取了 start
函数中设置的初始配置项,因此咱们从 start
函数开始解析。webpack
咱们从 start
函数开始解析(见下图):git
咱们对 start
函数进行逐行解析:github
第 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
注册子应用的回调函数中使用,咱们继续往下看。web
registerMicroApps(apps, lifeCycles?)
registerMicroApps
函数的做用是注册子应用,而且在子应用激活时,建立运行沙箱,在不一样阶段调用不一样的生命周期钩子函数。(见下图)算法
从上面能够看出,在 第 70~71 行
处 registerMicroApps
函数作了个处理,防止重复注册相同的子应用。bootstrap
在 第 74 行
调用了 single-spa
的 registerApplication
方法注册了子应用。
咱们直接来看 registerApplication
方法,registerApplication
方法是 single-spa
中注册子应用的核心函数。该函数有四个参数,分别是
name(子应用的名称)
回调函数(activeRule 激活时调用)
activeRule(子应用的激活规则)
props(主应用须要传递给子应用的数据)
这些参数都是由 single-spa
直接实现,这里能够先简单理解为注册子应用(这个咱们会在 single-spa
篇展开说)。在符合 activeRule
激活规则时将会激活子应用,执行回调函数,返回一些生命周期钩子函数(见下图)。
注意,这些生命周期钩子函数属于single-spa
,由single-spa
决定在什么时候调用,这里咱们从函数名来简单理解。(bootstrap
- 初始化子应用,mount
- 挂载子应用,unmount
- 卸载子应用)
若是你仍是以为有点懵,不要紧,咱们经过一张图来帮助理解。(见下图)
咱们从上面分析能够看出,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 |
将脚本文件内容注释后的 html 模板文件 |
assetPublicPath |
资源地址根路径,可用于加载子应用资源 |
getExternalScripts |
方法:获取外部引入的脚本文件 |
getExternalStyleSheets |
方法:获取外部引入的样式表文件 |
execScripts |
方法:执行该模板文件中全部的 JS 脚本文件,而且能够指定脚本的做用域 - proxy 对象 |
咱们先将 template 模板
、getExternalScripts
和 getExternalStyleSheets
函数的执行结果打印出来,效果以下(见下图):
从上图咱们能够看到咱们外部引入的三个 js
脚本文件,这个模板文件没有外部 css
样式表,对应的样式表数组也为空。
而后咱们再来分析 execScripts
方法,该方法的做用就是指定一个 proxy
(默认是 window
)对象,而后执行该模板文件中全部的 JS
,并返回 JS
执行后 proxy
对象的最后一个属性(见下图 1)。在微前端架构中,这个对象通常会包含一些子应用的生命周期钩子函数(见下图 2),主应用能够经过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操做。
在 qiankun
的 importEntry
函数中还传入了配置项 getTemplate
,这个实际上是对 html
目标文件的二次处理,这里就不做展开了,有兴趣的能够自行去了解一下。
咱们回到 qiankun
源码部分继续看(见下图)
从上图看出,在 第 85~87 行
处,先对单实例进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载以后才开始。
在 第 88 行
中,执行注册子应用时传入的 render
函数,将 HTML Template
和 loading
做为入参,render
函数的内容通常是将 HTML
挂载在指定容器中(见下图)。
在这个阶段,主应用已经将子应用基础的 HTML
结构挂载在了主应用的某个容器内,接下来还须要执行子应用对应的 mount
方法(如 Vue.$mount
)对子应用状态进行挂载。
此时页面还能够根据 loading
参数开启一个相似加载的效果,直至子应用所有内容加载完成。
咱们回到 qiankun
源码部分继续看,此时仍是子应用激活时的回调函数部分(见下图)
在 第 90~98 行
是 qiankun
比较核心的部分,也是几个子应用之间状态独立的关键,那就是 js
的沙箱运行环境。若是关闭了 useJsSandbox
选项,那么全部子应用的沙箱环境都是 window
,就很容易对全局状态产生污染。
咱们进入到 genSandbox
内部,看看 qiankun
是如何建立的 (JS)沙箱运行环境
。(见下图)
从上图能够看出 genSandbox
内部的沙箱主要是经过是否支持 window.Proxy
分为 ProxySandbox
和 SnapshotSandbox
两种(多实例还有一种 LegacySandbox
沙箱,这里咱们不做讲解)。
咱们先来看看 ProxySandbox
沙箱是怎么进行状态隔离的(见下图)
咱们来分析一下 ProxySandbox
类的几个属性:
字段 | 解释 |
---|---|
updateValueMap |
记录沙箱中更新的值,也就是每一个子应用中独立的状态池 |
name |
沙箱名称 |
proxy |
代理对象,能够理解为子应用的 global/window 对象 |
sandboxRunning |
当前沙箱是否在运行中 |
active |
激活沙箱,在子应用挂载时启动 |
inactive |
关闭沙箱,在子应用卸载时启动 |
constructor |
构造函数,建立沙箱环境 |
咱们如今从 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
沙箱画一张图来加深理解(见下图)
在不支持 window.Proxy
属性时,将会使用 SnapshotSandbox
沙箱,咱们来看看其内部实现(见下图)
咱们来分析一下 SnapshotSandbox
类的几个属性:
字段 | 解释 |
---|---|
name |
沙箱名称 |
proxy |
代理对象,此处为 window 对象 |
sandboxRunning |
当前沙箱是否激活 |
windowSnapshot |
window 状态快照 |
modifyPropsMap |
沙箱运行期间被修改过的 window 属性 |
constructor |
构造函数,激活沙箱 |
active |
激活沙箱,在子应用挂载时启动 |
inactive |
关闭沙箱,在子应用卸载时启动 |
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
标签,可是没有渲染样式的问题。
咱们再回到 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
挂载流程在一些初始化配置(如 子应用资源、运行沙箱环境、生命周期钩子函数等等
)准备就绪后,qiankun
内部将其组装在一块儿,返回了三个函数做为 single-spa
内部的生命周期函数(见下图)
single-spa
内部的逻辑咱们后面再展开说,这里咱们能够简单理解为 single-spa
内部的三个生命周期钩子函数:
bootstrap
:子应用初始化时调用,只会调用一次;mount
:子应用挂载时调用,可能会调用屡次;unmount
:子应用卸载时调用,可能会调用屡次;咱们能够看出,在 bootstrap
阶段调用了子应用暴露的 bootstrap
生命周期函数。
咱们这里对 mount
阶段进行展开,看看在子应用 mount
阶段执行了哪些函数(见下图)
咱们进行逐行解析:
第 127~133 行
:对单实例模式进行检测。在单实例模式下,新的子应用挂载行为会在旧的子应用卸载以后才开始。(因为这里是串行顺序执行,因此若是某一处发生阻塞的话,会阻塞全部后续的函数执行)第 134 行
:执行注册子应用时传入的 render
函数,将 HTML Template
和 loading
做为入参。这里通常是在发生了一次 unmount
后,再次进行 mount
挂载行为时将 HTML
挂载在指定容器中(见下图)
因为初始化的时候已经调用过一次render
,因此在首次调用mount
时可能已经执行过一次render
方法。在下面的代码中也有对重复挂载的状况进行判断的语句 -
if (frame.querySelector("div") === null
,防止重复挂载子应用。
第 135 行
:触发了 beforeMount
全局生命周期钩子函数;第 136 行
:挂载沙箱,这一步中激活了对应的子应用沙箱,劫持了部分全局监听(如 setInterval
)。此时开始子应用的代码将在沙箱中运行。(反推可知,在 beforeMount
前的部分全局操做将会对主应用形成污染,如 setInterval
)第 137 行
:触发子应用的 mount
生命周期钩子函数,在这一步一般是执行对应的子应用的挂载操做(如 ReactDOM.render、Vue.$mount
。(见下图)第 138 行
:再次调用 render
函数,此时 loading
参数为 false
,表明子应用已经加载完成。第 139 行
:触发了 afterMount
全局生命周期钩子函数;第 140~144 行
:在单实例模式下设置 prevAppUnmountedDeferred
的值,这个值是一个 promise
,在当前子应用卸载时才会被 resolve
,在该子应用运行期间会阻塞其余子应用的挂载动做(第 134 行
);咱们在上面很详细的剖析了整个子应用的 mount
挂载流程,若是你尚未搞懂的话,不要紧,咱们再画一个流程图来帮助理解。(见下图)
unmount
卸载流程咱们刚才梳理了子应用的 mount
挂载流程,咱们如今就进入到子应用的 unmount
卸载流程。在子应用激活阶段, activeRule
未命中时将会触发 unmount
卸载行为,具体的行为以下(见下图)
从上图咱们能够看出,unmount
卸载流程要比 mount
简单不少,咱们直接来梳理一下:
第 148 行
:触发了 beforeUnmount
全局生命周期钩子函数;第 149 行
:这里与 mount
流程的顺序稍微有点不一样,这里先执行了子应用的 unmount
生命周期钩子函数,保证子应用仍然是运行在沙箱内,避免形成状态污染。在这里通常是对子应用的一些状态进行清理和卸载操做。(以下图,销毁了刚才建立的 vue
实例)第 150 行
:卸载沙箱,关闭了沙箱的激活状态。第 151 行
:触发了 afterUnmount
全局生命周期钩子函数;第 152 行
:触发 render
方法,而且传入的 appContent
为空字符串,此处能够清空主应用容器内的内容。第 153~156 行
:当前子应用卸载完成后,在单实例模式下触发 prevAppUnmountedDeferred.resolve()
,使其余子应用的挂载行为得以继续进行,再也不阻塞。咱们对 unmount
卸载流程也画一张图,帮助你们理解(见下图)。
到这里,咱们对 qiankun
框架的总流程梳理就差很少了。这里应该作个总结,你们看了这么多文字,估计你们也看累了,最后用一张图对 qiankun
的总流程进行总结吧。
传统的云控制台应用,几乎都会面临业务快速发展以后,单体应用进化成巨石应用的问题。咱们要如何维护一个巨无霸中台应用?
上面这个问题引出了微前端架构理念,因此微前端的概念也愈来愈火,咱们团队最近也在尝试转型微前端架构。
工欲善其事必先利其器,因此本文针对 qiankun
的源码进行解读,在分享知识的同时也是帮助本身理解。
这是咱们团队对微前端架构的最佳实践(见下图),若是有需求的话,能够在评论区留言,咱们会考虑出一篇《微前端框架 qiankun
最佳实践》来帮助你们搭建一套微前端架构。
这篇文章我花了大约半个月的时间来进行排版、梳理、画图,坚持下来一路写完确实很不容易。
若是您已经看到这里了,但愿您仍是点个赞再走吧~
若是本文对您有帮助的话,请点个赞和收藏吧!
您的点赞是对做者的最大鼓励,也可让更多人看到本篇文章!
若是对 《微前端框架
qiankun 最佳实践》
有兴趣的话,还请在评论区留言告诉做者吧!