诞生背景
前后端协作
反观后端技术的发展趋势,从最初的前后端混合开发到前后端分离再到现在的微服务拆分。原本臃肿的后端服务在以垂直方向拆分之后变得清晰易维护。
微前端正是借鉴了微服务的架构理念,摒弃大型单体方式,将前端整体分解为小而简单的块,这些块可以独立开发、测试和部署,同时仍然聚合为一个产品出现在客户面前。可以理解微前端是一种将多个可独立交付的小型前端应用聚合为一个整体的架构风格。
简单来说:微前端是一种架构风格,将独立交付的前端应用程序组合成一个更大的整体。保证产品体验的同时提升开发体验。
目前较为流行的微前端架构的实现方案:在主应用中通过 loader 加载子应用,通过 router 判断子应用的加载时机,通过 store 来处理跨应用间的数据共享。在用户无感知的情况下将前端应用拆分为可独立运行、维护的多个子应用。
优势
- 不限技术栈:主框架不限制接入应用的技术栈,子应用可自主选择技术栈。
- 可独立开发部署:各个团队之间仓库独立,单独部署,互不依赖。
- 独立运行时:每个子应用之间状态隔离,运行时状态不共享。
- 增量升级:当一个应用庞大之后,技术升级或重构相当麻烦,而微应用具备渐进式升级的特性。
适用场景
- 兼容遗留系统:在遗留系统中新增功能时,可以微前端的形式单独创建一个子应用,自主选择技术栈摆脱历史系统的束缚。
- 应用聚合:聚合多个应用,提供统一入口页面。在保障用户体验的同时,赋能业务。
- 不同团队共同开发:跨团队合作时,摆脱技术栈的约束及部署冲突,高效协作。
实现方案
- MPA:将系统分为多个仓库维护,在首页聚合所有平台的入口或提供统一的导航组件,采用 MPA(Multi-page Application)多页应用模式。如下图所示应用的导航部分记录了所有子应用的入口地址,用户点击后会跳转到对应的子平台地址。公共导航部分需要抽离成组件给子平台使用。
微前端场景下的缺陷:
- 只能以页面维度拆分,无法拆分至区块部分。
- 用户在使用时体验割裂,会在不同平台间跳转,无法达到 SPA 应用带来的用户体验。
- 不同系统间不可以直接通信。
- 公共部分更新时,同一运维通知困难。
2. 服务端组合
在 webpack、react、vue 等框架大行其道前。我们常会使用 hbs、ejs 等页面模板框架来开发我们的前端页面。在服务端根据请求路径做静态模板拼接,来返回相应的页面结构。虽然微前端是一个较新的概念但是它的实现方式并非一定需要依赖新的技术。
3. 构建时组合
(a.) npm:通过将子应用打包为 npm 包,在主应用中作为依赖库引入。
微前端场景下的缺陷:
- 涉及子应用改造,需要打包为 npm 包成本较高。
- 升级维护麻烦,需要子应用更新包版本,主应用修改后重新发布。
- 子应用间公用依赖包重复引入
(b.) 模块联邦:模块联邦是一个 webpack5 提供的新特性。既可以做到打包发布模块供给后,消费者能够实时保持同步,也可以进行代码构建时候的优化。可以在一个应用中直接导出或使用另一个应用的模块。虽然 EMP 较好的解决了子应用动态更新的问题,但从实际微前端使用场景来说还需要考虑子应用间的相互影响,需要处理 JS 沙箱、CSS 隔离等问题。EMP 的方案更适用于微组件而非微应用的场景。
4. 运行时组合
(a.) iframe:在浏览器中组合应用程序的最简单的方法之一就是 iframe。从本质上讲,iframe 使从独立的子页面构建页面变得很容易。它们还在样式和全局变量不相互干扰方面提供了良好的隔离程度。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
微前端场景下的缺陷: - URL 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。如无法显示整页弹窗。
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 加载慢,每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
(b.) JS 加载子应用:通过 JS 加载子应用是最灵活的方法,也是目前最常采用的方法。每个子应用按约定暴露出相应的生命周期钩子,并且在加载后将其绑定到 window 对象下给主应用访问。然后主应用程序确定渲染哪个子应用,调用相关渲染函数传入渲染节点。
乾坤
基于 single-spa 实现路由与子应用的绑定关系根据路由加载相应应用。子应用将自己的信息注册到主应用中,包括入口文件地址、对应生效路由及命名空间等信息。同时子应用需暴露几个关键的生命周期钩子bootstrap、mount、unmount,以供主应用在适当的时机调用。相比而言乾坤是目前微前端框架中比较成熟且用户最多的框架了,基本解决了大部分微前端开发中会遇到的问题。但是也存在一些接入成本,需要对子应用做一些改造(umd 打包,暴露 hook 等)。
无界
基于 iframe 实现 js 沙箱,通过 WebComponent 处理 css 隔离。大致实现方式为:运行时动态加载子应用资源(加载方式在下文技术细节中会详细说明),在主应用中创建一个 shadowdom 节点和一个 iframe。将 js 注入 iframe 内运行,将 dom、css 放到 shadowdom 节点下。同时劫持 js 中的 dom 操作并指向 shadowdom。
在路由状态方面 通过劫持iframe的history.pushState和history.replaceState将子应用的url同步到主应用的query参数上,当刷新浏览器初始化iframe时,读回子应用的url并使用iframe的history.replaceState进行同步。
(c.) WebComponent:区别于上一种方式,WebComponent 的实现将子应用包裹为一个 HTML 自定义元素供容器实例化,而不是要求子应用暴露一些供容器调用的全局函数,同时借助于 shadowdom 的隔离能力可以有更好的样式隔离性。
(d. ) micro-app:micro-app借鉴了 WebComponent 的思想,通过 CustomElement 结合自定义的 ShadowDom,将微前端封装成一个类 WebComponent 组件,从而实现微前端的组件化渲染。并且由于自定义 ShadowDom 的隔离特性,micro-app不需要像single-spa和qiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置,接入成本较低。需要注意的是 shadowDOM 在 React 框架及一些 UI 库中的兼容不是很好,经常会出现一些不可预料的问题。如在 react16 及以下的版本中,在 shadowDOM 下绑定合成事件时会出现不触发的情况。实际在 micro-app 中默认也不会开启 shadowDOM 的能力。