协调微前端
现在是时候讨论如何协调微前端了。
首先,关于微前端应该是什么样子,有两种思路,如上一篇文章
中所述,我解释了微前端的不同实现:一个微前端对应着一块用户界面的区域,其中微前端是 SPA 或单个页面。
当我们考虑基于应用的不同逻辑区域(如标题,页脚,付款表单等)的微前端实现时,我们将面临不同的挑战,例如:
哪个团队将汇总聚合的视图?
我们如何避免每个团队的外部依赖?
哪个团队对汇总视图中的问题负责?
我们如何确保应用的特定区域与父容器没有紧密耦合?
我们怎样才能确定依赖关系之间没有冲突?
我们是在运行时还是编译时组装?
如果我们决定在运行时创建页面,那么我们的应用服务器层是否可以扩展?
内容是否可以缓存,可以的话持续多长时间?
我们如何确保开发流程不受分布式团队的影响?
还有许多其他问题(技术和组织上的)可能使我们的生活方式(life way)变得更加复杂。
有趣的是,这种方式没有提供预期的好处。Spotify 大规模地做了许多工作,回滚到了基于 SPA 的更“经典”的架构。
方便起见,我们将我们的微前端定义为 SPA 或单页,在编译时生成一版,以避免在合成层发生任何可能的意外。
无论如何,这种方法也面临着一些挑战,大概主要的是理解我们如何协调我们的微前端,这是本文的重点。
协调层可以位于客户端,服务器端或边缘端;解决方案取决于协调层对我们的应用应该是多么“智能”。
服务器端或边缘端协调器
服务器端或边缘端协调器意味着对于任何深层链接或器质性(organic)流量,必须通过应用服务器或边缘解决方案(例如 lambda@edge)来分析我们的域名,在这两种情况下我们都需要维护与静态 HTML 文件(又名微前端)对应的 URL 映射。
举个例子,如果用户从我们的应用注销,我们应该卸载经过身份验证的微前端并加载登录/注册微前端,因此应用服务器或边缘上运行的代码应该知道要提供哪个 HTML 文件,来服务我们将使用 SPA 的每个 URL 或 URL 组。
考虑到我们可以直接在服务器上快速更改微前端映射而不会对客户端产生任何影响,这种技术可以毫无问题地工作,但是它提出了一些潜在的挑战,例如找到在微前端之间共享数据的最佳方法是浏览器内存储的一些限制,并且对服务器进行太多往返是不理想的,特别是对于慢速连接。
另一个挑战是找到初始化应用的解决方案,考虑到我们将整体块分成多个子域的微前端,我们是否会在每次加载新的微前端时初始化应用?我们是否要使用服务器端渲染在 HTML 中存储配置?我们如何在微前端之间进行沟通?当有突发流量时,我们如何扩展我们的应用服务器?
这些是实现服务器端或边缘端协调器的一些挑战。
客户端协调器
另一种可能的方法是创建一个客户端协调器,负责:
- 初始化应用
- 将应用的配置共享给所有微前端
- 根据用户的状态加载/卸载微前端
- 微前端之间的路由
- 暴露一个用于在微前端和客户端协调器之间进行交互的 API
此解决方案的好处之一是你可以更好地控制应用初始化。
如果设计得很好,客户端协调器就不需要经常更改,因此会相当稳定。
它提供了可供各种微前端使用的附加功能,但它不是特定领域的,当我们的目标是从他们运行的平台(浏览器而不是移动设备或智能电视)中抽象出我们的微前端时,它也是一个很好的解决方案。)。
主要的弊端是初始化的投资,为了确定这个协调器应该处理哪个功能,巨大的风险暗藏其中,这一层上的错误可能会炸毁整个应用和新功能的实现,如果没有很好地协调,可能会减慢其他团队创建跨团队依赖的速度。
在 DAZN 中,我们选择了一个名为 引导程序(bootstrap) 的客户端协调器。
引导程序具有上面列出的所有职责以及与我们的用例相关的额外职责,实际上,引导程序正在抽象运行应用的平台的 I/O API,这样每个微前端都是在不知道平台已加载的情况下完成的。
通过这种技术,我们可以在多个智能电视,控制台或机顶盒上重复使用微前端,而无需重写特定设备的实现,除非实现存在内存泄漏或性能问题。
每次用户在浏览器中键入我们的域名或在智能电视上打开应用时,都会提供引导程序,它始终存在,并且在整个用户会话期间从不卸载。
让我们尝试进一步扩展引导程序,以了解它背后的主要思想:
初始化应用
引导程序应负责设置应用上下文,首先要了解用户是否经过身份验证,并根据应用初始化我们可以加载正确的微前端。
应在此阶段管理应用为整个应用设置上下文所需的任何其他有意义的信息。
它可以是静态的配置(JSON)或动态的(需要消费 API),无论哪种方式,我们的前端都有一个外部配置允许我们在不需要引导程序版本的情况下更改系统的某些行为。
例如,配置可以为应用生命周期提供有价值的信息,例如功能切换,用户界面的本地化标签等。
微前端路由
引导程序明确地负责微前端之间的路由,在我们的实现中,我们在引导程序和每个微前端之间有 2 个路由扩展。
引导程序没有我们应用的整个 URL 映射,而是在内存中加载根据用户状态和通过用户的交互或深层链接请求的 URL,加载相应微前端的映射。
这两个维度允许我们加载正确的微前端并留给处理 URL 的微前端代码来管理组成它的不同视图。
这里的经验法则是为微前端分配特定的第二级路径,这样就可以更容易地解决微前端的范围,例如,当用户键入 mydomain.com/account/* 时,应该加载认证微前端。反而当用户点击 mydomain.com/support/* 等链接时,应加载帮助页面的微前端。
在每个微前端内部,我们可以决定使用其他路径,例如 mydomain.com/support/help-page-A 或 mydomain.com/support/help-page-B,这样就可以在微前端没有通过应用的多个部分传播它时,保留域名知识。
这里的主要内容是:我们在具有客户端协调器的微前端应用中,有两种类型的路由,一种是在引导级别的全局路由,另一种是在微前端内部的本地路由。
微前端的生命周期
正如我们之前提到的,每个微前端应该通过引导程序加载,但是如何实现?
例如,Single-spa 使用 javascript 文件作为安装新微前端的入口点。
在 DAZN 中,我们采用了不同的方法,因为只使用 javascript 文件加载微前端会排除在编译时使用服务器端渲染的可能性,这对我们来说是一个有趣的选择,可以为我们的用户提供更快的反馈。它们可以从一个微前端过渡到另一个微前端。
考虑到 HTML 文件基本上是一个具有特定模式的 XML 文件,引导程序可以使用 DOMParser 加载和解析附加在其自身内的所有相关节点的文件,来加载微前端。这是解析 XML 或 HTML 字符串的标准接口。
可以在引导程序的 DOM 树中附加 body 或 head 标签内的任意内容。
潜在地,我们还可以决定为我们需要附加的所有标记定义特定属性,以便快速选择它们。
无论如何,总体思路是解析 HTML 文件并在引导程序中附加加载微前端所需的内容,因此微前端 HTML 文件中存在的任何外部依赖项(如 JavaScript 或 CSS 文件)都将被追加,并因此通过浏览器加载。
这种简洁方法的一个巨大好处是,它不是以自我为中心的(opinionated),任何人都可以以一个新的微前端开始工作,而不是学习我们决定处理微前端的方式,因为最后,只要微前端输出的结果是前端三板斧:HTML,JavaScript 和 CSS 文件。
我录制了一个限制连接的视频,以显示引导程序如何将 DOM 元素附加到自身内部,因为你将看到有 4 个阶段:
- 确定要加载的微前端,
- 加载微前端的 HTML,
- 解析它,
- 用于在页面中显示微前端的相关标签。
这是一个非常简单但有效的机制!
一个慢动作视频,用于显示引导程序如何从微前端加载自身内部的节点:https://youtu.be/TKhXupQxf1M
添加到每个微前端的附加功能,是可以在安装或卸载之前和之后执行某些操作,这样微前端可以执行任何逻辑,来清理附加到 window 对象的任何对象或任意的其他逻辑到在前面提到的 4 个生命周期的方法之一中运行。
引导程序负责触发微前端生命周期方法,并在加载下一个微前端之前清理内存,此操作可确保在不同的微前端使用的库的不同版本或相同版本中不会发生冲突.
引导程序的内存和依赖关系管理
现在是时候深入研究微前端的内存管理了,考虑到引导程序每次加载一个微前端,如上一篇文章中所述,并且每个微前端都没有与另一个微前端共享任何库或依赖,我们可能最终会出现微前端加载 React v.15 和接下来加载 React v.16 的情况。
与此同时,我们希望能够自由选择每个微前端内部的任意技术和库版本,因为保留业务和技术知识的开发团队应该提供最佳的实现选择,而不是在整个过程中进行不断的权衡。整个应用通常在我们使用单页面应用时就正好就绪了。
在这个阶段,我相信很容易猜到我们面临的挑战,因为微前端使用的任何库或框架都会在全局 window 上附加对象,而在 Javascript 中我们无法直接控制垃圾收集器,但我们可以方便地处理删除给定对象的所有引用和实例的元素。
为了实现这个目标,额外的引导责任是跟踪任意的微前端附加到 window 对象上的对象,并在卸载微前端之后,加载新的前端之前清理 window 对象JavaScript 中的元编程喜悦 🎉)。
引导程序获取附加到 window 对象的所有键的快照,并在加载新的微前端之前删除它们,这样我们就可以跟踪应该删除的内容,而无需复制内存中的所有的对象,通过这个数组的简单遍历,我们就可以删除 window 中卸载的微前端使用的对象。
用于在引导程序和微前端之间进行通信的 API 层
最后一点值得一提的是引导程序通过 window 对象公开的 API 层。
如果你问自己我们如何共享数据并在微前端之间进行通信,那么引导程序就是答案!
请记住,我们的实现是基于我们每次总是加载一个微前端的假设,并且我们基于用应用的子域切分微前端,你很快就会意识到跨越微前端共享的数据不会经常发生,如果你在定义所有子域的初始会话中运行良好的话。
在微前端之间共享数据非常简单,引导程序共享一些 API 用于存储和检索所有微前端可访问的信息,由你决定哪个存储更方便你的实现以及你想要添加到对象的限制类型在本地存储。
考虑到引导程序是在平台和微前端之间用 vanilla JavaScript 编写的一个微小层,它负责初始化应用,我们还需要公开一个 API 层来抽象 I/O 层,以便存储或从中检索信息。微前端
使用多个设备需要具有不同的 API 来存储和检索文件,因为 Web 在所有这些平台上存储 API 并不总是一致的。
要强调的另一个重要部分是从静态 JSON 文件或 API 中检索的配置,该文件通常与所有微前端共享,以了解它们运行的上下文(例如,根据国家/地区或语言共享特定配置)。
当我们设计引导程序暴露的 API 时,最重要的是试图进行前瞻性思考,因为引导程序应该是一个在每个版本都不会改变的层,否则你可能会破坏与微前端的一些约定并将微前端耦合起来。引导功能可能会危及在多个子域中拆分业务域所做的所有出色工作。
总结
在这篇文章中,我们探讨了协调微前端的可能性,我们深入探讨了在 DAZN 中被称为引导程序的客户端协调器,特别是,我们已经看到了这种方法的好处和挑战,以及我们如何应付、解决它们。
值得一提的是,我们看到引导程序有 3 个主要职责:
- 微前端之间的路由(加载,卸载和生命周期方法)
- 初始化应用
- 为微型前端通信和网络存储公开 API 层
在分享这些帖子之后,我经常收到的一个问题是,引导程序是否是开源的,这个答案是我们正在考虑的问题,但我们现在不能承诺具体的时间(这也是我之所以没有在这篇文章中分享代码的原因,再次道歉🙏)。
我真的希望你能够更清楚地了解如何构建你的下一个微前端项目,如果不能随意尝试,那么我可以写下一篇文章供你思考! ✌️