微前端

将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品

好处:

代码库更小,更内聚、可维护性更高
松耦合、自治的团队可扩展性更好
渐进地升级、更新甚至重写部分前端功能成为了可能
实现上,关键问题在于:

  • 多个 Bundle 如何集成?
  • 子应用之间怎样隔离影响?
  • 公共资源如何复用?
  • 子应用间怎样通信?
  • 如何测试?

多 Bundle 集成

微前端架构中一般会有个容器应用(container application)将各子应用集成起来,职责如下:

  • 渲染公共的页面元素,比如 header、footer
  • 解决横切关注点(cross-cutting concerns),如身份验证和导航
  • 将各个微前端整合到一个页面上,并控制微前端的渲染区域和时机

集成方式分为 3 类:

  • 服务端集成:如 SSR 拼装模板
  • 构建时集成:如 Code Splitting
  • 运行时集成:如通过 iframe、JS、Web Components 等方式

服务端集成

服务端集成的关键在于如何保证各部分模板(各个微前端)能够独立发布,必要的话,甚至可以在服务端也建立一套与前端相对应的结构:

image

每个子服务负责渲染并服务于对应的微前端,主服务向各个子服务发起请求

构建时集成

常见的构建时集成方式是将子应用发布成独立的 npm 包,共同作为主应用的依赖项,构建生成一个供部署的 JS Bundle

然而,构建时集成最大的问题是会在发布阶段造成耦合,任何一个子应用有变更,都要整个重新编译,意味着对于产品局部的小改动也要发布一个新版本,因此,不推荐这种方式

运行时集成

将集成时机从构建时推迟到运行时,就能避免发布阶段的耦合。常见的运行时集成方式有:

  • iframe
  • JS:比如前端路由
  • Web Components

虽然直觉上用 iframe 好像不太好(性能、通信成本等),但在这里确实是个合理选项,因为 iframe 无疑是最简单的方式,还天然支持样式隔离以及全局变量隔离

但这种原生的隔离性,意味着很难把应用的各个部分联系到一起,路由控制、历史栈管理、深度链接(deep-linking)、响应式布局等都变得异常复杂,因而限制了 iframe 方案的灵活性

另一种最常见的方式是前端路由,每个子应用暴露出渲染函数,主应用在启动时加载各个子应用的独立 Bundle,之后根据路由规则渲染相应的子应用。目前看来,是最灵活的方式

还有一种类似的方式是Web Components,将每个子应用封装成自定义 HTML 元素(而不是前端路由方案中的渲染函数),以获得Shadow DOM带来的样式隔离等好处

影响隔离

子应用之间,以及子应用与主应用间的样式、作用域隔离是必须要考虑的问题,常见解决方案如下:

  • 样式隔离:开发规范(如BEM)、CSS 预处理())、模块定义(如CSS Module)、用 JS 来写(CSS-in-JS)、以及shadow DOM特性
  • 作用域隔离:各种模块定义(如[ES Module])

资源复用

资源复用对于 UI 一致性和代码复用有重要意义,但并非所有的可复用资源(如组件)都必须在一开始就提出来复用,建议的做法是前期允许一定程度的冗余,各个 Bundle 在各自的代码库中创建组件,直到形成相对明确的组件 API 时再建立可供复用的公共组件

另一方面,资源分为以下 3 类:

  • 基础资源:完全不含逻辑功能的图标、标签、按钮等
  • UI 组件:含有一定 UI 逻辑的搜索框(如自动完成)、表格(如排序、筛选、分页)等
  • 业务组件:含有业务逻辑

其中,不建议跨子应用复用业务组件,因为会造成高度耦合,增加变更成本

对于公共资源的归属和管理,一般有两种模式:

  • 公共资源归属于所有人,即没有明确归属
  • 公共资源归集中管理,由专人负责

从实践经验来看,前者很容易衍变成没有明确规范,且背离技术愿景的大杂烩,而后者会造成资源创建和使用的脱节,比较推荐的模式是开源软件的管理模式

即,所有人都能补充公共资源,但要有人(或一个团队)负责监管,以保证质量、一致性以及正确性

应用间通信

通过自定义事件间接通信是一种避免直接耦合的常用方式,此外,React 的单向数据流模型也能让依赖关系更加明确,对应到微前端中,从容器应用向子应用传递数据与回调函数

另外,路由参数除了能用于分享、书签等场景外,也可以作为一种通信手段,并且具有诸多优势:

  • 其结构遵从定义明确的开放标准
  • 页面级共享,能够全局访问
  • 长度限制促使只传递必要的少量数据
  • 面向用户的,有助于依照领域建模
  • 声明式的,语义上更通用("this is where we are", rather than "please do this thing")
  • 迫使子应用之间间接通信,而不直接依赖对方

但原则上,无论采用哪种方式,都应该尽可能减少子应用间的通信,以避免大量弱依赖造成的强耦合

测试

每个子应用都应该有自己的全套测试方案,特殊之处在于,除单元测试、功能测试外,还要有集成测试

  • 集成测试:保证子应用间集成的正确性,比如跨子应用的交互操作
  • 功能测试:保证页面组装的正确性
  • 单元测试:保证底层业务逻辑和渲染逻辑的正确性

自下而上形成一个金字塔结构,每一层只需验证在其下层覆盖不到的部分即可

你可能感兴趣的:(微前端)