注册
web

三大微前端框架,谁是你的理想型?

1. 分享目标:

2. 什么是微前端?

故事开始于三年前…

小明为公司重构了一版新的管理后台,采用了市面上最流行的SPA渲染模式,具体技术栈使用的是 react + react-router。

项目第一版很快就顺利上线了,但在后续的迭代中,遇到一个棘手的问题:产品经理希望快速复用之前项目的某些页面。这让小明犯了难,因为老项目是用“上古神器” jQuery 写的,完全重构成 react,成本非常高。这时后端老哥丢过来一句:“你们前端用 iframe 嵌进来就可以了吧? ” 小明心里很清楚 iframe 有许多小毛病,但在当时,也确实没有比它更好的选择了。

上线后,随着时间的推移,用户产生了困惑:

  1. 为什么这个页面的弹框不居中了?
  2. 为什么这个页面的跳转记录无法保存? ...

小明心里其实非常清楚,这一切都是 iframe 带来的弊端

时间来到三年后的今天,小明听说微前端能够解决 iframe 的各种疑难杂症,于是展开了调研。

市面上对微前端的定义让人眼花缭乱,比如微前端是:

这里给出我对微前端最接地气的定义:

“类似于iframe的效果,但没有它带来的各种问题”——小明。

3. 主流技术方向分类

首先,“微前端”作为近几年国内前端界最火的技术之一,目前存在多个技术流派。我按照它们对 iframe 看法的不同,将主流微前端方案分为了三大派系:革新派、改良派、中间派

3.1. 革新派 qiankun

 qiankun 为主的革新派认为: iframe 问题很多,应避免使用它。 完全可以利用现有的前端技术自建一套应用隔离渲染方案。

3.1.1. 原理:

3.1.1.1. 基于 single-spa

将路由切换与子应用加载、卸载等生命周期结合起来是微前端的一项核心能力。这一步 qiankun 是基于 single-spa 实现的,不同的是它支持以 html 作为加载子应用的入口,不必像 single-spa 那样需要手动梳理资源链接,内部插件 import-html-entry 会自动分析 html 以获取 js 和 css。

3.1.1.2. 样式隔离

为了确保子应用之间样式互不影响,qiankun 内置了三种样式隔离模式:

  1. 默认模式。

原理是加载下一个子应用时,将上一个子应用的 等样式相关标签通通删除与替换,来实现样式隔离。缺点是仅支持单例模式(同一时间只能渲染单个子应用),且没法做到主子应用及多个子应用之间的样式隔离。

  1. 严格模式。

可通过 strictStyleIsolation:true 开启。原理是利用 webComponent 的 shadowDOM 实现。但它的问题在于隔离效果太好了,在目前的前端生态中有点水土不服,这里举两个例子。

  • 可能会影响 React 事件。比如这个issue 当 Shadow Dom 遇上 React 事件 ,大致原因是在 React 中事件是“合成事件”,在React 17 版本之前,所有用户事件都需要冒泡到 document 上,由 React 做统一分发与处理,如果冒泡的过程中碰到 shadowRoot 节点,就会将事件拦截在 shadowRoot 范围内,此时event.target 强制指向 shadowRoot,导致在 react 中事件无响应。React 17 之后事件监听位置由 document 改为了挂载 App 组件的 root 节点,就不存在此问题了。

  • 弹框样式丢失。 原因是主流UI框架比如 antd 为了避免上层元素的样式影响,通常会把弹框相关的 DOM 通过 document.body.appendChild插入到顶层 body 的下边。此时子应用中 antd 的样式规则,由于开启了 shadowDom ,只对其下层的元素产生影响,自然就对全局 body 下的弹框不起作用了,造成了样式丢失的问题。

解决方案:调整 antd 入参,让其在当前位置渲染。

  1. 实验模式。

可通过 experimentalStyleIsolation:true 开启。 原理类似于 vue 的 scope-css,给子应用的所有样式规则增加一个特殊的属性选择器,限定其影响范围,达到样式隔离的目的。但由于需要在运行时替换子应用中所有的样式规则,所以目前性能较差,处于实验阶段。

3.1.1.3. JS 沙箱

