极致的微前端方案_无界的源码剖析

2,184 阅读3分钟

无界:基于 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内。通过代理 iframedocumentwebcomponent,可以实现两者的互联。

如何利用无界搭建微前端框架?

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挂载的节点就是子应用本身的节点位置)

image.png

无界嵌入子应用分为三种模式

  • 保活模式

    • 子应用的 alive 设置为true时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。
    • 保活模式下,子应用只会进行一次渲染,页面发生切换时承载子应用domwebcomponent会保留在内存中,当子应用重新激活时无界会将内存中的webcomponent重新挂载到容器上
  • 重建模式

    • 子应既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式,每次页面切换不仅会销毁承载子应用domwebcomponent,还会销毁承载子应用jsiframe,相应的wujie实例和子应用实例都会被销毁

    • 重建模式下改变 url 子应用的路由会跳转对应路由,但是在 路由同步 场景并且子应用的路由同步参数已经同步到主应用url上时则无法生效,因为改变url后会导致子应用销毁重新渲染,此时如果有同步参数则同步参数的优先级最高

  • 单例模式

    • 子应用的alivefalse且进行了生命周期改造时进入单例模式。

    • 子应用页面如果切走,会调用window.__WUJIE_UNMOUNT销毁子应用当前实例,子应用页面如果切换回来,会调用window.__WUJIE_MOUNT渲染子应用新的子应用实例

    • 在单例式下,改变 url 子应用的路由会发生跳转到对应路由

    • 如果主应用上有多个菜单栏用到了子应用的不同页面,在每个页面启动该子应用的时候将name设置为同一个,这样可以共享一个wujie实例,承载子应用jsiframe也实现了共享,不同页面子应用的url不同,切换这个子应用的过程相当于:销毁当前应用实例 => 同步新路由 => 创建新应用实例

无界的实现方案

应用加载机制和 js 沙箱机制

将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱,内部有完整的historylocation接口,子应用实例instance运行在iframe中,路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。

iframe 连接机制和 css 沙箱机制

无界采用webcomponent来实现页面的样式隔离,无界会创建一个wujie自定义元素,然后将子应用的完整结构渲染在内部

子应用的实例instanceiframe内运行,dom在主应用容器下的webcomponent内,通过代理 iframedocumentwebcomponent,可以实现两者的互联。

document的查询类接口:getElementsByTagName、getElementsByClassName、getElementsByName、getElementById、querySelector、querySelectorAll、head、body全部代理到webcomponent,这样instancewebcomponent就精准的链接起来。

当子应用发生切换,iframe保留下来,子应用的容器可能销毁,但webcomponent依然可以选择保留,这样等应用切换回来将webcomponent再挂载回容器上,子应用可以获得类似vuekeep-alive的能力.

路由同步机制

iframe内部进行history.pushState,浏览器会自动的在joint session history中添加iframesession-history,浏览器的前进、后退在不做任何处理的情况就可以直接作用于子应用

劫持iframehistory.pushStatehistory.replaceState,就可以将子应用的url同步到主应用的query参数上,当刷新浏览器初始化iframe时,读回子应用的url并使用iframehistory.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一样传入子应用环境即可,不明白这里为什么单独走代理的形式)

image.png

最后将HTML以及CSS添加到shadowroot中