跳到主要内容

微前端概述

背景

随着春夏秋冬的四季更替,宇宙万物的生命轮转,看着日渐庞大的项目代码,面对全新的客户需求,你的心有没有蠢蠢欲动,发出声声呐喊:“我想使用新的技术~”。面对日渐变慢的构建速度,看着肆意浪费的时间滴滴答答走过,你的内心有没有急不可耐:“什么 🐢 速度~”。面对公司项目整合,老板一个眼神的诉说:“把 A 项目(angular 编写)和 B 项目(react 编写)整合到一起吧~” 你有没有心中默默落泪:“这 XX 是个什么样的工作量~~”

相信很多小伙伴在工作中多多少少都有过类似的场景,微前端架构可以完美解决上述的所有问题。

本篇将为您介绍微前端究竟是什么、目前常见的微前端解决方案以及微前端框架选型。

微前端概述

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

什么是微前端

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。微前端架构与框架无关,每个微应用都可以使用不同的框架。

微前端的价值

1. 项目增量迁移

迁移是一项非常耗时且艰难的任务,比如有一个管理系统使用 AngularJS 开发维护已经有几年时间,但是随时间的推移和团队成员的变更,无论从开发成本还是用人需求上,AngularJS 已经不能满足要求,于是团队想要更新技术栈,想在其他框架中实现新的需求,但是现有项目怎么办?直接迁移是不可能的,在新的框架中完全重写也不太现实。

使用微前端架构就可以解决问题,在保留原有项目的同时,可以完全使用新的框架开发新的需求,然后再使用微前端架构将旧的项目和新的项目进行整合。这样既可以使产品得到更好的用户体验,也可以使团队成员在技术上得到进步,产品开发成本也降到的最低。

2. 技术选型灵活

使用微前端架构,主框架不限制接入应用的技术栈,微应用具备完全自主权。

3. 更快且独立的发布

在目前的单页应用架构中,使用组件构建用户界面,应用中的每个组件或功能开发完成或者 bug 修复完成后,每次都需要对整个产品重新进行构建和发布,任务耗时操作上也比较繁琐。

在使用了微前端架构后,可以将不能的功能模块拆分成独立的应用,此时功能模块就可以单独构建单独发布了,构建时间也会变得非常快,应用发布后不需要更改其他内容应用就会自动更新,这意味着你可以进行频繁的构建发布操作了。

4. 允许单个团队做出技术决策

因为微前端构架与框架无关,当一个应用由多个团队进行开发时,每个团队都可以使用自己擅长的技术栈进行开发,也就是它允许适当的让团队决策使用哪种技术,从而使团队协作变得不再僵硬。

微前端使用场景

  1. 拆分巨型应用,使应用变得更加可维护
  2. 兼容历史应用,实现增量开发

微前端如何实现

1. 多个微应用如何进行组合 ?

在微前端架构中,除了存在多个微应用以外,还存在一个容器应用,每个微应用都需要被注册到容器应用中。 微前端中的每个应用在浏览器中都是一个独立的 JavaScript 模块,通过模块化的方式被容器应用启动和运行。 使用模块化的方式运行应用可以防止不同的微应用在同时运行时发生冲突。

2. 在微应用中如何实现路由 ?

在微前端架构中,当路由发生变化时,容器应用首先会拦截路由的变化,根据路由匹配微前端应用,当匹配到微应用以后,再启动微应用路由,匹配具体的页面组件。

3. 微应用与微应用之间如何实现状态共享 ?

在微应用中可以通过发布订阅模式实现状态共享,比如使用 RxJS。

4. 微应用与微应用之间如何实现框架和库的共享?

可以通过 import-map 和 webpack 中的 externals 属性。

在微应用架构中我们要使用模块化的方式去加载应用。 在最新的模块化当中新增了 import-map 特性,允许我们加载网络模块,而不是一定要将模块下载到本地,我们只需要配置好模块名字和对应的模块地址即可,这样每个微应用在引用公共模块时都可以引用提前配置好的公共模块了。

我们还需要修改微应用本身的 webpack 配置,通过 webpack 的 externals 属性告诉 webpack 在打包应用时哪些模块是不需要被打包的。

