微前端方案调研

691 阅读17分钟

微前端技术方案

  • 采用何种方案进行应用拆分?
  • 采用何种方式进行应用通信?
  • 应用之间如何进行隔离?

iframe

  • 微前端的最简单方案,通过iframe加载子应用。
  • 通信可以通过postMessage进行通信。
  • 完美的沙箱机制自带应用隔离。

缺点:用户体验差 (弹框只能在iframe中、在内部切换刷新就会丢失状态)

Web Components

  • 将前端应用程序分解为自定义 HTML 元素。
  • 基于CustomEvent实现通信
  • Shadow DOM天生的作用域隔离

缺点:浏览器支持问题、学习成本、调试困难、修改样式困难等问题

single-spa

  • single-spa 通过路由劫持实现应用的加载(采用SystemJS),提供应用间公共组件加载及 公共业务逻辑处理。子应用需要暴露固定的钩子 bootstrap、mount、 unmount)接入协议
  • 基于props主子应用间通信
  • 无沙箱机制,需要实现自己实现JS沙箱以及CSS沙箱

缺点:学习成本、无沙箱机制、需要对原有的应用进行改造、子应用间相同资源重复加载问 题。

Module federation

  • 通过模块联邦将组件进行打包导出使用
  • 共享模块的方式进行通信
  • 无CSS沙箱和JS沙箱

缺点:需要webpack5。

single-spa

子应用必须暴露钩子

  • single-spa 通过路由劫持实现应用的加载(采用SystemJS),提供应用间公共组件加载及 公共业务逻辑处理。

  • 每个子应用必须改造,暴露钩子,子应用需要暴露固定的钩子 bootstrap、mount、 unmount)接入协议

  • 匹配到路径才加载,没有预加载

  • 需要不停的用registerApplication 注册子应用

qiankun

qiankun.umijs.org/zh/guide

核心原理

Qiankun 是基于 single-spa 的微前端框架,提供了一套完整的微前端解决方案,封装了子应用的加载、卸载和运行环境的隔离。

  • 子应用加载:通过动态的 HTML 解析和 JS 执行加载子应用。当主应用加载子应用时,giankun通过 fetch 方式拉取子应用的静态资源,然后将 HTML 内容插入到主应用的 DOM 中。
  • 沙箱机制:通过 Proxy、iframe 或 snapshot(快照)等机制为每个子应用创建一个独立的运行环境,避免全局状态、变量的冲突。
  • 应用隔离: qiankun 实现了 JavaScript 隔离,通过沙箱机制保证每个子应用的全局对象和变量独立,避免了不同子应用之间的污染。
  • 通信机制:通过自定义事件、发布订阅等方式实现主应用和子应用之间的通信

缺点

  • 基于single-spa,子应用要暴露 bootstrap、mount、 unmount三个钩子

  • 要求子应用暴露的方式要是用umd格式

  • 样式隔离不好,css沙箱不完美

    • strictStylelsolation完全隔离问题,样式无法传递到子应用中。
    • experimentalStylelsolation 子应用dom 结构插入到 body中,样式无法生效
    • qiankun 做了样式隔离,有 动态样式隔离、shadow dom 和 scoped 三种方案,但都有问题:
      • 动态样式隔离:qiankun 默认开启,可以确保单实例场景子应用之间的样式隔离,原理是加载下一个子应用时,将上一个子应用的 <link rel="stylesheet" href="xxx.css"/>、<style>...</style> 等样式相关标签通通删除与替换,来实现样式隔离,缺点是仅支持单例模式(同一时间只能渲染单个子应用),但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离
      • shadow dom 自带样式隔离,但是 shadow dom 内的样式和外界互不影响,导致挂在弹窗的样式会加不上。父应用也没法设置子应用的样式。
      • scoped 的方案是给选择器加了一个 data-qiankun='应用名' 的选择器,这样父应用能设置子应用样式,这样能隔离样式,但是同样有挂在 body 的弹窗样式设置不上的问题,因为 qiankun 的 scoped 不支持全局样式
  • 对vite支持不好,代理沙箱实现的关键是需要将子应用的 window “替换”为 fakeWindow,在这一步 qiankun 是通过函数 window 同名参数 + with 作用域绑定的方式,更改子应用 window 指向为 fakeWindow,最终使用 eval(...) 解析运行子应用的代码。

    const jsCode = `
    (function(window, self, globalThis){
        with(this){
          // your code
          window.a = 1;
          b = 2
          ...
     }
    }).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
    ` 
    eval(jsCode)
    
    • 问题就出在这个 eval 上, vite 的构建产物如果不做特殊降级,默认打包出的就是 ESModule 语法的代码,使用 eval 解析运行会报下图这个错误
    • 官方目前推荐的解决方法是关闭沙箱
  • 快照沙箱,浪费内存

