微前端
一、微前端
是一种类似于微服务的架构,它将微服务的理念应用于浏览器端。
微服务是面向服务架构(SOA)的一种变体,把应用程序设计成一系列松耦合的细粒度服务,并通过轻量级的通信协议组织起来具体地,将应用构建成一组小型服务。
这些服务都能够独立部署、独立扩展,每个服务都具有稳固的模块边界,甚至允许使用不同的编程语言来编写不同服务,也可以由不同的团队来管理。
1、将单页面前端应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。
2、各个前端应用还可以独立开发、独立部署。
3、可以在共享组件的同时进行并行开发——这些组件可以通过 NPM 或者 Git Tag、Git Submodule 来管理。
1、特点
- 代码库更小,更内聚、可维护性更高
- 松耦合、自治的团队可扩展性更好
- 渐进地升级、更新甚至重写部分前端功能成为了可能
- 独立部署
2、实现方案
2.1 实现的几种方式
-
路由分发
- 通过路由将不同的业务分发到不同的、独立前端应用上。
- 可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。
- 采用最多、最容易实现的 “微前端” 方案。
- 它适用于以下场景:
- 不同技术栈之间差异比较大,难以兼容、迁移、改造
- 项目不想花费大量的时间在这个系统的改造上
- 现有的系统在未来将会被取代
- 系统功能已经很完善,基本不会有新需求
-
iFrame
- 将另一个HTML页面嵌入到当前页面中。
-
应用微服务化
- 每个前端应用一个独立的服务化前端应用,并配套一套统一的应用管理和启动机制,诸如微前端框架 Single-SPA 或者 mooa
-
微件化
- 对构建系统的 hack,使不同的前端应用可以使用同一套依赖。它在应用微服务化的基本上,改进了重复加载依赖文件的问题。
-
微应用化
- (组合式集成),即通过软件工程的方式,在开发环境对单体应用进行拆分,在构建环境将应用组合在一起构建成一个应用。
-
纯 Web Components
-
结合 Web Components
2.2 思考
1. 多个 Bundle 如何集成?
2. 子应用之间怎样隔离影响?
3. 公共资源如何复用?
4. 子应用间怎样通信?
5. 如何测试?
2.2.1多 Bundle 集成
微前端架构中一般会有个容器应用(container application)将各子应用集成起来,职责如下:
- 渲染公共的页面元素,比如 header、footer
- 解决横切关注点(cross-cutting concerns),如身份验证和导航
- 整合到一个页面上,并控制微前端的渲染区域和时机
集成方式分为 3 类:
- 服务端集成:如 SSR 拼装模板
-
关键在于如何保证各部分模板(各个微前端)能够独立发布,必要的话,甚至可以在服务端也建立一套与前端相对应的结构。
-
每个子服务负责渲染并服务于对应的微前端,主服务向各个子服务发起请求
-
- 构建时集成:如 Code Splitting
- 常见的构建时集成方式是将子应用发布成独立的 npm 包,共同作为主应用的依赖项,构建生成一个供部署的 JS Bundle
- 然而,构建时集成最大的问题是会在发布阶段造成耦合,任何一个子应用有变更,都要整个重新编译,意味着对于产品局部的小改动也要发布一个新版本,因此,不推荐这种方式
- 运行时集成:如通过 iframe、JS(前端路由)、Web Components 等方式
- iframe 好像不太好(性能、通信成本等),但在这里确实是个合理选项,因为 iframe 无疑是最简单的方式,还天然支持样式隔离以及全局变量隔离,但这种原生的隔离性,意味着很难把应用的各个部分联系到一起,路由控制、历史栈管理、深度链接(deep-linking)、响应式布局等都变得异常复杂,因而限制了 iframe 方案的灵活性
- 前端路由,每个子应用暴露出渲染函数,主应用在启动时加载各个子应用的独立 Bundle,之后根据路由规则渲染相应的子应用。目前看来,是最灵活的方式
- Web Components,将每个子应用封装成自定义 HTML 元素(而不是前端路由方案中的渲染函数),以获得Shadow DOM带来的样式隔离等好处
2.2.2 影响隔离
子应用之间,以及子应用与主应用间的样式、作用域隔离是必须要考虑的问题,常见解决方案如下:
- 样式隔离:开发规范(如BEM)、CSS 预处理(如SASS)、模块定义(如CSS Module)、用 JS 来写(CSS-in-JS)、以及shadow DOM特性
- 作用域隔离:各种模块定义(如ES Module、AMD、Common Module、UMD)
2.2.3 资源复用
资源分为以下 3 类:
- 基础资源:完全不含逻辑功能的图标、标签、按钮等
- UI 组件:含有一定 UI 逻辑的搜索框(如自动完成)、表格(如排序、筛选、分页)等
- 业务组件:含有业务逻辑
注意:不建议跨子应用复用业务组件,因为会造成高度耦合,增加变更成本
对于公共资源的归属和管理:
- 公共资源归属于所有人,即没有明确归属
- 公共资源归集中管理,由专人负责
注意:所有人都能补充公共资源,但要有人(或一个团队)负责监管,以保证质量、一致性以及正确性
2.2.4 应用间通信
原理
- 使用发布订阅者模式:一方订阅,一方发布。
- 使用单例模式:一个工程内使用同一个实例。
- 微前端加载,首先加载父工程,随后根据配置加载对应的一个或者多个子工程。
- 父工程先一步生成实例,并传递给子工程。子工程则使用这个实例初始化自己的实例。 共享同一个实例。
通过自定义事件间接通信是一种避免直接耦合的常用方式,此外,React 的单向数据流模型也能让依赖关系更加明确,对应到微前端中,从容器应用向子应用传递数据与回调函数
无论采用哪种方式,都应该尽可能减少子应用间的通信,以避免大量弱依赖造成的强耦合
2.2.5 测试
- 集成测试:保证子应用间集成的正确性,比如跨子应用的交互操作
- 功能测试:保证页面组装的正确性
- 单元测试:保证底层业务逻辑和渲染逻辑的正确性
3、缺点
- 导致依赖项冗余,增加用户的流量负担
不同应用之间依赖的包存在很多重复,由于各应用独立开发、编译和发布,难免会存在重复依赖的情况。导致不同应用之间需要重复下载依赖,额外再增加了流量和服务端压力。 - 操作/管理上的复杂性
大幅提升的团队自治水平可能会让各个团队的工作愈加分裂。各团队只关注自己的业务或者平台功能,在面向用户的整体交付方面,会导致对用户需求和体现不敏感,和响应不及时。
4、应用场景
- 兼容遗留系统
- 原来的系统功能已经完善,并且稳定运行,系统重构任务量大。需要使用新框架,新技术去开发新的应用扩展功能。
- 应用聚合
- 提供很多应用和服务,如何为用户呈现具有统一用户体验的应用聚合成为必须解决的问题
- 团队间共享
- 不用应用之间往往存在很多可以共享和功能和服务。
- 局部/增量升级
- 一个大的产品由很多应用和服务组成,很多时候只需要对部分应用和服务进行升级。如果是单应用,升级耗时长,风险高,影响可服务性。
5、拆分
- 按照业务拆分
- 按照权限拆分
- 按照变更频率拆分
- 按照组织结构拆分
- 跟随后端微服务拆分