常见的微前端解决方案

路由分发式

在还没有提出微前端概念时,就有很多公司通过反向代理服务器来编排公司不同业务线的前端应用,该模式支持将不同的前端项目部署在 N 个服务器上,每个服务器的前端项目可支持单独访问和代理访问。这样部署的好处是最终可以通过反向代理,将不同的前端项目通过代理服务器的路由合并到统一的域名和端口下,最终实现在用户眼里该平台仅仅是一个应用,而子应用间又可以通过单点登录的方式来共享登陆状态。

路由分发模式的落地实现,如图所示。

相信该架构是目前互联网体系内使用最多的微前端雏形架构,大量的公司通过这种设计方式将复杂的应用系统拆分成多个应用,在多个应用间通过服务端技术来实现登录状态的共享和通用数据的共享,这种方式实现的应用拆分和编排很好的解决了企业遗留系统的微前端迭代。

当然,这种方式也并不是微前端架构理想的解决方案,因为该路由系统必须借助反向代理服务器(如 Nginx),路由编排是通过反向代理实现的。而这种方式看似将应用融入了一个平台,但实际上应用间仍然是独立存在的,所以与服务端的微服务不尽相同。若用户从 应用 A 跳转到 应用 B 的界面时,仍然需要将视图重新加载。基于反向代理的路由编排实现的微前端项目存在一个本质上的弊端,即子应用无法在同一个窗口中同时存在,切换到新的路由时整个窗口都会发生变化。

优点: 简单、快速、易配置 缺点: 在切换应用时会触发浏览器刷新,影响体验

iframe + 自定义消息传递

iframe 做为内嵌页组件存在于 Web 项目的历史相当悠久,它可以实现在一个网页中通过 URL 路径加载另一个网页,两个页面间的 CSS 和 JS 是天然隔离的,iframe 容器还提供了 postMessage() 等 API 帮助开发者在不同的内嵌页面实现互相通信,iframe 就像一个天然的 HTML 沙箱容器,为微前端提供了入口。所以结合路由编排和 iframe 容器便可以将目前最早期的前端项目轻易的微前端化,如图所示:

iframe 容器解决了单独路由系统无法在相同页面中加载 N 个项目的问题,但其自身也存在一定的弊端:

  1. 网站 SEO 的优化问题 SEO 问题的体现于静态 SPA 的问题相同,基于 iframe 容器加载的应用并不能在网页源代码中直接展示子应用的源代码而导致首页的关键字无法被爬虫机器人获取。若为自应用单独定义 SEO 并对外暴露 URL 地址,则会导致爬虫机器人获取的快照信息直接指向子应用,这种情况在搜索引擎中打开的视图就是https://ip:443/app2内部的视图,并没有外部容器了。
  2. 保存子应用状态的问题 iframe 除存在 SEO 问题外,在状态保存上也存在比较重大的难题,iframe 与 SPA 的 Router 架构不同,由于 iframe 采用 HTTP 请求加载数据,一旦页面加载也会触发 iframe 的加载,这就导致了当视图切换到新页面再跳转回原页面时,iframe 默认无法记录其内部上一次访问的应用地址,导致重新回到默认的 src 位置。这种情况虽然可以通过使用 JavaScript 制造缓存的方式解决,但依然避免不了 iframe 内部每次重新加载的问题,若子应用内部存在跳转参数,做状态保存更加困难。

优点:

  1. 实现简单: 子应用之间自带沙箱,天然隔离,互不影响
  2. 技术不限制: 可以各自使用完全不同的前端框架;
  3. 消息传递: 只要每个 iframe 来自同一个来源,可以使用 Window.postMessageAPI 来进行消息传递;

缺点:

  1. 无 SEO: 众所周知 iframe 对 SEO 是毁灭性的打击,单这一条即可否定该方案了;
  2. URL 状态不同步: iframe 的页面 url 中的状态信息并不能同步到父窗口,无法使用浏览器的前进后退功能;
  3. DOM 结构不共享: iframe 的页面布局只针对于 iframe 窗口(例如:全局弹框无法给出合理布局);
  4. 全局上下文完全隔离,内存变量不共享: iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的微应用中实现免登效果;
  5. : 每次微应用进入都是一次浏览器上下文重建、资源重新加载的过程;