image-20241020170441378

优点

  • 提供标识,判断当前应用在父应用中是否被引用过

    image-20241020171541739

  • qiankun有三套JS沙箱

    • SnapshotSandbox 快照沙箱,存储修改或添加的属性,浪费内存
    • LegacySandbox 单例代理沙箱,通过 ES6的 Proxy 代理 window 属性的 set 操作来记录变更,同时加载多个应用会混乱
    • ProxySandbox 多例代理沙箱

Micro App

micro-app 是基于 webcomponent + qiankun sandbox 的微前端方案

micro-zoe.github.io/micro-app/d…

cloud.tencent.com/developer/a…

www.jianshu.com/p/112920162…

核心原理

基于 Web Components 的技术栈: MicroApp 使用了 Web Components 作为隔离方案,通过Shadow DOM 实现 DOM 隔离,保证每个子应用的 DOM 和 CSS 不会互相影响。

  • 沙箱隔离机制:同样支持 JavaScript 沙箱,通过劫持全局对象(如 window 、document )等方式来隔离子应用的运行环境,确保各个子应用之间不会相互干扰。
  • 预加载技术:通过资源预加载的技术,在用户尚未访问某个子应用时,提前加载部分资源,提升子应用的启动速度。
  • 通信与状态管理:通过事件总线和跨应用的通信协议来实现子应用之间的状态同步和数据共享

优点

  • 不必改造子应用,降低子应用改造的成本
  • 使用简单:将功能封装到 WebComponent 中
  • 零依赖。无依赖、更高的扩展性
  • 兼容所有框架 技术栈无关
  • 通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染
  • 静态资源地址补全
  • 元素隔离
  • 支持插件系统
  • 组件式的 api 更加符合使用习惯,支持子应用保活
  • 支持vite

MicroApp 相比于 qiankun 和 wujie 这两个微前端框架,在性能方面有几项独特的优势,主要体现在以下几个方面

资源预加载

  • MicroApp 提供了更加高效的 资源预加载 机制,在用户尚未访问某个子应用时,可以提前加载子应用的静态资源(如 HTML、CSS、JS 文件)。这样一来,当用户真正访问子应用时,子应用的加载速度会显著提升,避免了首次访问时的等待延迟。这在性能优化中非常关键,尤其适合频繁切换子应用的场景。
  • qiankun 和 wujie 也支持按需加载,但是它们通常不会主动提前预加载子应用的资源,而是在访问时通过异步加载方式拉取资源文件,首次加载速度相对较慢。

Web Components (Shadow DOM) 隔离

  • MicroApp 使用 Web Components 技术实现 DOM 和样式隔离,主要依赖 Shadow DOM 来确保每个子应用的样式与 DOM 结构都是独立的。由于 Shadow DOM 是浏览器原生支持的技术,性能上有天然的优势,不需要像 qiankun 和 wujie 那样依赖 JavaScript 层面的额外处理。
  • 在 qiankun 中,DOM 和样式的隔离依赖于 JavaScript 实现的沙箱机制,这种隔离方式会增加额外的开销,尤其是在需要频繁操作 DOM 时,性能开销更大。wujie 则使用了 iframe 隔离,虽然隔离彻底,但 iframe的性能在子应用嵌套或大量交互时表现较差,

