04.探索微前端架构
该笔记学习自书籍《微前端设计与实现》--卢卡.梅扎利拉
微前端决策框架的应用
你首先需要做出的决策就是选择横向拆分还是纵向拆分
纵向拆分简介
当项目需要持续一致的用户界面变化和跨页面的流畅的用户体验时,纵向拆分就显得非常有用。
但目前纵向拆分的客户端组合都在 App shell 内加载、卸载微前端,并且只应用这一种组合方式。微前端和 App shell 之间的关系是一对一的,也就是说,App shell 一次只加载一个微前端。当然也有例外,你可以使用客户端路由来集成多个微前端,即在 App shell 中利用一个全局路由来加载不同的微前端。
为了实现一个采用纵向拆分的微前端架构,App shell 加载 HTML 文件或者 JavaScript 文件作为应用的入口,不应该与其他的微前端共享任何业务逻辑,且不限制微前端使用的技术栈。为了满足未来的系统演进,不应该使用特定的前端框架实现 App shell。如果要自己实现,则使用原生 JavaScript 是一个不错的解决方案。
App shell 在用户会话中始终存在,因为它对应用起着协调作用。同时,通过暴露生命周期的 API,它让运行在其中的模块在被加载或卸载时能够做出相应的反应。
为了实现一个采用纵向拆分的微前端架构,App shell 加载 HTML 文件或者 JavaScript 文件作为应用的入口,不应该与其他的微前端共享任何业务逻辑,且不限制微前端使用的技术栈。为了满足未来的系统演进,不应该使用特定的前端框架实现 App shell。如果要自己实现,则使用原生 JavaScript 是一个不错的解决方案。
App shell 在用户会话中始终存在,因为它对应用起着协调作用。同时,通过暴露生命周期的 API,它让运行在其中的模块在被加载或卸载时能够做出相应的反应。
横向拆分简介
横向拆分在以下几种情况出现时效果较好:
- 当子业务在多个视图中渲染时,子业务的复用性成为项目的关键;
- 当 SEO 是项目的必选项,需要使用服务器端渲染时;
- 当前端项目需要至少几十人协同开发,不得不把项目拆分成多个子域时;
- 当面临一个定制化的多租户(multi-tenant)项目时。
如果团队对前端系统比较熟悉,或者项目更容易受到大流量和高峰值的影响,那么客户端组合是不错的选择,因为可以利用 CDN 缓存微前端以应对扩容的挑战。
当项目偏静态内容且流量大时,为了让 CDN 提供商替你的基础设施承担风险,可以选择边缘侧方案。
服务器端组合是在线服务最好的选择,对资讯网站和电商网站这种被高度索引的网站非常适用。举例来说,PayPal、美国运通等对性能指标要求高的站点采用了服务器端组合方式。
再看一下边缘侧组合。边缘侧组合的每个视图对应一个 HTML 页面。每次用户加载一个新的页面,CDN 都会新建这个页面,它将获取多个微前端来创建最终的视图。而通过服务器端路由,应用服务器就会知道哪个 HTML 模板与特定的路由相关联。路由操作和微前端组合发生在服务器端。
架构分析
- 完美的架构并不存在,它是一种权衡的艺术。这种权衡不仅是技术层面的思考,而且要基于业务需求和组织结构进行深入分析。先进的架构会同时考虑技术和其他影响最终产出的因素。
- 永远不要追求最好的架构,而要追求最不糟糕的架构
- 在决定架构之前,一定要花时间充分理解运行环境、组织架构和团队间沟通机制。
纵向拆分的架构
App shell
作为微前端应用的一个持久化的部分,App shell 是当一个微前端应用被访问时最先下载的内容。从用户打开应用到用户离开为止,App shell 一直存在,它会根据用户请求的不同资源去加载和卸载对应的微前端。使用 App shell 去加载微前端时主要有以下几点需要考虑:
- 处理用户初始状态(如果有的话)
- 获取全局配置
- 获取用户路由并加载对应的微前端
- 处理日志、可观察性或与营销相关的库
- 处理微前端加载失败时的错误
永远不要把 App shell 当作一个交互层去使用,比如在用户会话期间不断地让 App shell 与微前端进行通信。App shell 只应该用于处理特殊情况或者用在应用初始化时。如果把它作为微前端之间的共享层,有可能在微前端和 App shell 之间产生逻辑上的耦合,迫使重新测试或重新部署应用中的所有微前端。这种情况也被称为分布式单体(distributed monolith),它是开发人员最糟糕的噩梦。
在这种模式下,App shell 一次只加载一个微前端。这意味着你不需要创建一个机制来封装微前端之间相互冲突的依赖关系,因为微前端之间的库文件或 CSS 样式不会有任何冲突(图 4-4),只需要在卸载微前端时将两者从 window 对象中删除。
App shell 由一个简单的 HTML 页面和一个包含其逻辑的 JavaScript 文件组成。为了提升用户第一次加载时的体验,App shell 也有可能包含一些 CSS 样式,比如显示一个像 spinner 这样的加载动画。
一个 JavaScript 文件也可以作为微前端的入口被加载,但在这种情况下,用户等待页面加载的体验并不友好,因为必须等到 JavaScript 文件被解析后,它才能将新的元素添加到 DOM 中。
当我们想打造一致的用户体验,同时想让团队享有完整的控制权时,纵向拆分的方法很好用。
挑战
共享状态
当信息比较敏感时,可以给应用加上一些安全检查措施以确保 Web Storage 有足够的空间来存放信息
组合微前端
这使得我们受限于浏览器所提供的标准来做事。有四种技术方案可以在客户端组合微前端:
- ES Module
- SystemJS
- Module Federation
- HTML 解析
对于 HTML 解析:
- 我们可以将微前端也看作一个 XML 文档,并使用 DOMParser 对象将相关节点添加到 App shell 的 DOM 中。在解析微前端的 DOM 后,我们再使用 adoptNode 方法或 cloneNode 方法添加 DOM 节点。
- 浏览器不会解析通过这种方法添加的 script 元素。在这种情况下,我们需要创建一个新的 script 元素,用于传递在微前端的 HTML 页面中声明的脚本源文件。创建一个新的 script 元素将触发浏览器解析与该元素相关的 JavaScript 文件。
- 这种技术被一些框架所使用,比如 qiankun,它允许使用 HTML 文件作为微前端的入口。
多框架方法
虽然从技术上讲,你可以在单页应用中使用多个 UI 框架,但它会产生性能问题和潜在的依赖冲突。对于微前端也是同理,所以不建议在纵向拆分的架构中使用多框架实现。
相反,要遵循开发的最佳实践,比如尽可能地减少外部依赖,只从第三方库中导入需要使用的函数和组件,而不是把整个包全部导入,因为这可能会增加最终的 JavaScript 代码包的大小。许多 JavaScript 代码工具实现了 Tree Shaking[插图],以帮助减少代码包的最终大小。
在一些情况下,在微前端中使用多框架方法利大于弊
记住,我们的目标是构建微前端的基础,以便充满信心地快速推进开发工作,减少潜在的错误,将能自动化的工作尽可能地自动化,并在整个团队中培养正确的思维模式。
架构演变和代码封装
一个老用户在访问这个微前端时,可能是想到认证区域进行登录或找回账户的电子邮箱或密码,而一个新用户则有可能想注册或进行支付。那么,这个微前端的一个自然分割逻辑可以是拆分为一个用于认证的微前端和另一个用于注册的微前端。这样就可以根据业务逻辑将二者分开,而不会让用户下载不必要的代码。
当库甚至逻辑被用于多个业务领域中时,比如在表单验证库中,你有以下几个选择:
- 复制代码:代码重复并不一定总是一个坏做法,比如这个组件在某个业务领域中经常发生变更。
- 将代码抽离到一个共享库中:你确实想将业务逻辑集中到某个库或者组件中,以确保每个微前端都使用相同的实现,比如集成支付能力。
- 委托给后端 API:第三种选择是将公共功能委托给后端处理,由后端为所有纵向拆分的微前端提供某些配置文件和业务逻辑的实现。具有特定校验规则的输入字段
- 如果不能正确地抽象,请选择复制代码
实现一个设计系统
在考虑应用于微前端的设计系统时,想象一个由设计 token、基础组件、用户界面库和集成这些部分的微前端所组成的分层系统
- 第一层是设计 token,可以使用一些基础值为产品创建样式,比如字体、文本颜色、文字大小以及最终用户界面中的许多其他特征。通常,设计 token 被放在 JSON 文件或 YAML 文件中,token 代表了设计系统的每个细节。
- 下一层是基础组件。通常情况下,这些组件不具有业务逻辑。它们应该是尽可能通用的
- 第三层是 UI 组件,通常由基础组件组成,其中包含一些在特定领域内可重复使用的业务逻辑。我们可能也想共享这些组件,但在这样做时要谨慎。
- 最后一层是承载 UI 组件库的微前端。请牢记微前端独立性的重要意义。当有着超过三四个外部依赖项的时候,我们就会踩到分布式单体的坑,为了分布式而最终受制于分布式。这是最糟糕的问题,组织中的团队将为此丧失独立性。
你需要一个可靠的管理方法来维护初始投入。
开发体验
略
搜索引擎优化
- 第一个策略是以容易被爬虫索引的方式来优化应用代码。
- 第二个策略是创建一个对爬虫来说有意义的 HTML 标签
性能与微前端
微前端架构的性能是 Web 应用成功的关键。一个纵向拆分的架构可以实现良好的性能,这要归功于业务域的正确拆分,以及因此带来的客户端代码的拆分。
然而,在一个纵向拆分的架构中,未认证用户如果想在着陆页上看到商业声明,可以只下载该微前端的代码,而认证用户只需下载认证区域的代码。我们经常没有意识到用户的行为与应用的执行方式是不同的,因为我们常常把应用的性能作为一个整体来优化,而不是基于用户与网站的交互方式来优化。根据用户体验来优化网站会有更好的结果。
可用的框架
两个完全适配这种架构的框架是 single-spa 和 qiankun。single-spa 背后的概念非常简单:它是一个轻量级的库,为以下内容提供了无差别的重载。
用例
当前端开发人员有单页应用开发经验时,纵向拆分架构是一个不错的解决方案。它也会在一定程度上扩大规模,但如果有几百名前端开发人员在同一个前端应用上工作,那么横向拆分可能更适合你的项目,因为它可以进一步将应用模块化。
当想保持 UI 和 UX(user experience,用户体验)的一致性时,纵向拆分的架构也很好。如果你是微前端新手,我推荐你首选纵向拆分的架构,
横向拆分的架构
横向拆分的架构为大多数微前端应用的不同需求提供了多种选择。这些架构之所以能够细粒度模块化,是因为多个团队可以对任一视图进行分工协作,从而可以复用不同团队产出的不同微前端模块来组合页面。
依赖可靠的管理手段和定期的审查,以保持微前端之间的边界清晰
对于横向拆分架构,最重要的是要充分了解沟通的流程和团队的结构,以便开发人员能够完成开发工作,避免太多跨团队的外部依赖。
组件与微前端区别:
一个很好的经验法则是对于组件,更倾向于为不同的用例进行扩展,暴露多个属性以覆盖不同场景。相反,对于微前端,我们对业务逻辑进行封装,允许微前端之间通过事件进行通信。
客户端组合
假设你在打造一个视频网站,你决定使用客户端组合的横向拆分架构,该项目有多个团队参与。为了简化问题,这里只考虑两个视图:着陆页和目录页。这两个视图的开发工作涉及以下团队。
- 基础团队:基础团队负责 App shell 和设计系统,与用户体验团队一起工作,但大部分工作涉及技术层面。
- 着陆页团队:着陆页团队负责支持营销团队推广流媒体服务,并根据需求创建不同的着陆页。
- 目录团队:目录团队负责用户可以进行视频点播的认证区域。目录团队与其他团队合作,为用户提供良好的体验。
- 播放体验团队:考虑到打造适合多个平台且性能良好的视频播放器的复杂性,公司决定组建一个专门负责播放体验的团队。该团队负责视频播放器、视频分析、数字版权管理(digital rights management,DRM)的实施,以及关注并处理未经授权的用户通过非法手段观看视频等安全问题。
横向拆分架构的真正价值到底在哪里呢?假设现在是视频流媒体平台发布上线几个月之后,产品团队提出需求,让用户即使没有登录也可以使用我们的平台。这样做的目的是提高平台曝光量,为潜在客户提供节目预览入口。换言之,需要我们提供一个拥有目录功能,但没有播放器区域的界面。产品团队还希望在这个着陆页上呈现更多的信息,以方便用户充分考虑是否订阅平台服务。在这种情况下,需要基础团队、目录团队和着陆页团队一起合作来实现这一需求。
由于技术和协作方面的原因,Web 应用的迭代绝非易事。这种方法可以组合微前端,并在同一个视图中将其进行拼接。多个团队可高效协作,互不干扰,让团队成员拥有良好的开发体验,同时,业务可快速且自由迭代。
挑战
微前端通信
我们使用编排(choreography)模式,它使用异步通信,即事件代理(event broker),来通知所有对特定事件感兴趣的消费者。通过这种方法,可以获得以下好处。
- 独立的微服务,可以对一个或多个生产者触发的外部事件做出响应(或不做出响应)。
- 稳固、有边界的上下文,它们不会泄漏到多个服务中去。
- 减少了跨团队协调的沟通成本。
- 提高了每个团队的敏捷能力,使他们能够根据客户的需求开发自己的微服务。
开发人员不应该使用共享状态,而应该积极维护微前端之间的边界,并使用异步消息传递应该在视图上共享的事件
由于这种方法的松散耦合性质,微前端 D 只需要监听微前端 A 发出的事件。通过这种方式,所有的微前端和开发团队都能保持独立性
CSS 类的冲突和避免方法
在实施过程中,横向拆分架构的一个潜在问题是 CSS 类的冲突。当多个团队开发同一个应用时,很有可能出现重复的类名,这将破坏最终的应用布局。为了避免这种风险,我们可以为每个微前端的每个类名都加上前缀并规范化,以防止重复的名字出现
多框架方法
有很多方法可以解决这个问题。比如,利用 iframe 创建一个沙箱(sandbox),这样一来,在一个 iframe 内加载的内容就不会与另一个 iframe 发生冲突。
认证
当涉及系统认证时,横向拆分的架构提出了一个有趣的挑战,因为大多数的时候会有多个团队在同一个视图上工作,他们需要为客户提供独特的体验。当用户进入一个 Web 应用的认证区域时,组成该页面的所有微前端都必须与各自的认证 API 网关进行通信。
不同的微前端如何安全地拿到和存储同一个认证 token,而不需要多次往返于后端?最佳方案是将 token 存储在 LocalStorage、SessionStorage 或 cookie 中。在这种情况下,所有的微前端都将从事先约定的 Web Storage 中读取同一个 token。
如果我们使用 LocalStorage 或 SessionStorage 进行存储,那么所有的微前端域名都必须在同一个子域中,否则就不能访问存储 token 的 LocalStorage 或 SessionStorage。如果 token 存储在 cookie 中,那么微前端的域名可以是不同的多个子域,但必须在同一个域中。
当有多个微前端以相同的请求体请求相同的 API 时,还必须考虑这些微前端以后很可能被合并成一个微前端。
微前端重构
横向拆分的架构的另一个好处是,当代码变得过于复杂,无法由一个团队来管理,或者一个新团队开始负责没有开发过的微前端时,我们有能力去重构特定的微前端。虽然也可以在纵向拆分的情况下这样做,但横向拆分的微前端需要维护的逻辑要少得多。这使得重构相对容易,特别是对于那些在同一平台上工作多年的企业或组织。
搜索引擎优化
开发体验
通过有效沟通来维持可控性
用例
它包含了不同状态,这些状态根据视图类型和支付方式的不同而不同。支付微前端存在于用户执行支付操作的每一个视图中,包括着陆页、产品详情页,甚至是另一个产品的付款页面。在这类应用中,类似的用户体验载体会被复制到若干系统视图中。
对于企业级应用,经常要处理包含各种数据的概览页(dashboard)。出于不同的目的,比如财务需要和监控目的等,我们希望将这些数据收集到不同的视图中。
他们通过创建一个新的微前端来处理特定顾客的需求,并将其部署给租户。
Module Federation
Module Federation 真正让人眼前一亮的优点是它在暴露不同的微前端,甚至是设计系统等共享库时所展现的简单性,助你轻松地完成异步集成,让你感受到无比顺畅的开发体验。这就如同在处理一个单体代码库,你可以导入远程微前端,并以需要的方式组成一个视图。
假设所有的微前端都在使用 Vue.js 3.0.0。有了 Module Federation,只需要指定 Vue@3 为一个共享库。在编译时,webpack 将为所有使用它的微前端输出同一个 Vue 版本。在同一个项目中使用不同版本的 Vue,该怎么办呢? Module Federation 将把不同版本的 Vue 包裹在不同的作用域中,以避免运行时可能发生的冲突,或者甚至可以使用 Module Federation API 为同一个库的不同版本指定作用域。
iframe
iframe 可能不是微前端的首选方案,但是它的优势在于微前端之间自带沙箱,天然隔离,这是其他解决方案所不具备的特征。
iframe 允许我们对粒度进行控制。为 sandbox 设置一些属性值,可以打开 sandbox 的属性限制,如使用 allow-forms 或 allow-scripts 来分别允许提交表单或执行 JavaScript 脚本
可以用 postMessage 方法与被嵌入的父页面进行通信
用 iframe 实现微前端的这种方式主要应用于桌面应用和 B2B 应用。但是要注意,这种方式非常不适合 ToC 的网站,因为 iframe 会大大降低页面性能。特别是,在一个页面中引入多个 iframe 将占用大量 CPU 资源。
关于 iframe 的最佳实践与弊端
- 应尽量避免微前端之间有过多的交互。过多的交互会让代码变得复杂,难以维护。如果要在微前端间共享大量信息,就不应该选择 iframe。
- 在响应式网站中使用 iframe 是一种挑战,因为使用 iframe 处理流式布局 [插图] 及其内容会相当复杂。尽可能坚持使用固定尺寸。
- 当需要在 Web Storage 或 cookie 中存储数据时,请尽量存储在 App shell 中,这样可以避免跨多个 iframe 读取数据。
- 在 iframe 和主页面之间使用发布 / 订阅模式通信时,需要在页面的主要微前端之间共享一个事件触发实例。
用例:
iframe 绝对不是所有项目的通用解决方案,但 iframe 在某些情况下可以派上用场。当微前端之间没有太多交互,且需要使用沙箱对微前端进行封装时,iframe 就会大有用处。沙箱环境可以释放内存,且微前端之间不会发生依赖冲突,消除了其他实现的一些复杂性。
iframe 的缺点包括可访问性差、性能差和对搜索引擎不友好,因此 iframe 的最佳用例是在桌面应用、B2B 应用或企业内部应用中。
web 组件
Web 组件是一组 Web API,允许创建自定义、可复用、封装后的 HTML 标签,用于网页和 Web 应用。当提到微前端时,首先想到的可能不是 Web 组件,但是 Web 组件具有一些有趣的特性,使得它成为构建微前端架构的合适的解决方案。比如,可以将样式封装在 Web 组件中,而不必担心泄露到主应用中。
Web 组件由 3 种主要技术组成:
- 自定义元素
- shadow DOM
- HTML 模板
服务器端组合
服务器端组合的横向拆分架构是微前端生态系统中最灵活、最强大的解决方案,这要归功于云服务。
当应用对 SEO 有强烈要求时,通常会选择服务器端渲染,因为这种技术可以加快页面的加载速度,且页面是完全渲染好的,不需要任何 JavaScript 逻辑。鉴于每个搜索引擎进行排名时都会考虑页面的加载速度,服务器端渲染有助于应用在搜索引擎结果页面上的位置提升。
在服务器端可以使用一些技术来加快最终输出的速度,比如在不同的层(服务器、内存或 CDN)中进行缓存
这个架构的另一个挑战是组合层所有权。一种最佳实践是由前端开发人员和后端开发人员组成跨职能团队,端到端地共同管理微前端的组合层。
边缘侧组合
Edge Side Includes(ESI)是由 Akamai 和 Oracle 等公司在 2001 年创建的。它是一种标记语言,用于将不同的 HTML 片段组合成一个 HTML 页面,并将最终结果提供给客户端。通常,ESI 是在 CDN 层运行的,因为 CDN 提供了极大的可扩展性。