Web Components

Web Components 是一个 Web 标准,像 Angular、React/Preact、Vue 或 Hyperapp 这样的主流 JavaScript 框架都支持它们; 你可以将 Web Components 视为使用开放 Web 技术创建的可重用的用户界面小部件。

优点:

  1. 拥有自己独立的 ScriptsStyles,以及对应的用于单独部署子应用组件的域名;
  2. 代码的可读性变得非常清晰,组件资源内部高内聚,组件资源由自身加载控制;

缺点:

  1. 浏览器和框架的支持不够: 需要更多的 polyfills 从而影响到用户页面的加载体验;
  2. 重写现有的前端应用: 我们需要在整个前端应用上把它们全部转换成 Web Components;
  3. 系统架构复杂: 当应用被拆分为一个又一个的组件时,组件间的通讯就成了一个特别大的麻烦

Module Federation 模块联邦

webpack5 新增的 Module Federation(模块联邦)功能,他可以帮助将多个独立的构建组成一个应用程序,不同的构建可以独立的开发与部署,利用模块联邦我们可以在一定程度上去实现微前端。

优点:

  1. 开箱即用:只需要执行几行命令即可拉取相应的模板代码并把项目跑起来,包括基座应用和微前端应用,无需处理构建工具的复杂配置;
  2. 独立开发与部署:基于提供的代理工具,微应用开发者在单独开发微应用时,无需启动基座或者其它微应用;
  3. 去中心化: 因为采用的是模块共享,所以不用使用中心基座的概念;
  4. 组件共享: 与 npm 发包类似的组件共享管理;

缺点:

  1. 无法沙箱隔离: 需借助其它工具和框架才能做到应用层面的隔离;
  2. 技术单一: 仅限使用 webpack5 版本以上;
  3. 代码封闭性高: 依旧需要做 npm 那一套管理和额外的拉取代码,还不如直接 npm 复用方便;
  4. 拆分粒度需要权衡: 共享的 lib 无法做到 tree-shaking;
  5. 依赖前置: 导致时间加载变长;

组合式应用路由分发(中心基座方案)

前端微服务化架构是下一代微前端解决方案的思想核心,该思想来自于后端的微服务思想,基于“注册中心”的应用自治环境。前端微服务化指的是前端应用中存在一个类似“注册中心”的服务,该服务可以管理应用内的所有子应用的完整生命周期,并与子应用建立通信。通过模块化的加载方式将子应用与主应用组合,最终形成一个整体的应用。

微前端化的思想模式类似于将 Nginx 反向代理和 iframe 整合后的设计思路,其应用加载并不通过反向代理服务器而是通过基座应用进行应用和应用对应的路由进行注册,在基座应用上注册路由的应用可以看作是组件,所以应用基座可以看作是反向代理服务器。通过之前的学习会了解到通过路由分发的模式,一个页面中只能加载一个子应用,而前端微服务化之所以可以将多个子应用,是由于其路由策略类似于 MVVM 框架的路由系统,注册路由的子应用可以在基座应用的指定路由容器组件中加载,其加载方式并不是通过 iframe 的方式,而是通过类似 AJAX 访问数据的形式,将子应用的整体代码加载到网页中,这样可以完美的规避 iframe 无法保存视图状态的问题。

当访问指定子应用所对应的路由时,基座应用会加载、运行对应的应用。而原有的一个或多个应用,仍然可以在页面上保持运行的状态。这些子应用的状态完全交给基座应用管理,子应用间可以通过基座应用进行通信,子应用可以使用不同的技术栈来进行实现,也可以单独部署到任何服务器。

该方案的核心是 主从 思想,即包括一个基座(MainApp)应用若干个微(MicroApp)应用; 基座应用大多数是一个前端 SPA 项目,主要负责 应用注册,路由映射,消息下发 等,而微应用是独立前端项目,这些项目不限于采用 React,Vue,Angular 或者 JQuery 开发,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问。

优点:

  1. 技术不限制: 可以各自使用完全不同的前端框架;
  2. 无感切换: 因为是一个 SPA 项目,所以体验极佳;
  3. 利于 SEO
  4. 独立开发与部署
  5. 微前端优势几乎都有

