无界:基于 WebComponent 容器 + iframe 沙箱
官方文档:wujie-micro.github.io/doc/
无界的优势
- 性能和体积兼具(相比qiankun,没有使用with、eval,因此性能方面有很大的提升)
- 多应用同时激活在线(每个iframe就代表一个沙箱环境)
- 应用级别的 keep-alive
- 纯净无污染(得力于iframe和shadowroot天然的隔离)
- 组件式的使用方式
WebComponent
- 自定义标签
- 影子节点(shadow dom):可以很好的实现
样式隔离
- Template模板、Slot插槽
Iframe
优点
- 使用起来简单高效,成本低
- 隔离非常完美,CSS、DOM、JS都完全隔离
缺陷
- 无法保留iframe的状态,每次刷新页面,iframe的url都会恢复至默认值
- dom分割过于严重,以至于iframe内的弹框无法实现全屏展示
- Web间的通信麻烦
- 每次打开,白屏时间过长,视觉体验差
无界的核心思想:利用Iframe特性实现沙箱,让子应用的脚本在iframe
内运行,利用shadow dom实现样式隔离,子应用的dom
在主应用容器下的webcomponent
内。通过代理 iframe
的document
到webcomponent
,可以实现两者的互联。
如何利用无界搭建微前端框架?
import WujieVue from "wujie-vue2";
Vue.use(WujieVue);
const { setupApp, preloadApp } = WujieVue;
/** 配置子应用 */
setupApp({
/** 唯一性用户必须保证 */
name: "react17",
/** 需要渲染的url */
url: 'http://localhost:7100',
/** 自定义iframe属性 */
attrs: {},
/** 子应用保活,state不会丢失 */
alive: true,
/** 子应用采用降级iframe方案 */
degrade: false,
/** 预执行(搭配预加载使用) */
exec: true,
});
/** 预加载 */
preloadApp({name: 'react17'})
/** React17.vue 在基座项目中配置相关路由渲染该组件 */
<template>
<WujieVue width="100%" height="100%" name="react17"></WujieVue>
</template>
子应用的改造
无界对子应用的侵入非常小,在满足跨域条件下子应用可以不用改造。如果子应用与主应用存在跨域,需要做cors设置处理
这里以vue2为例($mount
挂载的节点就是子应用本身的节点位置)
无界嵌入子应用分为三种模式
-
保活模式
- 子应用的 alive 设置为
true
时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。 - 保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用
dom
的webcomponent
会保留在内存中,当子应用重新激活时无界会将内存中的webcomponent
重新挂载到容器上
- 子应用的 alive 设置为
-
重建模式
-
单例模式
-
子应用的
alive
为false
且进行了生命周期改造时进入单例模式。 -
子应用页面如果切走,会调用
window.__WUJIE_UNMOUNT
销毁子应用当前实例,子应用页面如果切换回来,会调用window.__WUJIE_MOUNT
渲染子应用新的子应用实例 -
在单例式下,改变 url 子应用的路由会发生跳转到对应路由
-
如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将
name
设置为同一个,这样可以共享一个wujie
实例,承载子应用js
的iframe
也实现了共享,不同页面子应用的url
不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例
-
无界的实现方案
应用加载机制和 js 沙箱机制
将子应用的js
注入主应用同域的iframe
中运行,iframe
是一个原生的window
沙箱,内部有完整的history
和location
接口,子应用实例instance
运行在iframe
中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。
iframe 连接机制和 css 沙箱机制
无界采用webcomponent来实现页面的样式隔离,无界会创建一个wujie
自定义元素,然后将子应用的完整结构渲染在内部
子应用的实例instance
在iframe
内运行,dom
在主应用容器下的webcomponent
内,通过代理 iframe
的document
到webcomponent
,可以实现两者的互联。
将document
的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body
全部代理到webcomponent
,这样instance
和webcomponent
就精准的链接起来。
当子应用发生切换,iframe
保留下来,子应用的容器可能销毁,但webcomponent
依然可以选择保留,这样等应用切换回来将webcomponent
再挂载回容器上,子应用可以获得类似vue
的keep-alive
的能力.
路由同步机制
在iframe
内部进行history.pushState
,浏览器会自动的在joint session history中添加iframe
的session-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用
劫持iframe
的history.pushState
和history.replaceState
,就可以将子应用的url
同步到主应用的query
参数上,当刷新浏览器初始化iframe
时,读回子应用的url
并使用iframe
的history.replaceState
进行同步
通信机制
承载子应用的iframe
和主应用是同域的,所以主、子应用天然就可以很好的进行通信,在无界我们提供三种通信方式
- props 注入机制
子应用通过$wujie.props
可以轻松拿到主应用注入的数据
- window.parent 通信机制
子应用iframe
沙箱和主应用同源,子应用可以直接通过window.parent
和主应用通信
- 去中心化的通信机制
无界提供了EventBus
实例,注入到主应用和子应用,所有的应用可以去中心化的进行通信
源码剖析
注册自定义标签在无界加载的时候就注册了而且只执行一次,connect里的逻辑则是在自定义元素链接到dom文档中时执行
WuJie类是无界沙箱的核心,沙箱实例就是WuJie类的实力,里面存储着shadowroot、模板以及各种子应用的配置属性
创建iframe的逻辑
重写子应用的pushState实现子应用路由和基座路由的联动
子应用执行window的函数时,期望函数的this指向子应用的window,但是由于代理window是在基座生成的,所以this指向的基座的window,所以需要修正this指向
分析出document的所有属性以及方法,有些属性需要代理到全局的document,有些需要代理到沙箱的shadow root节点上【这里是子应用js和Webcomponents链接的关键步骤】
这一步和qiankun都一样,解析子应用模板的脚本、样式、模板
将解析的脚本插入到子应用iframe内,这里如果开启了fiber并且浏览器支持,可以在预加载时优化一些性能,在空闲时间去执行该操作
插入脚本时,并不是直接插入,需要对其进行改造。需要将代理的window、location作用于子应用脚步的执行环境,你可能会说这里怎么没有将代理对象proxyDocument传入? 这里我也无法理解
在创建iframe的时候,这里监听了子应用的document(我感觉这里和window一样传入子应用环境即可,不明白这里为什么单独走代理的形式)
最后将HTML以及CSS添加到shadowroot中