确保子应用之间的“全局变量”不会产生冲突。

  1. 快照沙箱( snapshotSandbox 

  • 激活子应用时,对着当前 window 对象照一张相(所有属性 copy 到一个新对象 windowSnapshot 中保存起来)。
  • 离开子应用时,再对着 window 照一张相,对比离开时的 window 与激活时的 window (也就是 windowSnapshot )之间的差异。
    • 记录变更。Diff 出在这期间更改了哪些属性,记录在 modifyPropsMap 对象中。
    • 恢复环境。依靠 windowSnapshot 恢复之前的 window 环境。
  • 下次激活子应用时,从 modifyPropsMap 对象中恢复上一次的变更。
  1. 单例的代理沙箱 ( LegacySanbox 

与快照沙箱思路很相似,但它不用通过 Diff 前后 window 的方式去记录变更,而是通过 ES6的 Proxy 代理 window 属性的 set 操作来记录变更。由于不用反复遍历 window,所以性能要比快照沙箱好。

  1. 支持多例的代理沙箱( ProxySandbox 

以上两种沙箱机制,都只支持单例模式(同一页面只支持渲染单个子应用)。

原因是:它们都直接操作的是全局唯一的 window。此时机智的你肯定想到了,假如为每个子应用都分配一个独立的“虚拟window”,当子应用操作 window 时,其实是在各自的“虚拟 window”上操作,不就可以实现多实例共存了?事实上,qiankun 确实也是这样做的。

既然是“代理”沙箱,那“代理”在这的作用是什么呢?

主要是为了实现对全局对象属性 get、set 的两级查找,优先使用fakeWindow,特殊情况(set命中白名单或者get到原生属性)才会改变全局真实window。

如此,qiankun 就对子应用中全局变量的 get 、 set 都实现了管控与隔离。

3.1.2. 优势:

3.1.2.1. 具有先发优势

2019年开源,是国内最早流行起来的微前端框架,在蚂蚁内外都有丰富的应用,后期维护性是可预测的。

3.1.2.2. 开箱即用

虽然是基于国外的 single-spa 二次封装,但提供了更加开箱即用的 API,比如支持直接以 HTML 地址作为加载子应用的入口。

3.1.2.3. 对 umi 用户更加友好

有现成的插件 @umijs/plugin-qiankun 帮助降低子应用接入成本。

3.1.3. 劣势:

3.1.3.1. 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 解析运行会报下图这个错误。

报错的大意是, import 语法的代码必须放在 中执行。

官方目前推荐的解决方法是关闭沙箱... 但其实还有另一种比较取巧的方案:vite 生态里有一款专门兼容此问题的vite-plugin-qiankun 插件,它的原理是: eval 虽然没办法执行静态 import 语法,但它可以执行动态 import(...) 语法。

所以这款插件的解决方案就是替换子应用代码中的静态 import 为动态 import(),以绕过上述限制。

3.1.3.2. 子应用接入成本较高,详细步骤参考子应用接入文档

umi 用户可忽略这点,尤其是 @umi/max 用户,相比 webpack 接入成本要低很多。

3.1.3.3. JS 沙箱存在性能问题,且并不完善。

大致原因是 with + proxy 带来的性能损耗,详见 JS沙箱的困境 。当然 qiankun 官方也在针对性的进行优化,进展在这篇《改了 3 个字符,10倍的沙箱性能提升?!!》文章中可见一斑 。

3.2. 改良派 wujie

3.2.1. 原理:

wujie 是腾讯出品的一款微前端框架。作为改良派的代表,它认为: iframe 虽然问题很多,但仅把它作为一个 js 沙箱去用,表现还是很稳定的,毕竟是浏览器原生实现的,比自己实现 js 沙箱靠谱多了。至于 iframe 的弊端,可以针对性的去优化:

  • DOM 渲染无法突破 iframe 边界?(弹框不居中问题)

那 DOM 就不放 iframe 里渲染了,而是单独提取到一个 webComponent 里渲染,顺便用 shadowDOM 解决样式隔离的问题。

简单说,无界的方案就是:JS 放 iframe 里运行,DOM 放 webComponent 渲染

那么问题来了: 用 JS 操作 DOM 时,两者如何联系起来呢?毕竟 JS 默认操作的总是全局的 DOM。无界在此处用了一种比较 hack 的方式:代理子应用中所有的 DOM 操作,比如将 document 下的 getElementById、querySelector、querySelectorAll、head、body 等查询类 api 全部代理到 webComponent

下图是子应用真实运行时的例子:

至于多实例模式,就更容易理解了。给每个子应用都分配一套 iframe + webComponent 的组合,就可以实现相互之间的隔离了!

  • 刷新页面会导致子应用路由状态丢失?

通过重写 iframe 实例的history.pushState 和 history.replaceState,将子应用的 path 记录到主应用地址栏的 query 参数上,当刷新浏览器初始化 iframe 时,从地址栏读到子应用的 path 并使用 iframe 的 history.replaceState 进行同步。

简单理解就是:将子应用路径记录在地址栏参数中。

3.2.2. 优势:

3.2.2.1. 相比 qiankun 接入成本更低。

  • 父应用:

    • 与 iframe 的接入方式很类似,只需引入一个 React 组件渲染子应用即可。

  • 子应用理论上不需要做任何改造
3.2.2.2. vite 兼容性好

直接将完整的 ESM 标签块  插入 iframe 中,避免了 qiankun 使用 eval 执行 ESM 代码导致的报错问题。

3.2.2.3. iframe 沙箱隔离性好

3.2.3. 劣势:

3.2.3.1. 坑比较多

  • 明坑: 用于 JS 沙箱的 iframe 的 src 必须指向一个同域地址导致的问题。

    具体问题描述见下图:

此 [issue]() 至今无法在框架层面得到解决,属于 iframe 的原生限制。

手动的解决方案:

  • 主应用提供一个路径比如说 https://host/empty ,这个路径不需要返回任何内容,子应用设置 attr 为 {src:'https://host/empty'},这样 iframe 的 src 就是 https://host/empty。
  • 在主应用 template 的 head 插入这样的代码可以避免主应用代码污染。
  • 暗坑: 复杂的 iframe 到 webComponent 的代理机制,导致市面上大部分富文本编辑器都无法在无界中完好运行。所以有富文本的项目,尽量别用无界,除非你对富文本库的源码了如指掌。issues 在这里。

3.2.3.2. 长期维护性一般。

3.2.3.3. 内存开销较大

用于 js 沙箱的 iframe 是隐藏在主应用的 body 下面的,相当于是常驻内存,这可能会带来额外的内存开销。

3.3. 中间派 micro-app

3.3.1. 原理:

京东的大前端团队出品。

样式隔离方案与 qiankun 的实验方案类似,也是在运行时给子应用中所有的样式规则增加一个特殊标识来限定 css 作用范围。

子应用路由同步方案与 wujie 类似,也是通过劫持路由跳转方法,同步记录到 url 的 query 中,刷新时读取并恢复。

组件化的使用方式与 wujie 方案类似,这也是 micro-app 主打的宣传点。

最有意思的是它的沙箱方案,居然内置了两种沙箱:

  1. 类 qiankun 的 with 代理沙箱,据说相比 qiankun 性能高点,但目前微前端框架界并没有一个权威的基准性能测试依据,所以并无有效依据支撑。
  2. 类 wujie 的 iframe 沙箱,用于兼容 vite 场景。

开发者可以根据自身的实际情况自由选择。

整体感觉 micro-app 是一种偏“现实主义”的框架,它的特点就是取各家所长,最终成为了功能最丰富的微前端框架。

3.3.2. 优势:

3.3.2.1. 支持的功能最丰富。
3.3.2.2. 接入成本低。

3.3.2.3. 文档完善。

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

3.3.3. 劣势:

3.3.3.1. 功能丰富导致配置项与 api 太多。
3.3.3.2. 静态资源补全问题。

静态资源补全是基于父应用的,而非子应用这需要开发者自己手动解决。

4. 选型建议

统计时间2023.12.3npm周下载量star数issue数最近更新时间接入成本沙箱支持vite
qiankun22k15k362/155112天前
wujie1.3k3.4k280/27124天前
micro-app1.1k4.9k57/7481个月前
  1. 刚性建议。
  • vite 项目且对 js 沙箱有刚需,选 wujie 或者 micro-app。
  • 项目存在复杂的交互场景,比如有用到富文本编辑器库,选 wujie 前请做好充分的测试。
  • 如果你的团队对主、子应用的开发完全受控,即使有隔离性问题也可以通过治理来解决,那么可以试试更轻量的 single-SPA 方案。
  • 如果特别重视稳定性,那无疑是 iframe 最佳... 因为 iframe 存在的问题都是摆在明面的,市面上现有的微前端框架多多少少都有一些隐性问题。
  1. 综合推荐。

主要从接入成本、功能稳定性、长期维护性三方面来衡量:

image.png

  • 接入成本: wujie > microApp > qiankun (由低到高)
  • 功能稳定性:qiankun > microApp > wujie
  • 长期维护性:qiankun > microApp > wujie

看你的团队最看重哪一点,针对性去选择就好了,没有十全十美微前端框架,只有适合自己的。

最后

以上内容,确实会有我强烈的个人理解与观点,这也是我写文章一贯的风格。我并不喜欢那种客观且枯燥无味的文章,读完之后感觉像流水账,给不了读者任何的指导。我认为文章就是要有观点输出,技术文章也不例外,如果非常看重准确无误的表达,可以直接去看说明文档or源码,那应该是最权威的知识。如有错误或者误解,可以评论区或者私信指出,我积极改正。


作者:郑鱼咚
来源:juejin.cn/post/7309477710523269174

0 个评论

要回复文章请先登录注册