JavaScript 沙箱性能优化

  • MicroApp 通过更轻量化的 JavaScript 沙箱实现来降低运行时的性能消耗,它通过劫持全局对象(如 wind0w、 document )来隔离子应用的 JavaScript 执行环境,同时避免了重复劫持的性能开销。
  • qiankun 使用的沙箱机制较为复杂,尤其是对全局对象和变量的隔离,需要在每次切换应用时重新计算和重置上下文,可能会引发一定的性能损耗,特别是在大型应用中。
  • wujie 的 iframe 方式则无需依赖 JavaScript 沙箱,因为 iframe 本身天然隔离,但这也带来了 iframe 自身的性能问题,比如加载开销较大、内存占用多等问题。

内存占用与性能开销

  • MicroApp 由于采用了轻量的资源管理和沙箱机制,内存占用相对较少,且在子应用的挂载与卸载方面进行了优化,可以更快速地释放内存资源,减少应用长期运行时的性能压力。
  • qiankun 的内存占用相对较大,尤其是在子应用较多时,内存的管理和释放不够及时,有时会导致内存泄漏问题。 qiankun 的沙箱和生命周期管理机制相对复杂,因此每个子应用切换时都需要做较多的状态管理可能导致性能瓶颈。 wujie 使用 iframe 会占用较多的内存资源,尤其是在多个 iframe 同时存在时,性能开销会更大,因为每个iframe 相当于一个独立的浏览器上下文。

启动速度

  • MicroApp 的启动速度通常较快,尤其是在子应用数量较多的场景中,得益于资源预加载和轻量化沙箱的组合,子应用的加载和切换性能更为流畅。
  • giankun 的子应用启动过程涉及静态资源的拉取和沙箱上下文的初始化,启动速度相对较慢,特别是应用切换频繁时,性能瓶颈会更加明显。
  • wujie 的 iframe 隔离机制意味着每个子应用都需要一个独立的浏览器上下文,这也导致子应用启动时可能需要更多时间进行渲染和初始化。

MicroApp 沙箱机制的详细实现方案

MicroApp 的沙箱机制是其核心功能之一,主要用于实现对每个子应用的运行环境隔离,从而避免多个子应用

之间的相互干扰和污染。与其他微前端框架不同,MicroApp 的沙箱机制专注于轻量化和性能优化,它通过

**理模式(Proxy)**和对全局环境的劫持来保证子应用的独立性。下面详细介绍其沙箱机制的实现方案。

  • MicroApp 的沙箱机制通过以下几个方面实现了子应用的隔离与性能优化
    • 1.JavaScript 隔离:通过 Proxy 劫持全局对象,确保子应用的独立运行环境
    • 2.样式隔离:通过 Shadow DOM 实现子应用样式作用域的隔离,防止样式污染
    • 3.DOM 隔离:独立的 DOM 挂载节点,确保子应用的 DOM 结构不影响其他应用
    • 4.事件隔离:劫持全局事件绑定,保证子应用事件的独立性。
    • 5.性能优化:通过按需启停和快照沙箱,减少资源开销,提升应用性能
    • 这种设计使得 MicroApp 能够在保证子应用独立性和隔离性的同时,优化性能、特别适合对运行效率和资源管理要求较高的前端项目。

JavaScript 执行环境隔离

  • 劫持全局对象

    • MicroApp 创建一个用于子应用的沙箱环境,在这个环境中通过代理模式拦截对全局对象(如 window、 document 等)的访问。通过 Proxy 可以拦截并重定向对这些全局对象的读写操作,保证每个子应用在沙箱内的 window 变量是独立的,避免互相干扰。

      image-20241022195225181

  • 变量劫持与恢复

    • 当子应用加载时, MicroApp 会启动一个沙箱,在这个沙箱中将全局变量的读写操作重定向到代理对象,避免对主应用的全局对象进行直接修改。当子应用卸载时,沙箱会被销毁,所有的全局变量修改也会自动恢复到应用加载前的状态。

      image-20241022195325801

  • 执行隔离

    • 通过代理模式,将子应用的全局变量、事件绑定等操作隔离在各自的上下文中,保证每个子应用运 行时不会影响到其他子应用和主应用。