缺点:

  1. 沙箱不隔离: 也就是 js 与 css 样式会出现冲突的问题

小结一下

解决方案优点缺点
服务器路由重定向(通过 Nginx 配置代理映射到不同的子模板应用上)简单、快速、易配置在切换应用时会触发浏览器刷新,影响体验
iframe(iframe + 自定义消息传递)1. 实现简单 2. 技术不限制 3. 消息传递1. Bundle 的大小各异 2. 无 SEO 3. URL 状态不同步 4. DOM 结构不共享 5. 全局上下文完全隔离,内存变量不共享 6. 速度慢
Web Components1. 拥有自己独立的 Scripts 和 Styles,以及对应的用于单独部署子应用组件的域名; 2. 代码的可读性变得非常清晰,组件资源内部高内聚,组件资源由自身加载控制;1. 浏览器和框架的支持不够 2. 重写现有的前端应用 3. 系统架构复杂
Module Federation1. 开箱即用 2. 独立开发与部署 3. 去中心化 4. 组件共享1. 无法沙箱隔离 2. 技术单一 3. 代码封闭性高 4. 拆分粒度需要权衡 5. 依赖前置
组合式应用路由分发1. 技术不限制 2. 无感切换 3. 利于 SEO 4. 独立开发与部署 5. 微前端优势几乎都有沙箱不隔离

微前端框架选型

qiankun

qiankun 是基于 single-spa 的微前端方案

特点

  • html entry 的方式引入子应用,相比 js entry 极大的降低了应用改造的成本
  • 完备的沙箱方案
    • js 沙箱做了 SnapshotSandbox、LegacySandbox、ProxySandbox 三套渐进增强方案
    • css 沙箱做了 strictStyleIsolation、experimentalStyleIsolation 两套适用不同场景的方案
  • 做了静态资源预加载能力
  • !适配成本比较高,工程化、生命周期、静态资源路径、路由等都要做一系列的适配工作
  • !css 沙箱采用严格隔离会有各种问题,js 沙箱在某些场景下执行性能下降严重
  • !无法同时激活多个子应用
  • !无法支持 vite 等 esmodule 脚本运行

成本

接入成本

  1. 子应用需要接入生命周期代码;
  2. 主应用需要接入注册微应用代码;

microApp

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

特点

  • 使用 webcomponet 加载子应用相比 single-spa 这种注册监听方案更加优雅
  • 基于 CustomElement 和样式隔离、js 隔离来实现微应用的加载,所以子应用无需改动就可以接入
  • 支持应用隔离
  • 通过劫持底层接口实现了元素隔离
  • 降低子应用改造的成本,提供静态资源预加载能力
  • 组件式的 api 更加符合使用习惯
  • !支持 vite 运行,但必须使用 plugin 改造子应用,且 js 代码没办法做沙箱隔离

成本

接入成本

子应用无需改动,主应用需要接入微应用代码

EMP

EMP 是基于 webpack 5 module federation 的微前端方案

特点

  • webpack 联邦编译可以保证所有子应用依赖解耦
  • 应用间去中心化的调用、共享模块
  • 模块远程 ts 支持
  • !对 webpack 强依赖,老旧项目不友好
  • !没有有效的 css 沙箱和 js 沙箱,需要靠用户自觉
  • !主、子应用的路由可能发生冲突

wujie

无界 是基于 webcomponent 容器 + iframe 沙箱 的微前端方案

特点

  • webcomponet 部分的有点同 microApp
  • 适配 vite 更加简便

总结

方案选择

优先考虑 qiankun、microApp、wujie,加载微应用的方式都是通过 http 的方式,可以实现主应用和微应用之间的解耦

接入成本

优先考虑 microApp、wujie,接入成本更低

兼容性

优先考虑 microApp、wujie,因为 qiankun 改造成本相对来说更大,并且不支持viteESM 脚本运行

性能

优先考虑 microApp,microApp 推出最新版本MicroApp V1.0.0-rc后,性能已接近原生

❤️ 🤔 基于以上四点考虑,综合推荐使用 microApp