样式隔离

微前端中的样式隔离问题通常是由于不同应用共享相同的 CSS 作用域,可能会导致样式冲突。MicroApp 采

用了Shadow DOM 来隔离子应用的 DOM 和 CSS

  • Shadow DOM 隔离

    • 在每个子应用的根节点上使用 Shadow DOM,这样子应用的样式作用域被限制在Shadow DOM 内部,保证样式不外泄,不会影响到其他子应用或主应用的页面布局和样式

    • const shadowRoot = document,getElementById( 'app').attachShadow({ mode: 'open' });shadowRoot.innerHTML ='<style>/* 子应用独立样式 */</style><div>子应用内容</div>';
      
    • 动态样式处理:对于子应用的外部样式表, MicroApp 通过动态解析 CSS,并将其作用范围限制在子应用的 Shadow DOM 中。例如通过动态插入 标签,将子应用的 CSS 隔离到对应的沙箱中。

DOM隔离

  • DOM 隔离 是为了防止子应用的 DOM 节点与主应用或其他子应用发生冲突

  • DOM 节点管理: MicroApp 为每个子应用创建一个独立的挂载节点,所有的子应用 DOM 元素都会挂载到这个节点中。由于在沙箱环境下,子应用只能操作它自己的 DOM 树,这样可以有效防止子应用篡改主应用的 DOM 结构。

  • const appContainer = document.createElement('div');appContainer.setAttribute("id',"micro-app-container');document.body.appendchild(appContainer);
    
  • 卸载时清理 DOM:当子应用被卸载时, MicroApp 会清理子应用对应的 DOM 节点,避免残留的 DOM 元素影响到页面性能或布局。

全局事件隔离

在普通的前端应用中,事件绑定通常是全局的,例如使用 window.addEventListener 。在微前端中,多个子

应用共享同一个 window 对象,容易发生事件冲突

  • 事件劫持: MicroApp 通过在沙箱中劫持全局事件的添加和移除,确保子应用的事件绑定只在当前沙箱内生效。例如,每个子应用的 addEventListener 都会通过代理模式进行包装,保证事件只对当前子应用有效。

  • const rawAddEventListener = window.addEventListener;window.addEventListener = function (type, listener, options){//仅允许当前沙箱的事件在当前子应用内生效if(isIncurrentSandbox()){return rawAddEventListener.call(window, type, listener, options);
    
  • 卸载时清理事件:当子应用卸载时, MicroApp 也会自动移除所有在沙箱内添加的全局事件监听器,避免事件泄漏或冲突。

性能优化

  • 沙箱的启停:为了减少内存和资源的占用, MicroApp 的沙箱是按需启动和销毁的。即在子应用加载时启动沙箱,在子应用卸载时销毁沙箱,确保资源能够及时释放,避免长时间运行后出现内存泄漏问题。
  • 快照沙箱:在某些场景中,MicroApp 也支持类似于快照沙箱的机制,在子应用被挂起或重新激活时,能够快速恢复到之前的状态,进一步提升性能。

wujie

wujie-micro.github.io/doc/

  • 用iframe做js沙箱
  • 用webComponent(shadowRoot) 做css隔离
  • 渲染采用webComponent

核心原理

Wujie 是一种轻量级的微前端框架,设计的目标是提供更加开箱即用的体验,简化微前端的开发与集成。

  • iframe 沙箱: wujie 使用 iframe 作为主要的隔离机制,确保子应用在一个真正隔离的环境中运行,避免了 CSS 和 JavaScript 的全局污染问题。
  • 动态注入资源: wujie 可以通过动态注入 HTML、CSS 和 JS 资源的方式来实现子应用的加载,并且在 iframe 中执行这些资源,保证了与主应用的解耦。
  • 自定义通信机制:提供了主应用与子应用之间的消息传递机制,方便数据共享和状态管理

img

综合考量

  • MicroApp 的性能优势体现在 预加载、轻量级沙箱 和 Web Components 隔离 方面,它能够更加高效地管理资源并减少切换和运行时的性能开销。
  • qiankun 更适合功能丰富的大型系统,但由于其沙箱机制复杂,在性能上会稍逊色于 MicroApp。
  • wuie 则以简化开发体验为目标,但由于依赖 iframe,性能开销相对较大,特别是在高频交互的场景下。 因此,MicroApp 更加适合对 性能要求较高,尤其是子应用多、需要快速切换的项目。
  • qiankun 更加成熟、支持灵活的沙箱和丰富的生命周期管理,适合大型复杂系统。

  • wujie 使用 iframe 作为主要隔离手段,简化了隔离问题,适合对开发体验要求高的项目。

  • MicroApp 通过 Web Components 进行隔离,关注性能优化,适合对运行时性能要求较高的场景。这些微前端框架的共同点在于:

    • 应用隔离:通过沙箱技术,保证子应用之间的隔离性。

    • 动态加载:按需加载子应用的资源,降低初始加载时间

    • 通信机制:提供主应用与子应用之间的高效通信方式

自己设计数据+样式隔离

JavaScript隔离

JavaScript 数据隔离的核心是避免全局变量、函数或对象在不同子应用之间共享。通过沙箱机制可以实现每个

子应用独立的 JavaScript 运行环境

  • 使用 Proxy 代理全局对象

    • 通过 Proxy 劫持对全局对象(如 window 、document 等)的访问,给每个子应用创建一个独立的上下文环境。每个子应用通过 Proxy 访问到的是独立的全局变量,避免彼此干扰。

      image-20241022200329834

  • 劫持全局函数和事件

    • 需要对全局事件和函数调用进行隔离处理,比如通过拦载 window.addEventListener 使得事件只在当前子应用的沙箱内有效。

      image-20241022200411581

  • 生命周期管理与数据清理

    • 当子应用卸载时,清理相关数据和事件,防止内存泄漏或污染全局环境。可以通过保存原始的全局对象和事件绑定,在卸载时恢复。

      image-20241022200446839

CSS样式隔离

CSS 样式隔离的目的是防止子应用的样式污染到主应用或其他子应用。常见的隔离方式包括 Shadow DOM

Scoped css 和 Css Modules

  • Shadow DOM 隔离

    • 使用 Shadow DOM 隔离每个子应用的样式。Shadow DOM 是浏览器原生的隔离方案,可以有效防止 CSS 样式的全局污染。每个子应用可以使用 Shadow DOM 作为根节点,使其内部的样式只作用于这个子应用的 DOM 结构中。

      image-20241022200558445

  • Scoped Css 或 Css Modules

    • 如果无法使用 Shadow DOM,还可以通过Scoped CSS或CSS Modules来实现样式隔离。Scoped Css 是通过在每个组件的 CSS 选择器前自动添加独特的标识符来实现样式作用范围的限制。CSS Modules 则通过生成唯一的 CSS 类名来确保样式的唯一性。

    • 例如,在使用 CSS Modules 时,CSS 类名会被自动编译成独一无二的名称

      image-20241022200642217

image-20241022200655263

  • 动态样式隔离

image-20241022200742175

资源加载隔离

image-20241022200811832

子应用的卸载与清理

image-20241022200838046

  • 要实现一个完整的数据和样式隔离方案,可以采取以下措施
    • JavaScript 数据隔离:通过 Proxy 劫持全局对象,为每个子应用创建独立的运行环境,隔离全局变量和事件

    • CSS 样式隔离:通过 Shadow DOM 或 CSS Modules 等技术实现样式的作用域隔离,防止样式污染

    • 资源加载隔离:动态加载和管理子应用的静态资源,确保资源的作用范围仅限于子应用内。

    • 子应用卸载与清理:子应用卸载时,清理 DOM、事件和全局状态,避免资源泄漏和全局污染。

    • 通过这些技术,你可以实现多个子应用的数据和样式隔离,确保每个子应用都能够在独立的环境中安全运行,同时不影响主应用和其他子应用。