React18-微前端构建指南-全-

React18 微前端构建指南(全)

原文:zh.annas-archive.org/md5/df6bd0609a4c1b46c7b69478aee369a4

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

通过将大型、复杂的 Web 应用分解为更小、独立的组件,微前端已成为一种流行的架构模式。这使得开发规模化、加速发布速度和逐步采用新技术变得更加容易。

本书提供了在实践中进行微前端实现的全面指南,从架构原则和模式到使用 React 等框架的动手示例。你将学习如何将单体应用划分为自治的微前端。我们将探讨微前端的一些关键原则和微前端可能或可能不是正确模式的应用场景。

我们将探讨微前端的多种模式,并使用模块联邦为客户端渲染应用和服务器端渲染应用创建微前端。

我们将学习如何在构建微前端时处理诸如路由和状态管理等问题,最后,我们将学习如何使用 Azure 在 Firebase 和 Kubernetes 集群上部署我们的微前端。

本书面向对象

本书面向前端和全栈开发者,旨在使用现代 JavaScript 框架(如 React)构建大型、可扩展的 Web 应用。它也将对希望采用微前端架构的解决方案架构师有所帮助。你应该对 JavaScript、React、模块打包和基本 Web 开发概念有良好的理解。

本书涵盖内容

第一章, 介绍微前端,介绍了不同的架构模式,例如用于构建微前端的多个 SPA 和微应用模式。

第二章, 微前端的核心理念和组件,涵盖了核心原则,如独立部署性、边界上下文、隔离故障、运行时集成等。

第三章, 微前端的多仓库与单仓库对比,比较了单仓库和多仓库在管理微前端代码库方面的差异,以及为什么单仓库更适合构建微前端。

第四章, 为微前端实现多 SPA 模式,展示了如何将微前端构建为一个单页应用的集合。

第五章, 为微前端实现微应用模式,深入探讨了使用模块联邦构建微前端,并涵盖了关于不同微应用之间路由和状态共享的关键主题。

第六章, 服务器端渲染微前端,展示了如何使用模块联邦构建服务器端渲染的微前端。

第七章将微前端部署到静态存储,带我们了解了将我们的微前端部署到静态存储托管服务(如 Firebase)的过程。

第八章将微前端部署到 Kubernetes,演示了将微前端部署到 Kubernetes,例如 Azure 上的 AKS。

第九章在生产中管理微前端,涵盖了诸如分支策略、版本控制、回滚策略和功能开关等主题,这些对于在生产中管理微前端至关重要。

第十章构建微前端时避免的常见陷阱,讨论了一些开发人员和架构师犯的常见错误,这些错误会负面影响我们最初选择微前端的好处。

第十一章微前端最新趋势,涵盖了某些新趋势,如 ES 构建、云或边缘函数、岛屿模式和生成式 AI,以及它们如何被用于构建未来的微前端。

为了充分利用本书

代码示例使用 React、webpack、Node.js 和 npm。熟悉这些工具将有所帮助。示例可以在任何操作系统上跟随。

本书数字版包含详细的代码示例,可以复制粘贴以快速启动。为了获得最佳学习体验,请尝试从头开始构建示例。

本书涵盖的软件/硬件 操作系统要求
React 18 Windows、macOS 或 Linux
TypeScript 3.7 Windows、macOS 或 Linux
Docker Engine 24 Windows、macOS 或 Linux

如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将有助于您避免与代码复制和粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub(github.com/PacktPublishing/Building-Micro-Frontends-with-React-18)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“确保我们在proxy.conf.json文件中设置了 URL 路由。”

代码块设置如下:

  "scripts": {
    "start": "nx serve",
    "build": "nx build",
    "test": "nx test",
    "serve:all": "nx run-many --target=serve"
  },

当我们希望将你的注意力引到代码块中的特定部分时,相关的行或项目将以粗体显示:

pnpm install semantic-ui-react semantic-ui-css

任何命令行输入或输出都应如下编写:

pnpm serve:all

粗体:表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个例子:“接下来,我们更新添加删除按钮的 onclick 事件,如下所示。”

小贴士或重要注意事项

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果你对这本书的任何方面有疑问,请通过 customercare@packtpub.com 发送电子邮件,并在邮件主题中提及书名。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们非常感谢你能向我们报告。请访问 www.packtpub.com/support/errata 并填写表格。

盗版: 如果你在网上以任何形式发现了我们作品的非法副本,我们非常感谢你能提供位置地址或网站名称。请通过 copyright@packt.com 联系我们,并提供材料的链接。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请访问 authors.packtpub.com

分享你的想法

一旦你阅读了 Building Micro Frontends with React 18,我们很乐意听听你的想法!请点击此处直接进入此书的亚马逊评论页面并分享你的反馈

你的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载这本书的免费 PDF 副本

感谢你购买这本书!

你喜欢在旅途中阅读,但无法随身携带你的印刷书籍吗?

你的电子书购买是否与你的选择设备不兼容?

不要担心,现在,每当你购买一本 Packt 书籍时,你都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从你喜欢的技术书籍中搜索、复制和粘贴代码到你的应用程序中。

优惠不会就此结束,你还可以获得独家折扣、时事通讯和每日免费内容的每日电子邮件。

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781804610961

  1. 提交你的购买证明

  2. 就这样!我们将直接将你的免费 PDF 和其他福利发送到你的电子邮件。

第一部分:微前端简介

本部分涵盖了微前端背后的核心概念和原则,包括采用这种架构的动机、关键组件以及微前端与单体架构的区别。

本部分包含以下章节:

  • 第一章, 介绍微前端

  • 第二章, 微前端的关键原则和组件

  • 第三章, 单仓库与多仓库在微前端中的应用

第一章:微前端介绍

我们在微前端上又回到了起点!在 Web 1.0 时代,网站主要由 ASP、JSP 或 PHP 构建的单页组成,我们可以对每个单独的页面进行更改,并通过 FTP 上传到服务器,然后立即对消费者可用。然后是 Web 2.0 时代和 Web 应用以及单页应用SPAs)的概念,我们编译、转译和部署大型单体应用。现在,我们似乎又回到了处理更小应用和页面的工作方式。

2000 年代初迎来了 Web 2.0 时代和 Web 应用的概念。几年后,JavaScript 框架允许你构建即时更新且每次用户点击链接或按钮时不会重新加载新页面的单页应用(SPAs)。对于小型到中型应用,SPAs 确实很快,但随着团队全力以赴构建大规模的 SPAs,以及应用和团队的成长,开发速度和速度显著下降。由于集中管理的库等原因,团队似乎在争论文件夹结构、状态管理和破坏彼此的代码。这些大型 SPAs 也因为这些应用的大包体积而开始变得性能不佳。更重要的是,解析这些 JavaScript 包所需的高执行时间使得低端设备和手机上的应用更加缓慢。正是在这个时候,开发人员和架构师开始寻找解决这些问题的方案。幸运的是,他们不需要走得太远。

你看,后端团队在几十年前就遇到了与大型后端单体相同的问题,并转向微服务架构模式来解决它们的性能和扩展挑战。现在,前端团队正试图将微服务的相同原则应用于他们的前端应用,这些应用被称为微前端

后端团队走向微服务的过程非常漫长,跨越了多个十年,许多团队仍在为此而努力。然而,得益于许多辩论、讨论、思考、领导和从各种微服务实现中学习到的经验,微服务架构已经达到了一个整体成熟度和共识。

前端团队刚刚开始意识到微前端的概念,关于什么是微前端,存在多种观点,包括实际上微前端是否是一件好事。可能需要几年甚至十年,才能在微前端上达成一些共识。然而,好事是,我们可以从微服务的发展历程中学到很多东西,因为许多微服务的原则和架构模式也适用于微前端。

在本章中,我们将首先了解微前端的需求。我们将涵盖微前端的定义,然后讨论微前端的不同模式。我们还将探讨有助于我们选择设计应用时采用哪种模式的参数。最后,我们将创建我们自己的第一个微前端。

在本章中,我们将涵盖以下主题:

  • 定义微前端

  • 理解微前端模式

  • 选择合适的模式

  • 使用微前端实现 Hello World

到本章结束时,您将更好地理解构建微前端最常用的两种模式,以及帮助您决定哪种模式最适合您的指南。

在本章的结尾,我们将构建一个简单的多 SPA 微前端示例,并了解如何在不同的 SPA 之间导航。

技术要求

在您浏览本章的代码示例时,您需要以下内容:

  • 至少 8 GB RAM 的 PC、Mac 或 Linux 桌面/笔记本电脑(16 GB 更佳)

  • 英特尔 i5+芯片组、AMD 或苹果 M1+芯片组

  • 至少 256 GB 的空闲硬盘存储空间

您还需要在您的计算机上安装以下软件:

  • Node.js 版本 16+(如果您必须管理不同版本的 Node.js,请使用nvm)。

  • 终端:一个现代的 shell,例如zsh,Mac 上的 iTerm2 配合oh-my-zsh(您稍后会感谢我的),或者 Windows 上的 Hyper(hyper.is/)。

  • 集成开发环境(IDE):我们推荐 VS Code。

  • npmyarnpnpm。我们推荐 PNPM,因为它速度快且存储效率高。

  • 浏览器:Chrome/Microsoft Edge、Brave 或 Firefox(我使用 Firefox)。

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Building-Micro-Frontends-with-React

定义微前端

在本节中,我们将专注于定义微前端及其关键优势,并了解设置微前端所涉及的相关初始投资。

当前接受的微前端定义如下。

“微前端是由可以独立部署的微应用组成的,由独立团队拥有,这些团队负责交付整个应用中某个特定领域的业务价值”。

这个定义中的关键词是独立部署的,并且由独立团队负责。如果您或您的团队中至少有一个这些术语不适用,那么您可能不需要微前端。一个常规的单页应用(SPA)会更有效率且更富有生产力。正如我们稍后将要看到的,微前端带来了一些初始的复杂性,除非您有一个大型应用,其中应用的部分由个别团队管理,否则可能不值得。

我们注意到,一些正在实施微前端的团队误解了微前端中的“微”部分,并认为除非应用程序被分解到最小级别,否则它不遵循微前端架构。他们将应用程序分解成非常小的应用程序,这增加了许多不必要的复杂性。实际上,这抵消了微前端本应带来的所有优势。

在我们看来,实际上情况正好相反。当将应用程序分解为微应用时,团队应理想地寻找识别最大的可能微应用或微应用,这些微应用可以由敏捷团队独立管理并部署到生产环境中,而不会影响其他微应用。

从这个要点中,关键不是被“微”这个术语所左右,而是要确定可以由单个敏捷团队独立部署的最大应用程序。

在我们深入探讨微前端奇妙世界之前,重要的是要记住,并非每个应用程序都需要是微前端。让我们在下一节中了解更多。

理解微前端溢价

马丁·福勒 讨论了微服务溢价。这指的是微服务带来了一些开销和复杂性,主要是在初始设置和服务之间的通信渠道方面。马丁接着说,微服务架构的优势只有在规模和复杂性提升器启动时才开始显现。为了理解这一点,让我们看看以下图表:

图 1.1 – 微服务溢价图(来源:https://martinfowler.com/bliki/MicroservicePremium.html)

图 1.1 – 微服务溢价图(来源:martinfowler.com/bliki/MicroservicePremium.html)

上述图表是应用程序生产力和复杂性的图表,描述了随着复杂性的增长,单体 SPA 和微前端生产力的下降。

对于微前端架构来说,也是如此。将组件的各个部分、路由和模板解耦并将它们委托给不同的系统,对于小型或中型应用程序来说,整个过程可能成为不必要的开销。

微前端的优势只有在项目开始达到图 1.1中所示的大小和复杂性阈值时才会显现。

探索微前端的优势

微前端架构的所有优势都与规模和范围相关。话虽如此,以下微前端的优势仅适用于由超过 15 人团队构建和支持的应用程序。

在接下来的章节中,我们将了解团队在实施微前端架构时可以期待的好处,所有这些好处都与提高生产力和改善团队成员的开发体验直接相关。

更快的开发和部署

单体应用的主要缺点之一是,随着应用和团队规模的扩大,功能开发和部署速度会变得非常缓慢。我们注意到团队花费了更多的时间在一个团队等待另一个团队完成某些工作,然后才能部署应用。在微前端架构中,每个敏捷团队独立工作于他们的微应用,构建和发布功能,无需过多担心其他团队正在做什么。

随着应用的增长更容易扩展

微前端架构完全是关于组合更小的微应用,因此随着应用规模的扩大,这只是添加额外的微应用并由敏捷团队负责的问题。

现在,由于每个团队处理的是更小的微应用,团队成员需要花费更少的时间来理解代码库,并且不应该因为担心他们的代码更改会影响其他团队而感到不知所措或担忧。

微前端允许快速扩展,一旦设置了基本微前端框架,敏捷团队就可以并行工作。

改进的开发者体验

使用隔离的、独立的微应用,每个团队编译、构建和运行自动化单元测试所需的时间大大减少。这使得团队能够更快地构建和交付功能。

虽然团队更频繁地为他们的微应用运行隔离的单元和自动化测试,但我们建议在需要时或在提交代码到 Git 之前运行完整的回归测试套件 进行端到端测试

渐进式升级

前端生态系统是发展最快的生态系统。每隔几个月,就会出现一个比之前更好的新框架或库。话虽如此,总是有一种冲动想要使用最新的框架重写现有的应用。

对于大型应用来说,没有重写整个应用就难以轻松升级或引入新框架。重写应用的成本以及由于重写而引入错误的关联风险都太高了。团队不断推迟升级,几年后,他们发现自己正在使用过时的框架。

使用微前端,更容易选择一个小型微应用进行升级或重写,然后逐步将其推广到其他微应用。这也使得团队能够体验新变化的好处,并在迁移新框架到其他微应用时学习和纠正方向。

在我们进入下一节之前,让我们快速回顾一下我们迄今为止学到的关键点:

  • 微前端适合构建大型应用,其中团队被设置为全栈团队,后端开发者、前端开发者、产品负责人等都在同一个敏捷团队中。

  • 微前端有许多好处,如团队独立性、以改进的速度发布的功能和更好的开发者体验。然而,一旦你克服了与“微前端溢价”相关的复杂性的初始阶段,这些好处才会开始显现。

理解微前端模式

当谈到微前端时,有太多的解释。微前端还处于早期阶段,没有正确或错误构建它们的方法。对任何技术/架构问题的答案是“这取决于……”。在本节中,我们将关注团队在构建微前端时采用的最常见的两种模式。我们将探讨在决定哪种模式可能适合您时需要考虑的关键因素。我们将通过构建一个真正基本的微前端来结束本节,以推动进展。

在非常高的层面上,微前端有两种主要模式。这两种模式都可以应用于你是在构建服务器端渲染SSR)应用还是客户端渲染CSR)应用。为了更好地说明这些模式,我们将以亚马逊这样的电子商务应用为例。

在接下来的子节中,我们将探讨这两种模式以及它们之间的区别。

多 SPA 模式

我们将要讨论的第一个模式是多 SPA模式。正如其名所示,该应用由多个 SPA 构建而成。在这里,应用被分解为 2-3 个独立的 SPA,并且每个应用都在其自己的 URL 上渲染。当用户从一个 SPA 导航到另一个 SPA 时,他们会通过浏览器刷新进行重定向。在电子商务应用的情况下,我们可以将搜索、产品列表和产品详情视为一个 SPA,而购物车和结账作为另一个 SPA。同样,我的账户部分,包括登录、注册和资料信息,将形成第三个 SPA。

下图展示了电子商务应用的多 SPA 模式微前端的示意图:

图 1.2 – 电子商务应用的**多 SPA**模式微前端

图 1.2 – 电子商务应用的多 SPA模式微前端

如前图所示,我们的电子商务应用由三个 SPA 组成:目录 SPA、结账 SPA 和账户 SPA。

在这种模式的简单形式中,每个应用都表现得像一个独立的 SPA,位于其独特的全局 URL 中。

每个 SPA 都部署在独特的全局路由上。例如,目录应用将被部署在类似mysite.com/catalog/*的 URL 上,而目录应用内的所有后续二级路由都将作为 SPA 加载到/catalog/*路由中。

同样,账户应用将位于全局路由mysite.com/accounts/,而账户应用内的不同页面,如登录、注册和资料,将在如下 URL 中可用:mysite.com/accounts/loginmysite.com/accounts/register

如前所述,当用户从一个宏应用移动到另一个宏应用时,浏览器中的页面将重新加载。这是因为我们通常使用 HTML href标签在应用之间导航。这种浏览器刷新是完全可以接受的。我看到一些团队为了实现单页体验,不遗余力地复杂化他们的架构。然而,事实是用户并不真的关心你的应用是 SPA 还是多页应用(MPA)。只要体验快速且流畅,他们就会满意。

有时,浏览器重新加载可能会对你有利,因为它可以减少由于内存泄漏或数据存储中放入过多数据而导致的内存膨胀风险。

然而,如果你真的想实现那种 SPA 体验,那么你可以始终创建一个薄应用壳,它托管全局路由和数据存储,这样每个应用都在这个应用壳内调用。我们将在接下来的章节中详细介绍这种模式。

在这种模式中,路由通常分为两部分,全局或主要路由,位于应用壳内,以及次要路由,位于相应的应用内。

下图显示了带有应用壳的多-SPA 模式的示例:

图 1.3 – 带有应用壳的多-SPA 模式以提供 SPA 体验

图 1.3 – 带有应用壳的多-SPA 模式以提供 SPA 体验

在这里,你会注意到我们引入了应用壳的概念,它包含了头部组件,不同的单页应用(SPA)在内容槽中加载。这种模式提供了真正的 SPA 体验,因为当从一个 SPA 切换到另一个 SPA 时,头部组件不会刷新。

微应用模式

构建微前端的其他模式是我们所说的微应用模式。我们之所以称之为微应用模式,是因为这是一种对应用程序更细粒度的分解。

图 1**.4所示,网页由不同的组件组成,每个组件都是一个独立的微应用,它可以独立存在,并且可以作为同一页面的组成部分与其他微应用协同工作。

图 1.4 – 产品图像和推荐产品作为不同微应用共存的微应用架构

图 1.4 – 产品图像和推荐产品作为不同微应用共存的微应用架构

你会注意到前面的图是图 1**.3的一个更细粒度的版本,其中我们将中央内容槽进一步分解为更小的微应用。注意中央内容区域现在由两个微应用组成,即产品详情和推荐产品微应用。

微应用模式比多-SPA 模式复杂得多,主要推荐用于非常大的 Web 应用,其中多个团队拥有单页上的不同元素。

图 1.4 中,我们假设有一个专门负责管理页面产品描述组件的团队,另一个团队负责管理同一页面的产品推荐组件。

我们还假设这些组件更新以增强功能的频率会有所不同;例如,推荐微应用会不断进行 A/B 测试,因此需要比产品图片和描述微应用更频繁地部署,后者可能不会那么频繁地发生变化。

在这种模式中,所有路由,无论是主要还是次要的,都由应用程序外壳管理。在这里,除了管理路由和全局状态外,应用程序外壳还需要存储/检索每个路由和每个页面中需要加载的不同微应用的页面布局信息。

在大多数情况下,这样的大型应用程序通常已经有一个内容管理系统CMS)或模板引擎,其中存储和提供布局和组件树。

总结来说,随着本节的结束,我们看到了构建微前端的两种主要模式:多 SPA 模式和微应用模式。这些模式主要区别在于你分解应用程序的粒度级别,以及如何在微前端架构中管理路由。

在下一节中,我们将探讨有助于您选择正确模式的指南。

选择合适的模式

现在我们对微前端这两种模式有了广泛的理解,让我们花些时间来探讨一些关键考虑因素,这将帮助您决定选择哪种模式。

虽然可能存在许多关于什么是正确的、考虑多远未来以及如何使应用程序和架构具有前瞻性的观点,但我们认为有两个主要因素将帮助您决定为您的微前端架构选择哪种模式。让我们在接下来的章节中详细探讨它们。

团队构成

对于在微服务和微前端上构建应用程序的团队来说,根据业务功能进行垂直切分是一种常见的做法。在电子商务的例子中,我们可能有一个专注于浏览旅程的团队,另一个专注于结账旅程的团队。如果有一个敏捷团队负责整个浏览器旅程,而另一个敏捷团队负责整个结账旅程,那么建议您采用多 SPA 模式。然而,如果您有多个小型团队,分别拥有业务领域的不同实体,例如,搜索、产品推荐和促销,那么采用微应用模式将是明智的选择。如前所述,一个经验法则是每个敏捷团队理想情况下拥有一个单独的微应用。

部署频率

在决定如何拆分你的微前端时,另一个需要考虑的因素是部署的频率。如果应用中有一些部分比其他部分变化更频繁,那么这些部分可以分离成自己的微前端,可以单独部署而不影响应用的其它部分。这减少了需要进行的测试量,因为现在我们只需要测试正在更改的微应用,而不是整个应用。

如我们所见,是否选择多 SPA 模式或微应用模式的决定归结为两个关键因素:团队组成和部署频率,这与微前端定义中的两个关键词直接相关,即独立团队和独立部署。

使用微前端技术的 Hello World

好的,现在是时候动手编写一些代码了。我们将从构建一个基本的多个 SPA 模式应用开始。在这个例子中,我们将使用 Next.js,这是目前构建高性能 React 应用最受欢迎的工具。按照以下步骤操作:

注意

在本章的剩余部分,我们假设你正在使用pnpm作为包管理器。如果不是,请在相应的命令中将pnpm替换为npm

  1. 让我们首先为我们的应用创建一个根文件夹。我们将它命名为my-store。在你的终端中运行以下命令:

    mkdir my-store
    
  2. 现在,让我们cdmy-store,并在我们的终端中通过输入以下命令创建我们的两个 Next.js 应用,即homecatalog

    cd my-storepnpm create-next-app@12
    

    或者,我们可以输入以下内容:

    cd my-storenpx create-next-app@12
    
  3. 当提示你添加项目名称时,将其命名为home。然后它会经过各种步骤并完成安装。

    create-next-app 的有趣之处在于,即使你定义的版本为@12,它仍然会拉取 Next.js 的最新版本,因此为了确保与本章的其余部分保持一致性,我们将更新 package.json 中 next 的版本如下:

     "dependencies": {    "next": "12",
        "react": "18.2.0",
        "react-dom": "18.2.0"
    
  4. 现在删除node_modules文件夹和包锁定文件,并运行 pnpm i 命令

重要提示

虽然你可以始终使用yarnnpx来运行 CLI,但我们推荐使用pnpm,因为它比npmyarn快 2-3 倍。

  1. 设置完成后,继续创建另一个应用,重复步骤 2-5。让我们把这个项目命名为catalog

    完成后,你的文件夹结构将如下所示:

    └── my-store/    ├── home
        └── catalog
    
  2. 现在,让我们通过输入以下命令来运行home应用:

    cd homepnpm run dev
    
  3. 你的应用现在应该在端口3000上提供服务。通过在浏览器中访问http://localhost:3000来验证。

  4. 让我们去掉样板代码并添加简单的导航。找到并打开位于home/pages/index.js的文件,并将

    标签内的所有内容替换为以下内容:

         <main className={styles.main}>       <nav><a href="/">Home</a> | <a href="/catalog">Catalog</a> </nav>
            <h1 className={styles.title}>
              Home:Hello World! 
              </h1>
              <h2>Welcome to my store</h2>
            </main>
    

    注意,我们已经添加了基本的导航来在主页和目录页之间导航。现在运行在localthost:3000上的你的主页应用应该看起来如下:

图 1.5 – 屏幕截图:带有“主页”和“目录”两个导航链接的主应用

图 1.5 – 屏幕截图:带有“主页”和“目录”两个导航链接的主应用

  1. 现在,让我们继续讨论目录应用。导航到位于 /catalog/pages/index.js 的索引页面,再次,让我们移除样板代码,并将

    标签内的内容替换为以下代码:

          <main className={styles.main}>        <nav><a href="/">Home</a> | <a href="/catalog">Catalog</a> </nav>
            <h1 className={styles.title}>
                Catalog:Hello World! 
          </h1>
           <h2>List of Products</h2>
          </main>
    

    现在,由于我们已经在端口 3000 上提供了主页,我们将我们的目录应用运行在端口 3001 上。

  2. 我们通过在 catalog/package.json 文件的 scripts 部分中为 dev 命令添加端口标志来完成此操作,如下所示:

    "scripts": {    "dev": "next dev -p 3001
    …
    }
    
  3. 现在,在目录应用内部运行 pnpm run dev 命令应该会在 http://localhost:3001 上运行目录应用。您可以在下面的屏幕截图中看到这一点:

图 1.6 – 在端口 3001 上运行的目录应用屏幕截图

图 1.6 – 在端口 3001 上运行的目录应用屏幕截图

下一步是将这些连接起来,以便当用户访问 localhost:3000 时,它将用户导向主页应用,而当用户访问 localhost:3000/catalog 时,他们将重定向到目录应用。这是为了确保两个应用都感觉像是同一应用的一部分,尽管它们运行在不同的端口上。

  1. 我们通过在 home/next.config.js 文件中设置 rewrites 规则来完成这项操作,如下所示:

    const nextConfig = {  reactStrictMode: true,
      swcMinify: true,
      async rewrites() {
        return [
          {
            source: '/:path*',
            destination: `/:path*`,
          },
          {
            source: '/catalog',
            destination: `http://localhost:3001/catalog`,
          },
          {
            source: '/catalog/:path*',
            destination: `http://localhost:3001/catalog/:path*`,
          },
        ]
      },
    }
    module.exports = nextConfig
    

    如前述代码所示,我们只是告诉 Next.js,如果源 URL 是 /catalog,则从 localhost:3001/catalog 加载应用。

  2. 在我们测试之前,需要对目录应用进行另一个小的修改。如您所见,目录应用将在端口 3001 的根目录上提供服务,但我们的目标是让它服务在 :3000/catalog。这是因为我们之前所做的重写,Next.js 预期目录应用及其资产将在 /catalog/ 下可用。我们可以通过在 catalog/next.config.js 文件中设置 basePath 变量来实现这一点,如下所示:

    const nextConfig = {  reactStrictMode: true,
      swcMinify: true,
      basePath:'/catalog'
    }
    
  3. 现在,为了测试这一点是否正常工作,我们将通过导航到主页和目录应用并在两个不同的终端窗口中运行 pnpm run dev 命令来启动这两个应用。

  4. 在您的浏览器中打开 http://localhost:3000 并验证主页应用是否已加载。点击 目录 链接并验证目录页面是否在 http://localhost:3000/catalog 上加载。请注意,运行在端口 3001 上的独立应用目录以一种类似于“代理”的方式加载到父应用/主应用的唯一 URL 中。这是微前端的关键原则之一,其中运行在不同端口和不同位置的应用被“缝合”在一起,使其看起来像是同一应用的一部分。

有了这些,我们就完成了使用多-SPA 模式创建我们的第一个微前端。在接下来的章节中,我们将更详细地探讨微应用模式。这种模式符合构建微前端的大多数用例,并符合微前端的所有关键原则,这些原则我们将在下一章中看到。

摘要

本章到此结束。我们首先学习了微前端(当执行正确时)如何帮助团队在应用规模和复杂性增长的同时,以一致的速度继续发布新功能。然后,我们了解到有两种主要的微前端实现模式,即多-SPA 模式和微应用模式。我们看到多-SPA 模式更容易实现,将适合大多数用例。当给定页面的不同元素由不同的敏捷团队拥有时,微应用模式将更为合适。最后,我们学习了如何构建我们自己的微前端应用程序,并看到了我们如何在两个应用程序之间导航,同时仍然给用户一种它们都是单个应用程序一部分的错觉。

在下一章中,我们将探讨在设计微前端架构时必须严格遵守的一些关键原则。我们还将探讨微前端的一些关键组件以及它们可以实现的多种方式。

第二章:微前端的关键原则和组件

微前端是一把双刃剑。如果做得正确,它们可以为团队带来巨大的快乐和生产力;然而,如果实施不当,它们可能会使事情变得更糟。

话虽如此,在构建微前端架构时,我们需要牢记一些关键原则和考虑因素。

在本章中,我们将探讨微前端架构的关键设计原则以及为什么将它们视为神圣不可侵犯的重要性。我们强调这些原则的原因是,它们为微前端架构奠定了基础。如果团队选择忽视这些原则,他们可能无法从微前端模式中提取所有好处。然后,我们将探讨对任何微前端架构至关重要的关键组件。

在本章中,我们将涵盖以下主题:

  • 理解关键原则

  • 微前端架构的关键组件

到本章结束时,你将更好地理解团队在设计微前端架构时需要牢记的指导原则和关键考虑因素。

理解关键原则

对于所有软件团队来说,制定一套规则和指导原则,让所有团队成员和他们编写的代码都遵守,这一点非常重要。这确保了当团队讨论某些技术方法时,他们可以将其与这些指南进行验证。这反过来又确保了团队可以通过将这些关键指南与结果进行映射来专注于结果,而不是过于沉迷于过程的细微差别。这有助于团队更快地做出决策。

在接下来的章节中,我们将探讨团队在遵循微前端模式时必须遵守的关键原则。

领域驱动团队

Meta 的 React 项目负责人 Dan Abramov 曾发推文提问:“微前端是在解决技术问题还是组织问题?

当你这么想的时候,我们今天在软件开发中看到的大部分问题确实源于团队的组织方式。

领域驱动设计(Domain Driven Design)是微服务世界中一个成熟的概念。后端微服务团队通常围绕这些领域模型组织。随着微前端的出现,我们将同样的思维方式扩展到前端世界,通过在这些领域模型内重新组织前端团队,我们现在能够创建垂直切分的团队,其中领域驱动团队能够从端到端拥有业务功能的责任,并且能够独立工作。

为了在微前端方面取得成功,至关重要的是,微应用及其所属团队必须与这些领域模型以及他们旨在提供的业务价值相映射。

让我们快速了解一下领域驱动团队可能的样子:

图 2.1 – 领域驱动团队

图 2.1 – 领域驱动团队

上述图示显示了电子商务应用程序的三个领域驱动团队,即目录团队结账团队用户账户团队。在每一个团队中,你会看到他们有专门负责前端、后端和集成工程师的团队成员。

隔离故障

微前端天生就是“去中心化”设计的。其众多好处之一就是隔离故障,并减少错误的影响范围。单体单页应用SPAs)的一个常见问题是,任何一个模块中的一行错误都会阻止整个应用程序编译,或者运行时错误会导致整个页面出错。

当设计微前端架构时,你需要确保如果有一个或多个微前端失败,系统可以优雅地降级服务。

如果一个微前端依赖于另一个微前端来运行,那么我们就违反了微前端的一个关键原则,这个原则应该不惜一切代价避免。

独立部署

微前端架构的另一个关键原则是能够独立部署每个应用程序,而无需重新部署其他应用程序。

当部署新应用程序时,它应立即对用户可用,并且不需要重启主机应用程序或服务器以使更改生效。

当不同团队在微前端上工作时,一个有趣的观察是,从架构角度来看,这些微应用可以独立更新,但部署这些微前端的 DevOps 管道被设计为同时部署所有应用程序,从而抵消了独立部署的好处。

确保 DevOps 管道也设计得如此,即当任何应用程序准备好部署时,只有相关的管道运行并部署该应用程序,而不会影响其他应用程序。

这种 DevOps 管道配置错误主要源于存在负责构建管道和生产部署的独立 DevOps 团队的问题。

解决这个问题的最好方法是确保我们有“全生命周期团队”,这些团队负责构建应用程序,同时也负责将其部署到生产环境中。这些团队与 DevOps 团队紧密合作,构建 CI 和 CD 管道,然后接管管理和运行这些管道的控制权。

优先考虑运行时集成

在微前端背景下,一个常见的讨论是构建时集成与运行时集成。在构建时集成中,不同的团队将他们的微应用构建并发布到版本控制系统或工件存储库,例如 NPM 或 Nexus。

然后,在构建时间,所有这些微应用被汇集在一起构建一个单一的应用程序包,然后部署到生产环境中。我们强烈反对这种构建时集成的模式,因为它破坏了上述独立部署的原则。这种模式可能适用于你有一个计划发布的版本,每个月发生一次或两次。然而,在这种情况下,你可能更适合使用单体单页应用程序,并且真的不需要处理微前端架构的所有复杂性。

设计微前端架构时,始终优先考虑运行时集成。

您的微应用应该在部署后立即可用,这确保了每个团队可以持续地将他们的微应用部署到生产环境中,而不依赖于其他团队来使他们的应用可用。

在大多数微前端模式中,我们可以利用一个宿主应用程序或外壳应用程序,它跟踪在其中加载的不同微应用,但必须注意确保这个宿主/应用程序外壳是以可扩展性为前提构建的。如果检查微应用新版本的过程消耗了大量的 CPU 或内存资源,那么当你的应用程序扩展时,它将有很大的风险成为单点故障,无论是从微应用还是从它接收到的流量来看。

避免陷入“分布式单体”的陷阱

不要重复自己(DRY)在微服务/微前端的世界中有稍有不同的含义。大多数开发者将 DRY 与代码重用性联系起来。当与微前端一起工作时,团队可能会过度创建库和实用工具,这些库和实用工具最终被导入并用于每个微应用中。现在,随着每个团队需求的增长,他们开始向这些通用库和实用工具添加功能,希望这对其他团队有益。然而,它造成的问题是现在额外的未使用代码被导入到其他微应用中(虽然摇树优化可以解决这个问题,但在大多数情况下,主要是由于糟糕的编码实践,摇树优化工作得不好,我们最终会在应用程序中导入不必要的代码)。这些共享库的另一个问题是,引入破坏性变化的风险要高得多,一个微应用所做的更改现在会破坏其他微应用。通过过度追求代码重用性,我们最终得到一个通常被称为“分布式单体”的东西,这实际上是两者的最坏之处。

有一些共享库或,如果使用 TypeScript,一个共享的类型/接口文件是可以的,但我们必须避免创建大型通用库。

在微服务/微前端的世界里,DRY(Don’t Repeat Yourself)本质上是指自动化任务,这样你就不必为每个微服务或微应用手动重复步骤。这些可能包括自动化质量门、性能和安全检查作为开发者管道的一部分。

技术中立

微前端架构的另一个原则是它应该是技术中立的,这意味着每个微应用“理论上”可以使用不同的框架/语言构建。然而,尽管这是可能的,但这并不意味着团队应该全力以赴,使用 Vue、Angular 或 React 来构建不同的微应用。

有多个原因应该避免这样做:

  • 使用多个库/框架意味着向用户设备发送额外的数据负载

  • 这使得轮换团队成员变得困难,从一个团队转移到另一个团队意味着需要适应新的框架/库

这一原则的主要目的是允许增量升级,无论是从旧版本到同一库的新版本,还是探索新框架的好处。

细粒度扩展

在规划微前端的部署策略时,您必须确保它支持细粒度扩展。通过细粒度扩展,我们的意思是,如果某些页面因营销活动或其他类似原因而获得大量流量,那么仅应扩展服务这些页面的服务器,而其他为微前端其他部分服务的 Pod 可以保持其常规水平。这确保了最佳的云和托管成本。

自动化和 DevOps 文化

强大的自动化和 DevOps 文化对于微前端架构的长期成功至关重要。

如您所想象,在微前端中,由于我们将单个应用拆分成更小的应用,与编译应用和运行质量、性能和安全检查等任务相关的所有活动现在都需要为每个应用多次执行。如果我们没有上述所有项目的自动化流程,那么这些应用的总体开发和发布将比单体应用所需的时间长得多。

因此,投入时间和精力构建这些自动化流程非常重要,这些流程通常作为 DevOps 管道的一部分完成。

团队还可以投资于工具和构建代码生成器和微应用模板,这有助于加快新微应用的创建速度。他们还可以在 DevOps 管道中自动运行 linters、安全和其他质量检查。

因此,我们来到了本节的结尾,其中我们看到了团队在设计微前端架构时必须牢记的一些重要原则。

我们看到了诸如领域驱动团队、独立部署和细粒度扩展等原则如何使团队能够持续快速地移动。我们看到了团队应避免陷入分布式单体陷阱,并构建一个使用构建时集成的模式,最后,我们看到了保持架构技术中立并专注于自动化如何帮助架构轻松演进并成为未来证明。

在下一节中,我们将探讨微前端架构的一些重要组件。

微前端架构的关键组件

在花时间研究微前端的原则之后,现在让我们来看看微前端架构的一些关键组件。

在本节中,我们将探讨任何微前端架构都需要具备的基本组件,并探讨与它们相关的细微差别。

完成本节后,你将了解构成任何微前端架构的四个基本组件。

路由引擎

正如我们在上一章中看到的,根据你打算构建的微前端模式类型,你的应用的路由引擎将与你的应用部分或完全解耦。

我们可以采取多种方法。我们可以使用 NGINX 作为反向代理,并有一个所有主要路由映射到多 SPA 模式中相应应用的列表。如果应用部署在 Kubernetes 集群中,我们可以使用入口路由将主要路由映射到相应应用。我们将在第八章“将微前端部署到 Kubernetes”中更详细地介绍这一点,我们将探讨在云中部署这些微前端。

全局状态和通信通道

除了路由之外,在设计微前端架构时,还需要很好地设计不同应用之间的通信通道以及全局状态的概念,这些状态可以在不同应用之间共享。

对于单体 SPA,最常用的做法是使用单个全局存储库,如 Redux 或 MobX,其中所有内容都写入该存储库并从中读取。对于微前端,建议避免使用此类全局客户端存储库,而是让每个微应用从后端 API 获取其数据,因为这才是真正的真相来源。

然而,为了避免对后端进行不必要的调用,例如获取user_id或购物车数量,确实需要客户端状态管理。对于这些事情,我们可以在应用外壳中使用一个非常薄的全球存储库,或者甚至可以考虑使用localStorageIndexedDB来存储需要用于 API 调用的值。

使用微前端模式,建立不同应用之间使用的通用通信通道也变得非常重要。一个典型的用例是在产品页面上点击添加到购物车按钮时,页眉中显示的迷你购物车会自动增加。在这种情况下,事件驱动的通信通道效果最佳。

源代码版本控制

团队需要达成共识的另一个重要事项是他们计划如何组织他们的 Git 仓库。这里有两种主要的观点——在多仓库或单仓库中组织应用。让我们看看它们的细微差别。

多仓库

多代码库(Polyrepos)是指每个多 SPA 或微应用都管理在自己的独立 Git 仓库中。这些仓库最容易开始使用,并且提供了完整的团队独立性。从 DevOps 的角度来看,它们也更容易管理。然而,这种方法有几个缺点。团队之间可能会出现孤岛化,协作减少。另一个缺点是工具(如 DevOps 管道和自动化脚本)的重复和更高的维护成本,这些工具需要在每个仓库中重复和更新。

单一代码库

在单一代码库结构中,所有多 SPA 或微应用都位于单个 Git 仓库中,每个应用位于其自己的单独文件夹中。

单一代码库(Monorepos)正开始成为许多前端团队管理代码库的事实上的方法。单一代码库的主要优势是增强了团队协作,因为每个人都能看到其他团队的代码并提供有价值的反馈。工具和自动化脚本可以集中管理,这样一支团队所做的优化可以立即供其他团队跟进。单一代码库的一些缺点包括 DevOps 设置稍微复杂一些。团队还需要设置细粒度的文件夹级权限,以防止团队之间相互覆盖代码。从大局来看,单一代码库提供了更多优势,因此是管理微前端源代码的首选方法。

组件库

当构建微前端时,确保用户在浏览不同应用时保持一致的视觉和触感体验至关重要。我们实现这一目标的方式是确保所有应用都使用一个通用的设计系统和组件库。还建议所有团队使用一个通用的主题和样式引擎,以确保所有组件无论在哪个应用中呈现,外观和行为都保持一致。

一种常见的模式是将组件库作为 NPM 模块发布,并将所有其他应用设置为导入和使用它。每次发布组件库的新版本时,团队都需要将各自的应用更新到最新版本。

由于单一代码库的出现,一个新兴的趋势是直接从源代码构建。这意味着组件库存储在单一代码库的libs部分,组件直接从库路径链接。这种方法的主要优势是每次团队构建应用时,都会自动接收到组件库的最新版本。

在本节中,我们学习了微前端架构的关键组件,即路由引擎、全局状态和通信通道。我们还看到了多代码库(polyrepo)和单一代码库(monorepo)之间的区别,并了解了为什么前端团队更喜欢使用单一代码库。最后,我们还学习了组件库以及团队从通用库中消费组件的不同方式。

摘要

有了这些,我们就结束了第二章的内容。我们以审视我们需要牢记的关键原则开始这一章。我们看到了为什么基于领域模型拆分团队是重要的,以及为什么团队能够独立部署自己的应用是至关重要的。我们还了解了与代码复用相关的误解以及它如何导致分布式单体陷阱。我们还看到了 DevOps 和自动化文化的重要性。最后,我们学习了微前端的核心四个组件。在本章中学到的所有内容,我们将在接下来的章节中付诸实践,因为我们着手构建我们自己的微前端应用程序。

在下一章中,我们将更深入地探讨单仓库与多仓库之间的区别,并了解这更多是关于团队文化而非技术。我们还将从设置我们的代码仓库作为单仓库开始,为未来的工作打下基础。

第三章:微前端的单仓库与多仓库对比

自从谷歌和 Facebook 的工程师提到他们在组织中有一个单仓库以来,开发者社区——尤其是前端社区——一直在积极参与关于单仓库与多仓库的辩论和讨论。

我们看到越来越多的团队倾向于使用单仓库来维护他们的前端代码。然而,根据社区的看法,您应该在多仓库和单仓库之间做出选择?

正如我们将在本章中学习的那样,选择单仓库或多仓库的决定远不止是花哨的技术或炒作。我们将看到,实际上,这更多与团队以及我们希望在团队内部建立的文化有关。

在本章中,我们首先了解多仓库和单仓库是什么。我们将看到它们如何影响团队的工作和协作,然后我们将看到为什么单仓库更适合微前端。最后,我们将设置我们的单仓库基础应用程序,并分配必要的权限以便团队工作。

在本章中,我们将涵盖以下主题:

  • 仓库类型及其细微差别

  • 为什么单仓库适合微前端?

  • 设置我们的单仓库并分配团队权限

到本章结束时,您将对选择多仓库与单仓库之间的差异及其影响有深入的理解。

我们还将设置好单仓库并使其准备好,以便垂直切片的领域驱动型团队可以开始使用它。

技术要求

在我们浏览本章中的代码示例时,我们需要以下内容:

  • 至少 8 GB RAM 的 PC、Mac 或 Linux 台式机或笔记本电脑(16 GB 更佳)

  • 英特尔芯片组 i5+、AMD 或 Mac M1+芯片组

  • 至少 256 GB 的空闲硬盘存储空间

您还需要在您的计算机上安装以下软件:

  • Node.js 版本 16+(如果需要管理不同版本的 Node.js,请使用nvm

  • 终端:iTerm2 与 Oh My Zsh(您会感谢我的)

  • 集成开发环境(IDE):我们强烈推荐 VS Code,因为我们将会使用一些 VS Code 附带的插件来提升开发者体验

  • npm、yarn 或 pnpm——我们推荐 pnpm,因为它速度快且存储效率高

  • 浏览器:Chrome、Microsoft Edge、Brave 或 Firefox(我使用 Firefox)

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Building-Micro-Frontends-with-React

我们还假设您对 Git 有基本的操作知识,例如分支、提交代码和发起拉取请求。

仓库类型及其细微差别

在本节中,我们将确切了解多仓库和单仓库是什么。

如你们大多数人现在可能已经知道的那样,repo 是 repository 的简称,指的是存储你项目所有文件的地方。它还会跟踪这些文件的所有更改。这意味着,在任何时候,我们都可以轻松地查看哪些代码行被更改了,由谁更改,以及何时更改。在大多数情况下,我们使用 Git 进行版本控制。有些团队可能使用其他系统,例如 Mercurial 或一些其他分布式版本控制系统。

团队最常用的两种管理仓库的策略是 monorepos 和 polyrepos。还有其他模式,如 Git 子模块或 Git 子树,但这些超出了本章的范围。我们将专注于 monorepos 和 polyrepos。

Monorepos

如其名所示,mono 意味着单一,因此源代码是在单个 Git repo 中管理的。这意味着所有团队成员都在一个共同的单一仓库上工作,在大多数情况下,monorepo 将包含多个应用程序。以下图显示了 monorepo 的设置:

图 3.1 – Monorepo 设置

图 3.1 – Monorepo 设置

如您在前面的图中所见,我们有一个包含多个应用程序的单个仓库。所有应用程序都使用共享的工具集进行 CI/CD、代码审查和构建共享组件库,该库通常在构建应用程序时从源代码构建。您还会注意到,所有团队都可以访问仓库中的所有项目。

Polyrepos

Polyrepos 是指每个应用程序都有自己的仓库。团队通常在多个仓库上工作,当他们处理不同的应用程序时,会切换仓库。

大多数团队更喜欢选择 polyrepos 路线,因为它们更容易管理,每个团队都可以定义自己的分支策略和仓库权限。以下图显示了 polyrepo 的设置:

图 3.2 – Polyrepo 设置

图 3.2 – Polyrepo 设置

在 polyrepo 设置中,您会注意到多个仓库,由虚线框表示。每个应用程序通常都有自己的仓库,我们还有一个用于共享组件和库的仓库。共享组件需要首先发布到如 npm 或 Nexus 这样的工件仓库,然后其他仓库才能使用它们。我们还注意到,每个仓库都有自己的团队,通常,团队没有访问其他团队的仓库(除非你是管理员或负责多个应用程序的高级开发者)。

Polyrepos 和 Monorepos 之间的区别

如前所述,选择 polyrepo 或 monorepo 不仅仅关乎代码的组织方式,它的影响要深远得多,对团队的协作方式、团队文化、构建工具的设置等方面都有巨大的影响。

在本节中,让我们更深入地了解 monorepos 和 polyrepos 的细微差别。

团队协作

在多仓库中,团队创建自己的独立仓库,并决定和定义自己的规则和指南来维护代码。显然,这是开始的最简单和最快的方式,团队可以很快地变得高效。然而,这种模式也有一些缺点。在多仓库中,团队往往会变得更加封闭,因为每个团队只关注自己的仓库,并没有真正看到其他团队在做什么。

多仓库的另一个缺点是设置和维护所有构建管道、预提交钩子等所需的工作量,并且每个仓库都会重复。

在单仓库(monorepo)中,团队被迫进行协作,因为他们需要就代码维护的通用方式达成一致。在单仓库设置中,由于每个人都能看到其他人的代码,他们各自为政的可能性大大降低。团队通过提供代码反馈自然地被鼓励进行协作,这也为团队复制其他团队可能已实施的优秀代码模式提供了机会。

构建工具和质量门

使用多仓库(polyrepos)时,每个团队都需要实现自己的构建系统和质量门,例如预提交钩子。这导致了工作重复,并导致维护成本增加。这也反映出了每个团队的工程成熟度。有强大领导的团队显然会有优化的构建工具,而初级团队可能会在不太优化的构建工具和质量门上遇到困难,并需要其他团队的帮助。

在单仓库中,所有构建工具和质量门都可以集中管理,从而减少工作重复。在大多数情况下,这通常由一个团队中的专家设置。这允许利用所有团队的优势和技能集,并且团队可以立即从整个组织内的知识中受益。

代码所有权

在多仓库中,权限是在仓库级别设置的,即谁有权限查看仓库中的代码或对其进行更改。

在单仓库中,所有团队成员都有权查看和编辑代码中的所有文件。在本章后面的内容中,单仓库中的权限和控制通过CODEOWNERS文件来维护。

单仓库的心理模型是团队中的每个人都可以更改文件并提交合并请求;然而,只有CODEOWNERS文件中定义的合法所有者才有权接受或拒绝团队成员所做的更改。

灵活性

如此明显的,多仓库在如何管理每个团队内部的代码方面提供了最高级别的灵活性。

在单仓库中,这种灵活性有意被限制,以确保所有团队成员都能从团队可以提供的最佳编码实践和工具设置中受益。

代码重构

使用多仓库时,跨多个仓库重构代码可能会很耗时,因为需要检出所有不同的仓库,并为每个仓库分别提交合并请求或拉取请求。

使用单仓库,通过创建原子提交,可以轻松地进行大规模的重构,其中单个合并请求可以包含所有应用程序所需的必要更改。

升级所有权

当需要升级库或工具时,多仓库和单仓库设置之间最有趣的不同之处就会显现出来。

在多仓库中,升级共享库或工具的责任落在每个团队身上,并且如果团队有其他优先事项,可以选择推迟升级。这既有好的一面也有不好的一面。虽然这允许团队根据自己的节奏进行升级,但总有一些团队可能会在升级库方面落后很远。如果过时的库存在严重的安全漏洞,而团队忽略了升级它,这就会成为一个严重的问题。由于每个团队负责升级库,因此他们也有责任修复破坏性变更,而这通常是推迟升级的主要触发因素。

使用单仓库,如果正在升级共享库或工具,则可以轻松地在单仓库内的所有应用程序中进行原子提交,这意味着所有团队都可以直接获得最新版本的益处。有趣的是,对于单仓库(拥有适当的构建工具和质量门控机制)来说,修复任何破坏性变更的责任在于库所有者或进行升级的人员,因为构建管道不会允许你合并代码,除非它通过了所有构建步骤和质量门控。

代码库大小

使用多仓库时,代码库会随着时间的推移逐渐增加;然而,使用单仓库,你从第一天开始就要处理大型代码库,并且随着应用程序的增长,单仓库往往会呈指数级增长。

大型代码库对生产力有负面影响。不仅检出代码需要时间,而且所有其他活动,如运行构建步骤或运行单元测试,在本地开发者的 PC 上以及 CI/CD 管道上都需要更长的时间。

除非使用缓存和仅构建和测试已更改的内容等特性,否则单仓库可能会变得非常慢。

当我们到达本节的结尾时,我们已经了解了多仓库和单仓库之间的区别,并深入探讨了它们在代码重构、所有权、工具团队文化、协作等方面如何不同。

在下一节中,我们将看到哪一种更适合构建微前端。

为微前端选择单仓库

在经过多仓库和单仓库的优缺点分析后,你会选择哪一个用于你的项目?嗯,你可以选择其中的任何一个来构建微前端。就像编程中的所有事情一样,每个决策都有权衡,你需要清楚你愿意接受哪些权衡。

在本书的剩余部分,我们将选择使用单仓库设置,以下是一些原因:

  • 使用单仓库,团队成员自然会通过学习和审查彼此的代码来鼓励协作。

  • 它允许所有团队轻松使用共享的组件库。这确保了作为整体应用一部分构建的每个微应用都具有相同的视觉和感觉,并且整体用户体验在用户与不同微应用交互时保持一致。

  • 它还使得中央平台团队,如 DevOps 团队或管理员团队,能够轻松地对所有微应用进行代码重构。

  • 单仓库的一些缺点,如管道上质量门执行速度较慢,可以通过使用缓存技术来克服,其中许多是大多数单仓库工具的默认设置。

  • 随着整体应用的成长和新功能的添加,新的微应用将不断添加到你的应用中。现在,如果你有一个每个微应用都有自己的仓库的多仓库设置,管理大量仓库将变得相当困难。

  • 在微前端设置中,大多数时候,你会专注于你的单个微应用;然而,有时你需要一起运行所有微应用来在本地测试你的应用。如果你的微应用是在多仓库中设置的,这将相当难以实现。

在下一节中,我们将探讨一些流行的开源单仓库工具,这将帮助你决定哪一个最适合你。

流行的单仓库工具

本节涵盖了在构建微前端时可以选择的一些最受欢迎的开源单仓库工具。

Lerna

Lerna 可能是第一个也是最广泛使用的单仓库工具。它遵循所谓的基于包的单仓库风格。这基本上意味着每个应用都位于packages文件夹下,并有自己的package.json文件,因此每个应用都有自己的依赖项集合,这些应用之间没有共同点。

Lerna 最近被 nrwl 团队采用,该团队最初构建了 Nx 单仓库。

Nx

Nx 是下一个变得非常流行的单仓库,可能是所有单仓库工具中最成熟和功能丰富的。Nx 最初是一个集成单仓库。这意味着在 Nx 中,根目录下有一个单一的package.json文件,所有应用都使用相同版本的包。Nx 现在已经发展到也支持基于包的单仓库风格。

它配备了先进的本地和分布式缓存解决方案,非常适合管理大型单仓库代码库。

Turborepo

Turborepo 是单仓库竞赛中的最新参与者。它遵循基于包的样式,并且与 Lerna 的工作方式非常相似。Turborepo 的主要优势是它支持本地和分布式缓存系统,并且与 Vercel 的产品套件紧密集成,包括 Next.js 和 Vercel 云托管。

当我们进入本节的尾声时,我们已经了解了多仓库与单仓库的优缺点。我们看到了选择使用单仓库进行微前端的一些原因,我们还了解了一些团队使用的流行单仓库工具。在下一节中,我们将着手设置我们的 monorepo。

设置我们的 Monorepo

在本节中,我们将设置我们的 monorepo,它将作为我们的微前端应用程序的基础。我们将学习如何设置正确的权限和必要的质量门。在这个过程中,我们还将了解一些提高开发者体验的生产力技巧和插件。

对于本例和本章的其余部分,我们将使用 Nx 作为 monorepo 来构建我们的微前端,因为它允许你构建包设置样式和集成样式 monorepo。你也可以选择 Lerna 或 Turborepo 来构建你的微前端。

按照以下分步指南设置 Nx monorepo:

  1. 打开终端,cd 到你通常存放项目的文件夹,并运行以下命令:

    org name). This will be the name of the folder within which your monorepo will be set up. We will call it my-mfe for the sake of consistency.
    
  2. 接下来,它将提示你选择你想要创建的应用类型。我们将选择 react

图 3.3 – 选择一个包含单个 React 应用的工作区

图 3.3 – 选择一个包含单个 React 应用的工作区

  1. 当提示输入应用程序名称时,输入 catalog,因为这将是我们的微前端中的目录应用程序。

  2. 当提示选择样式表格式时,你可以选择默认的 CSS 或任何你喜欢的其他格式。

  3. 接下来,它将提示你启用分布式缓存。对于这个练习,我们将说 No

重要提示

你可以在 nx.dev/getting-started/intro 找到设置 NX 的完整细节。

然后,它将继续安装所有依赖项,一旦成功完成,你应该有一个类似于 图 3.4 的文件夹结构:

图 3.4 – 我们 monorepo 的文件夹结构

图 3.4 – 我们 monorepo 的文件夹结构

你会注意到它已经创建了一个名为 my-mfe 的 monorepo 和 apps 文件夹中的一个名为 catalog 的应用程序。

  1. 勇敢地打开这个文件夹在 Visual Studio Code 中,一旦你这样做,你将得到一个提示来安装推荐的插件。请继续安装推荐的插件。

一旦所有插件都已安装,你将在 VS Code 面板中注意到一个新的图标,如这里所示:

图 3.5 – VS Code 上安装的 Nx Console

图 3.5 – VS Code 上安装的 Nx Console

Nx Console 是使用 Nx 的最酷特性之一,我们将在本书的剩余部分广泛使用它。

对于那些好奇如何出现安装推荐插件的弹出窗口的人来说,答案在于 my-mfe/.vscode/extensions.json 文件。

这是一个 VS Code 功能,你可以在这里了解它:code.visualstudio.com/docs/editor/extension-marketplace#_workspace-recommended-extensions

你可以使用此文件添加你希望团队成员使用的推荐插件列表。

这是一种让团队标准化插件并帮助初级开发者更快地提高生产力的简单方法,而无需他们通过艰难的方式学习。

你还会注意到 Nx 还创建了一些其他文件,例如 eslintrc.json.prettierrc.editorconfig 等等。所有这些文件都有助于为编写良好的代码打下良好的基础,并确保代码在缩进、单引号与双引号的使用等方面的一致性。

在本地运行应用

要在本地运行应用,我们始终可以运行终端命令,但为了更好的开发者体验,我们将使用我们之前提到的自动添加的 Nx Console 扩展。

点击 Nx Console 图标,然后,在 生成 & 运行目标 下选择 serve,然后从顶部下拉菜单中选择 catalog 应用,然后选择 执行:nx run catalog:serve

图 3.6 – 使用 Nx Console 提供目录应用

图 3.6 – 使用 Nx Console 提供目录应用

你会发现它实际上在终端中运行了 pnpm exec nx serve catalog,几秒钟后,你将在 http://localhost:4200 上运行 catalog 应用。

在浏览器中打开链接,感受一下新创建的目录应用:

图 3.7 – 目录应用在端口 4200 上运行

图 3.7 – 目录应用在端口 4200 上运行

使用 Nx Console 创建新应用

接下来,让我们创建另一个新应用。按照以下步骤操作:

  1. 前往 Nx Console,从 生成 & 运行目标 中选择 生成 命令。然后,从下拉菜单中选择 创建 React 应用程序。在接下来的屏幕上,当询问应用程序名称时,输入 checkout

  2. 当你向下滚动到 e2eTestRunner 部分的表单时,选择无。这将确保不会创建 checkout-e2e 文件夹。

重要提示

注意,当你填写表单字段时,Nx 实际上在终端中进行了干运行,以显示输出将是什么样子。

  1. 前往并点击 monorepo 中的 apps 文件夹。

  2. 使用 Nx Console,继续提供 checkout 应用。在 nx serve checkout 屏幕上,向下滚动一点,选择 端口 并输入 4201,然后选择 执行:nx run checkout:serve –port=4201 以在端口 4201 上运行 checkout 应用。

我们可以遵循相同的步骤来创建额外的应用程序。Nx 附带了一套核心和社区插件,这允许您使用不同的框架创建应用程序,例如 Angular、Next.js、Vue 等。您可以在以下位置查看可用的插件完整列表:nx.dev/community

在您的单一代码仓库中设置权限

现在我们单一代码仓库中有多个应用程序,并且假设每个应用程序都有独立团队在开发,接下来出现的问题是如何确保正确的权限,以确保团队不会意外更改其他团队的代码。

如我们之前所见,单一代码仓库的一般思路是,所有有权访问仓库的人员都有权访问单一代码仓库中的所有应用程序和文件夹,但他们不能合并他们不拥有的应用程序的代码更改。

在单一代码仓库中,权限是在文件夹级别设置的,并且通过使用CODEOWNERS文件来实现。您可以在以下位置详细了解CODEOWNERSdocs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners

重要提示

CODEOWNERS文件与 GitHub 和 GitLab 兼容。如果您使用 Azure DevOps,此功能通过所需的审批者功能实现:learn.microsoft.com/en-gb/azure/devops/repos/git/branch-policies?view=azure-devops&tabs=browser#automatically-include-code-reviewers

简而言之,CODEOWNERS文件允许我们确保个人或团队明确参与他们拥有的文件代码审查和变更批准。

我们可以通过两种方式分配文件:给定文件夹内的所有文件或特定类型的所有文件。

让我们看看实际操作。

在我们的单一代码仓库根目录下,创建一个名为CODEOWNERS的文件:

/apps/catalog @my-org/catalog-team
/apps/checkout @my-org/checkout-team

这意味着,如果任何拉取请求包含对apps/catalog文件夹内文件的修改,它将自动将目录团队的人员添加为拉取请求的审阅者,并且没有该团队的批准,拉取请求无法合并。

对于修改checkout文件夹内文件的拉取请求,同样适用。在这种情况下,它将需要 checkout 团队成员的明确批准。

我们还可以在CODEOWNERS文件中指定个人。假设我们想要确保对tools文件夹内文件的任何更改都需要 GitHub 用户@msadmin的批准。假设我们团队中有一位 CSS 专家,我们希望此人审查整个仓库中的所有 CSS 更改。我们可以添加以下两条规则来启用此功能:

/tools @msadmin
*.css @cssexpert

这样,我们可以确保对拉取请求进行精细的审批流程,确保所有更改的正确利益相关者都参与了他们负责的文件更改的审批。如您所见,这也允许您设置规则,以便个人在某个特定主题上的专业知识可以为整个团队的利益做出贡献。

在创建 CODEOWNERS 文件条目时,以下是一些需要注意的要点:

  • 文件路径在文件中是区分大小写的

  • 规则的优先级是从 CODEOWNERS 文件的底部到顶部;例如,如果有多个匹配规则,则最底部的行具有最高优先级

  • 如果某一行有语法错误,它将被跳过,GitHub 将简单地移动到下一行

为了测试这一点,请将带有 CODEOWNERS 文件条目的代码推送到 GitHub,修改该文件,并提交一个拉取请求以查看 CODEOWNERS 文件的实际应用。

到达本节末尾,我们学习了如何使用 Nx 初始化单仓库,如何在单仓库中创建应用,以及如何使用 NX Console 单独运行它们。我们还快速浏览了一些 Nx 提供的工具优势,它为初学者提供了非常好的开发者体验,同时也通过自动设置一些质量门为您的仓库提供了坚实的基础。最后,我们探讨了我们可以设置的各种权限,以允许开放协作并利用个别团队成员的优势,以整个团队的利益为出发点。

摘要

有了这些,我们就到了这一章的结尾,我们详细探讨了相当多的内容。我们看到了今天团队在版本控制代码库时,是如何在单仓库和多仓库方法之间进行选择的。

然后,我们详细探讨了选择多仓库或单仓库如何影响团队的操作方式,代码重构的难易程度,以及谁负责修复仓库中的破坏性更改。

然后,我们看到了为什么选择单仓库用于微前端有更多好处,例如,在单个仓库中管理所有微应用,尤其是在本地运行多个应用并集中管理单仓库中所有应用的工具时。

最后,我们着手设置了我们的单仓库,我们看到了使用像 Nx 这样的工具的好处,它为我们提供了预配置的质量门,如 ESLint 和 Prettier,以确保一致性和代码质量。我们还看到了如何使用 Nx Console 轻松创建新的微应用并运行现有的微应用。然后我们看到如何设置 CODEOWNERS 文件以确保对特定微应用的代码更改审批有细粒度控制。

在下一章中,我们将基于当前设置,着手创建一个完整的、多-SPA 模式微前端。

第二部分:微前端架构

这一部分探讨了实现微前端的各种架构模式,包括多 SPA、微应用、模块联邦和服务器端渲染方法。它提供了具体的示例,涵盖了微前端的路由和状态管理相关主题。

这一部分包含以下章节:

  • 第四章**,实现多 SPA 模式

  • 第五章**,实现微应用模式

  • 第六章**,服务器端渲染微前端

第四章:为微前端实现多 SPA 模式

假设你是一名建筑师,负责构建一个大型政府电子门户的前端,该门户为个人和企业提供众多在线服务。这些服务包括注册健康福利、提交所得税申报、注册小型企业以及支付车辆道路税,此外还有大量信息内容的发布。

或者,情景二,假设你被分配去构建一个提供多种在线服务的银行门户,从管理储蓄账户到购买保险,再到投资机会、贷款、抵押贷款、信用卡等等。

你会如何规划不仅你的架构,还包括将负责构建该架构的团队?自然,首先的思考层次是将大型门户分解成多个较小的模块或微应用,并让每个团队专注于其中一个微应用。

这将是正确的做法,这也是我们所说的构建微前端的多 SPA 模式。

在本章中,我们将构建我们的多 SPA 模式微前端,我们将探讨以下内容:

  • 多 SPA 微前端的高级架构

  • 在多 SPA 之间建立路由

  • 使用共享组件库

  • 设置持久状态以在微应用之间共享状态

技术要求

在我们浏览本章中的代码示例时,我们需要以下内容:

  • 至少 8 GB RAM 的 PC、Mac 或 Linux 桌面或笔记本电脑(16 GB 更佳)

  • 英特尔芯片组 i5+或 Mac M1+芯片组

  • 至少 256 GB 的空闲硬盘存储空间

你还需要在你的计算机上安装以下软件:

  • Node.js 版本 16+(如果你需要管理不同版本的 Node.js,请使用nvm

  • 终端:iTerm2 配合 OhMyZsh(你以后会感谢我的)

  • IDE:我们强烈推荐 VS Code,因为我们将会使用一些 VS Code 内置的插件来提升开发者体验

  • npm、yarn 或 pnpm – 我们推荐 pnpm,因为它速度快且存储效率高

  • 浏览器:Chrome、Microsoft Edge 或 Firefox(我使用 Firefox)

  • 对 Nx.dev 单一代码库的基本理解,以及对在 VS Code 中使用 NX 控制台插件的基本理解

  • 熟练掌握 React

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Building-Micro-Frontends-with-React

我们还假设你对 Git 有基本的操作知识,例如分支、提交代码和发起拉取请求。

理解多 SPA 架构

多 SPA 架构模式是构建大型应用程序中最常见的模式之一。正如其名所示,在这个模式中,我们有一组 SPA,它们共同构成一个大型应用程序。在这个模式中,每个 SPA 都作为其自己的独立功能或模块运行,可以通过命名空间并映射到应用程序的 URL 直接访问。这些 SPA 还共享一个非常薄的共享组件和全局状态层,以确保应用程序之间的连贯性和一致性。

图 4.1 – 多 SPA 架构

图 4.1 – 多 SPA 架构

如您在 图 4**.1 中所见,我们有四个 SPA:一个目录,将包含产品列表、产品详情、搜索等页面;一个包含购物车、支付等页面的结账 SPA;我的账户 SPA;以及卖家/管理员 SPA。您还会注意到这种模式允许我们轻松地随着应用程序的增长添加额外的 SPA。

每个这些 SPA 都映射到一个唯一的初级 URL,这样用户点击 /catalog URL 将会被重定向到目录应用程序,而用户点击 /checkout URL 将会进入结账应用程序。

构建我们的多 SPA 微前端

构建一个多 SPA 微前端基本上包括三个广泛领域:将应用程序分解成逻辑上的小程序,然后我们需要在这些小程序之间设置路由,最后,我们设置一个全局状态,不同的小程序可以从中读取和写入数据。让我们在接下来的章节中逐一查看。

设置我们的小程序

我们将从上一章结束的地方开始。

如果您跳过了上一章并直接跳到这里,您可以从克隆 github.com/PacktPublishing/Building-Micro-Frontends-with-React/tree/main/ch3/my-mfe 的存储库开始。

让我们快速运行 pnpm install(如果您还没有这样做的话)并服务相应的应用程序,以确保它们运行正常。

由于我们打算构建一个电子商务应用程序,让我们把我们的应用程序命名为 eBuy。请随意重命名您的应用程序文件夹为 ebuy

在积极开发期间,我们理想上是在自己的小程序上工作,您可以使用 NX 控制台轻松地服务您的小程序。

然而,您可能需要定期测试跨越不同小程序的整个端到端应用程序流程,为此,您能够在本地上运行所有小程序非常重要。这正是我们接下来要做的。

我们首先需要确保每个小程序运行在其自己的唯一端口上。为此,我们需要首先找到位于 apps/catalog 文件夹中的 project.json 文件。您会注意到它基本上包含运行您应用程序上各种任务所需的所有命令和配置。

我们导航到 "serve" 部分,并在 "options" 下添加行 "``port": 4200

    "serve": {
      "executor": "@nrwl/web:dev-server",
      "defaultConfiguration": "development",
      "options": {
        "buildTarget": "catalog:build",
        "hmr": true,
        "port": 4200
      },
      "configurations": {
        "development": {
          "buildTarget": "catalog:build:development",
        },
        "production": {
          "buildTarget": "catalog:build:production",
          "hmr": false
        }
      }
    },

我们在 apps/checkout 文件夹中的 project.json 文件中也做同样的事情,但这次我们将确保它在 "port": 4201 上运行,如下所示:

      "options": {
        "buildTarget": "checkout:build",
        "hmr": true,
        "port": 4201
      },

这样可以确保,默认情况下,目录将在端口 4200 上运行,而结账应用将在端口 4201 上运行。

多亏了继承,我们可以在相同的端口上以开发和生产模式运行应用。

接下来,我们将创建一个脚本命令,允许我们在各自的端口上并行运行所有应用。

为了做到这一点,我们进入项目根目录下的 package.json 文件,并添加一个名为 "serve:all": "nx run-many --target=serve" 的脚本:

  "scripts": {
    "start": "nx serve",
    "build": "nx build",
    "test": "nx test",
    "serve:all": "nx run-many --target=serve"
  },

现在,在你的终端中运行以下命令:

pnpm serve:all

你将看到 nx 正在启动 webpack 开发服务器并启动两个应用。

通过访问这些两个 URL 在浏览器中验证它:

在微前端中,每个 SPA 遵循相同的品牌指南和外观感觉非常重要。我们通过构建一个共享的 UI 组件集来实现这一点,这两个应用都会使用这些组件。在下一节中,我们将看到如何创建共享组件库。

使用共享组件库

由于你正在构建一系列作为你整体更大应用一部分的迷你应用,我们希望确保所有这些迷你应用都有一致的设计——例如,拥有一致的头尾和组件行为的一致方式。同样重要的是,当我们对一些核心元素进行更改时,我们需要确保它可以在所有不同的应用中轻松更新。这正是 libs 文件夹发挥作用的地方。

这也是一个定义 NPM 范围的好时机,以便所有这些共享组件都可以通过它们的范围名称导入。

要定义 NPM 范围,我们打开位于单仓库根目录下的 nx.json 文件。我们将命名我们的范围为 ebuy,但实际上,它可以是任何名称——你团队的名称,你为组件库取的名称,等等。

nx.json 文件中定位 npmScope 属性,并按以下方式更新:

  "npmScope": "ebuy",

让我们使用我们可信赖的 Nx 控制台来创建一个库。从 Nx 控制台,选择 generate,然后选择 @nrwl/react – library React Library

选择 显示所有选项,提供/修改以下详细信息,其余的保持默认:

Library name   : ui
Generate a default component    : No
importPath : @ebuy/ui

我们可以将其余的保持默认,并点击运行按钮以在 libs 中生成 ui 文件夹。

除了在 libs 中创建 ui 文件夹外,你还会注意到 Nx 还在 tsconfig.base.jsonpaths 对象中添加了一个条目,如下所示:

    "paths": {
      "@ebuy/ui": ["libs/ui/src/index.ts"]
    }

正是这个设置将允许我们通过范围名称而不是长文件夹路径导入我们的 UI 组件。

接下来,让我们创建一些 UI 组件。

我们将使用令人惊叹的 Semantic-UI React 组件库来构建我们的 UI 组件。您也可以使用任何其他组件库,例如 Chakra UI、MUI React-Bootstrap 等:

  1. 让我们使用以下命令在单一代码仓库的根目录下安装它:

    pnpm install semantic-ui-react semantic-ui-css
    
  2. 记住您始终可以使用 npmyarn 安装 npm 包,如下所示:

    yarn add semantic-ui-react semantic-ui-css
    npm install semantic-ui-react semantic-ui-css
    

    现在让我们在 libs/ui 文件夹中创建一些我们的常用组件。

  3. 让我们使用 Nx 控制台创建一个新的组件:

    Nx | 生成 | 创建一个 react 组件

  4. 使用以下信息创建组件:

    • 名称header

    • 项目ui

    • 扁平化:选择复选框以确保我们在内部拥有更扁平的文件夹结构。

  5. 点击运行按钮并验证 header.tsx 文件是否在 libs/ui/src/lib 文件夹中创建。

  6. 打开 header.tsx 文件,并用我们头部组件的简单标记替换其内容:

    import { Menu, Container, Icon, Label } from 'semantic-ui-react';
    export function Header() {
      return (
        <Menu fixed="top" inverted>
          <Container>
            <Menu.Item as="a" header>
              eBuy.com
            </Menu.Item>
            <MenuItems />
            <Menu.Item position="right">
              <Label>
                <Icon name="shopping cart" />
                00
              </Label>
            </Menu.Item>
          </Container>
        </Menu>
      );
    }
    const MenuItems = () => {
      return (
        <>
          {NAV_ITEMS.map((navItem, index) => (
            <Menu.Item key={index}>
              <a href={navItem.href ?? '#'}>{navItem.label}</a>
            </Menu.Item>
          ))}
        </>
      );
    };
    interface NavItem {
      label: string;
      href?: string;
    }
    const NAV_ITEMS: Array<NavItem> = [
      {
        label: 'Catalog',
        href: '/',
      },
      {
        label: 'Checkout',
        href: '/checkout',
      },
    ];
    export default Header;
    

    这是一段简单的 React 组件代码,它将显示目录和检查的导航头部。

  7. 下一步是将它从 ui 中导出。定位到 /libs/ui/src/index.ts 文件,并添加以下条目:

    export * from './lib/header';
    

    这将允许我们的头部组件可以通过较短的导入路径进行导入。现在让我们将其导入到我们的目录和检查应用中。

  8. 打开 apps/catalog/src/spp/app.tsx 文件,并按如下方式导入头部组件:

    import { Header } from '@ebuy/ui';
    
  9. 让我们清理一些样板代码。移除对 stylesNxWelcome 的导入,并在 JSX 中添加 Header 组件。您还可以删除 catalog 文件夹中的 nx-welcome.tsx 文件。您的最终代码应如下所示:

    import { Header } from '@ebuy/ui';
    import { Container, Header as Text } from 'semantic-ui-react';
    import 'semantic-ui-css/semantic.min.css';
    export function App() {
      return (
        <Container style={{ marginTop: '5rem' }}>
          <Header />
          <Text size="huge">Catalog App</Text>
        </Container>
      );
    }
    export default App;
    

    在前面的代码中,我们导入了 semantic-ui 的 CSS 文件,并包含了显示应用名称的 Header 组件和文本。

    在浏览器中运行时,目录应用看起来可能如下所示:

图 4.2 – 带有通用头部菜单栏的目录应用

图 4.2 – 带有通用头部菜单栏的目录应用

  1. 我们将在检查应用中的 apps/checkout/src/app/app.tsx 文件内进行相同的更改。

  2. 让我们测试一下我们的代码。运行 pnpm serve:all 并在 http://localhost:4200 上刷新浏览器以查看我们的最新更改。

尝试点击目录或检查的导航链接,注意它没有任何反应。这是因为我们还没有在我们的应用之间设置路由,这正是我们接下来将要做的。

设置路由

正如我们之前讨论的,我们时不时地想要测试我们的端到端应用功能,尽管我们能够在不同的端口上并行运行应用,但在测试端到端功能方面存在一些挑战:

  • 我们需要确保我们的应用在本地和在生产环境中都拥有一致的导航结构。

  • 在不同端口上运行的应用被视为不同域上的应用,因此无法共享 cookies、会话状态等。

为了克服这些问题,我们需要让浏览器认为应用正在同一端口上运行。我们通过设置反向代理来实现这一点。我们将设置路由的方式是每个迷你应用都将有自己的命名空间主路由,例如:

  • eBuy.com: 首页应用

  • eBuy.com/catalog: 目录应用

  • eBuy.com/checkout: 结账应用

二级路由通常在迷你应用内部设置。例如,苹果的产品详情页面将是 eBuy.com/catalog/apples

Webpack 开发服务器和 Nx 提供了易于使用的代理支持,我们可以利用这些支持。

在目录应用的根目录 /apps/catalog 中,让我们创建一个名为 proxy.conf.json 的新文件,并包含以下条目:

{
  "/catalog": {
    "target": "http://localhost:4200"
  },
  "/checkout": {
    "target": "http://localhost:4201"
  }
}

接下来,我们需要告诉目录应用使用此文件进行其代理配置。

我们通过在 apps/catalog/project.json 文件中的 serve 对象下开发配置中添加 proxyConfig 属性来完成此操作,如下所示:

    "options": {
        "buildTarget": "catalog:build",
        "hmr": true,
        "port": 4200,
        "proxyConfig": "apps/catalog/proxy.conf.json"
      },

让我们快速测试一下。我们需要重新启动我们的开发服务器以获取最新的代理配置。

运行 serve:all 命令并尝试点击 结账目录 导航链接… 哎呀… 它没有工作,当你点击 结账 链接时,显示的是相同的目录应用… 但是等等 – 浏览器标签页上的标题标签确实显示 结账

图 4.3 – 标题中为结账应用但正在加载目录包

图 4.3 – 标题中为结账应用但正在加载目录包

那么,这里发生了什么?如果你查看开发工具,问题就变得非常明显。这里发生的情况是代理已正确地将我们重定向到结账应用,这就是为什么我们看到通过结账应用提供的正确 index.html 文件,然而,脚本中的 src 标签加载的 js 包指向根目录,因此它们实际上是从目录应用加载的 js 包。

由于 Nx 的帮助,解决这个问题相对容易。

我们只需要为结账应用定义 baseRef。我们通过在 /apps/checkout/project.json 文件中添加 "baseHref": "/checkout/" 来完成此操作。

这就是你的父 serve 对象下的开发对象应该看起来像这样:

     "options": {
          "buildTarget": "checkout:build:development",
          "port": 4201,
          "baseHref": "/checkout/"
        },

重新启动开发服务器,现在你将能够在两个应用之间导航,并正确加载 JS 包。在下一节中,我们将努力添加产品列表响应以模拟产品列表 API 调用的模拟响应。

设置模拟产品列表

在所有网络开发活动中,一个常见的做法是设置一个模拟服务器或一组模拟的 API 响应,直到实际的 API 准备好。由于我们的电子商务应用需要在所有其他迷你应用中使用的商品列表,我们创建了一个共享库来保存我们的模拟。

所以,再次使用我们最喜欢的 Nx Console,让我们创建另一个 React 库,让我们称它为 mocks,我们将使用作用域名称 @ebuy/mocks

mocks 库的 libs/mocks/src/lib 中,让我们创建一个名为 product-list-mocks.tsx 的文件,并包含以下代码:

interface productListItem {
  id: string;
  title: string;
  image: string;
  price: number;
}
export const PRODUCT_LIST_MOCKS: Array<productListItem> = [
  {
    id: '1',
    title: 'Apples',
    image: '/assets/apple.jpg',
    price: 1.99,
  },
  {
    id: '2',
    title: 'Oranges',
    image: '/assets/orange.jpg',
    price: 2.5,
  },
  {
    id: '3',
    title: 'Bananas',
    image: '/assets/banana.jpg',
    price: 0.7,
  },
];
export default PRODUCT_LIST_MOCKS;

让我们不要忘记从 /libs/mocks/src/index.ts 文件中导出它,以下是一行代码:

export * from './lib/product-list-mocks';

此外,别忘了将产品图片放在 catalog 应用程序的 src/assets 文件夹中。你可以在这里找到图片 github.com/PacktPublishing/Building-Micro-Frontends-with-React-18/tree/main/ch4/ebuy/apps/catalog/src/assets

我们现在将尝试在应用程序中使用它,无论何时我们需要从产品列表中获取数据。

添加产品网格和结账组件

现在我们有一个看起来不错的标题和一个应用程序,我们可以从一个迷你应用程序导航到另一个。然而,应用程序的其余部分并没有做什么,所以让我们向目录应用程序添加一个产品列表组件,并向结账应用程序添加一个购物篮组件。

我们将首先在 /apps/catalog/src/app 文件夹中创建 ProductList 组件。我们将文件命名为 product-list.tsx。我们将首先创建一个空壳组件:

import { Card } from 'semantic-ui-react';
import ProductCard from './product-card';
import { PRODUCT_LIST_MOCKS } from '@ebuy/mocks';
export function ProductList() {
  return (
    <Card.Group>
      {PRODUCT_LIST_MOCKS.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </Card.Group>
  );
}
export default ProductList;

我们将因为缺少 ProductCard 组件而得到一个错误。别担心 – 我们将在下一步创建该组件。接下来,我们需要创建我们的 ProductCard 组件。我们将文件命名为 product-card.tsx

我们首先定义我们的 ProductCard 组件的框架:

import { Button, Card, Image } from 'semantic-ui-react';
export function ProductCard(productData: any) {
  const { product } = productData;
  return (
    <Card>
      <Card.Content>
        <Image alt={product.title} src={product.image} />
        <Card.Header>{product.title}</Card.Header>
        <Card.Description>{product.description}</Card.Description>
        <Card.Header>${product.price}</Card.Header>
      </Card.Content>
      <Card.Content extra>
        <div className="ui three buttons">
          <Button basic color="red">
            Remove
          </Button>
          <Button basic color="blue">
            {0}
          </Button>
          <Button basic color="green">
            Add
          </Button>
        </div>
      </Card.Content>
    </Card>
  );
}
export default ProductCard;

接下来,让我们导入位于 /apps/catalog/src/app/app.tsx 的目录应用程序的 ProductList app.tsx 文件。

你的 app.tsx 代码现在应该看起来像这样:

import { Header } from '@ebuy/ui';
import { Container, Header as Text } from 'semantic-ui-react';
import 'semantic-ui-css/semantic.min.css';
import ProductList from './product-list';
export function App() {
  return (
    <Container style={{ marginTop: '5rem' }}>
      <Header />
      <Text size="huge">Catalog App</Text>
      <ProductList />
    </Container>
  );
}
export default App;

如果你的目录应用程序看起来像下面的截图,那么这意味着你正在正确的道路上:

图 4.4 – 带有标题和产品列表组件的目录应用程序

图 4.4 – 带有标题和产品列表组件的目录应用程序

接下来,我们将创建我们的购物篮组件。因此,在我们的 /apps/checkout/src/app 文件夹中的 app.tsx 结账文件中,让我们创建以下代码的基本框架:

import { Header } from '@ebuy/ui';
import { Container, Header as Text } from 'semantic-ui-react';
import 'semantic-ui-css/semantic.min.css';
import ShoppingBasket from './basket';
import { PRODUCT_LIST_MOCKS } from '@ebuy/mocks';
export function App() {
  return (
    <Container style={{ marginTop: '5rem' }}>
      <Header />
      <Text size="huge">Checkout</Text>
      <ShoppingBasket basketList={PRODUCT_LIST_MOCKS} />
    </Container>
  );
}
export default App;

这段代码现在应该看起来很熟悉了。正如你所见,我们有一个 ShoppingBasket 组件,并且目前我们正在将其传递给 PRODUCT_LIST_MOCKS 以进行模拟。

接下来,我们要创建那个正在抛出错误的 ShoppingBasket 组件。

因此,让我们在 /apps/checkout/src/ 应用程序文件夹中创建一个名为 basket.tsx 的文件:

import { Table, Image, Container } from 'semantic-ui-react';
export function ShoppingBasket(basketListData: any) {
  const { basketList } = basketListData;
  return (
    <Container textAlign="center">
      <Table basic="very" rowed>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Items</Table.HeaderCell>
            <Table.HeaderCell>Amount</Table.HeaderCell>
            <Table.HeaderCell>Quantity</Table.HeaderCell>
            <Table.HeaderCell>Price</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {basketList.map((basketItem: any) => (
            <Table.Row key={basketItem.id}
>
              <Table.Cell>
                <Image src={basketItem.image} rounded size="mini" />
              </Table.Cell>
              <Table.Cell> {basketItem.title}</Table.Cell>
              <Table.Cell>{basketItem.quantity || 0}</Table.Cell>
              <Table.Cell>${basketItem.price * basketItem.quantity}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </Container>
  );
}
export default ShoppingBasket;

这一切都是自解释的占位符标记内容,目前并没有做什么。在接下来的章节中,我们将使所有这些内容协同工作。

你正在运行的结账应用程序现在应该看起来像这样:

图 4.5 – 模拟的结账应用程序

图 4.5 – 模拟的结账应用程序

这样,我们的两个应用就能良好地工作并显示正确的数据,然而,它们还没有“交谈”。结账应用不知道用户在目录应用中添加了哪些商品。在下一节中,我们将设置一个全局共享状态,这两个迷你应用都可以与之交谈并读取。

在我们进入下一节之前,让我们快速回顾一下到目前为止我们已经完成的事项清单:

  • 确保目录应用和结账应用在不同的端口上运行

  • 确保我们在proxy.conf.json文件中设置了 URL 路由

  • 我们两个应用都在从模拟的产品列表中读取数据

设置全局共享状态

现在我们能够在两个迷你应用之间导航,接下来要解决的是在这两个不同的应用之间设置共享状态。因为这些是两个独立的应用,通常的状态管理解决方案,如 Context API、Redux、MobX 等,将不起作用。这是因为这些库将状态作为应用内的一个对象存储,当你刷新页面或导航到另一个应用时,这个状态就会丢失。因此,为了克服这个问题,我们求助于使用浏览器的一些原生功能,如本地存储、会话存储或 Index-db。

对于这个例子,我们将使用会话存储。我们将设置一个简单的自定义钩子来在sessionStorage中持久化状态,并让我们的迷你应用都能读取和写入这个状态。

在任何大型应用中,都会有大量的类似自定义钩子,团队可以重用。这也是我们为这些自定义钩子设置另一个库的好机会。

重要的是要记住,这个全局状态应该仅在我们需要在不同的迷你应用之间共享信息时才少量使用。为了管理每个微应用内的状态,我们应该使用常规的状态管理工具,例如 Context API 或 Redux 等。

让我们使用 Nx 控制台创建另一个名为custom-hooks的库:

Nx Console > generate > Create a React Library

然后,我们将在表单中填写以下信息:

  • 名称custom-hooks

  • 组件off(生成默认组件)

  • 导入路径@ebuy/custom-hooks

验证custom-hooks文件夹是否在libs下创建,并确保它已经被添加到 monorepo 根目录下的tsconfig.base.json文件中,现在它看起来应该像这样:

    "paths": {
      "@ebuy/custom-hooks": ["libs/custom-hooks/src/index.ts"],
      "@ebuy/mocks": ["libs/mocks/src/index.ts"],
      "@ebuy/ui": ["libs/ui/src/index.ts"],
      "@ebuy/utils": ["libs/utils/src/index.ts"]
    }

让我们现在创建我们的自定义钩子。使用generate命令创建一个具有以下信息的 React 组件:

  • 组件名称useSessionStorage

  • 项目custom-hooks

  • 文件名use-session-storage

  • flatSelected(生成扁平文件结构)

在新创建的use-session-storage.tsx组件文件中,让我们用以下代码替换样板代码:

import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useState,
} from 'react';
import { useEventCallback, useEventListener } from 'usehooks-ts';
declare global {
  interface WindowEventMap {
    'session-storage': CustomEvent;
  }
}
type SetValue<T> = Dispatch<SetStateAction<T>>;
export function useSessionStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
  // Get from session storage then
  // parse stored json or return initialValue
  const readValue = useCallback((): T => {
    // Prevent build error "window is undefined" but keep working
    if (typeof window === 'undefined') {
      return initialValue;
    }
    try {
      const item = window.sessionStorage.getItem(key);
      return item ? (parseJSON(item) as T) : initialValue;
    } catch (error) {
      console.warn(`Error reading sessionStorage key "${key}":`, error);
      return initialValue;
    }
  }, [initialValue, key]);
  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState<T>(readValue);
  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to sessionStorage.
  const setValue: SetValue<T> = useEventCallback((value) => {
    // Prevent build error "window is undefined" but keeps working
    if (typeof window === 'undefined') {
      console.warn(
        `Tried setting sessionStorage key "${key}" even though environment is not a client`
      );
    }
    try {
      // Allow value to be a function so we have the same API as useState
      const newValue = value instanceof Function ? value(storedValue) : value;
      // Save to session storage
      window.sessionStorage.setItem(key, JSON.stringify(newValue));
      // Save state
      setStoredValue(newValue);
      // We dispatch a custom event so every useSessionStorage hook are notified
      window.dispatchEvent(new Event('session-storage'));
    } catch (error) {
      console.warn(`Error setting sessionStorage key "${key}":`, error);
    }
  });
  useEffect(() => {
    setStoredValue(readValue());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  const handleStorageChange = useCallback(
    (event: StorageEvent | CustomEvent) => {
      if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) {
        return;
      }
      setStoredValue(readValue());
    },
    [key, readValue]
  );
  // this only works for other documents, not the current one
  useEventListener('storage', handleStorageChange);
  // this is a custom event, triggered in writeValueTosessionStorage
  // See: useSessionStorage()
  useEventListener('session-storage', handleStorageChange);
  return [storedValue, setValue];
}
export default useSessionStorage;
// A wrapper for "JSON.parse() to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
  try {
    return value === 'undefined' ? undefined : JSON.parse(value ?? '');
  } catch {
    console.log('parsing error on', { value });
    return undefined;
  }
}

这个自定义钩子代码是usehooks-ts库的一部分,并在此处提供:usehooks-ts.com/react-hook/use-session-storage

由于这个自定义钩子使用了 usehook-ts 库,我们将安装该 npm 模块:

pnpn i usehook-ts

接下来,我们需要将其导出,以便可以通过作用域路径导入。我们在 /libs/custom-hooks/src/index.ts 文件中添加以下行来执行此操作:

export * from './lib/use-session-storage'

接下来,我们将在 product-card 组件中使用我们新创建的自定义钩子,这样每次用户向购物车添加或从购物车中删除产品时,它都会将其作为数组存储在 sessionStorage 中。

/apps/catalog/src/app/productcard.tsx 文件中,我们将首先导入 useSessionStorage 钩子:

import { useSessionStorage } from '@ebuy/custom-hooks;

然后,在产品卡片组件中,我们使用 useSessionStorage 钩子并添加以下代码来添加和从购物车中删除项目的函数:

const [basket, setBasket]: any = useSessionStorage('shoppingBasket', {});
  const addItem = (id: string) => {
    basket[id] = basket[id] ? basket[id] + 1 : 1;
    setBasket(basket);
  };
 const removeItem = (id: string) => {
    basket[id] = basket[id] <= 1 ? 0 : basket[id] - 1;
    setBasket(basket);

接下来,我们更新 添加删除 按钮的点击事件,如下所示:

<div className="ui three buttons">
          <Button basic color="red" onClick={() => removeItem(product.id)}>
            Remove
          </Button>
          <Button basic color="blue">
            {basket[product.id] || 0}
          </Button>
          <Button basic color="green" onClick={() => addItem(product.id)}>
            Add
          </Button>
        </div>

让我们通过运行以下命令来测试它:

pnpm serve:all

点击 添加删除 按钮来查看一些产品的产品计数。

让我们打开开发工具并查看 应用程序 选项卡下的 sessionStorage

图 4.6 – 存储在会话存储中的购物车

图 4.6 – 存储在会话存储中的购物车

一旦状态存在于 会话存储 中,我们将在不同组件的多个地方读取它。最好创建一个可按需重用的实用函数。

我们将使用 Nx Console 创建另一个库,但这次不是创建一个 React 库,而是使用 @nrwl/workspace – library 模板来生成我们的通用 utils 库,并使用名为 @ebuy/utils 的导入作用域。

Nx Console > generate 步骤中我们填写的信息如下:

  • @****nwrl/workspace: library

  • 名称utils

  • importScope: @ebuy/utils

运行此命令将生成 utils 文件夹并创建 utils.ts 文件。让我们将其重命名为 get-session-storage.ts

添加以下代码以读取给定键的值:

export function getSessionStorage(key: any) {
  const sessionStorageValue = JSON.parse(
    window.sessionStorage.getItem(key) || '{}'
  );
  return sessionStoragevalue;
}
export default getSessionStorage;

如您所见,这是一个非常简单的函数,它接受一个键并返回给定键的会话存储中的值。

接下来,我们将获取页眉中的迷你购物车并将其连接起来以显示购物车中的总项目数。在 header.tsx 文件中,让我们添加必要的代码来读取和总计购物车中的项目。

让我们导入必要的函数:

import { useEffect, useState } from 'react';
import { useEventListener } from 'usehooks-ts';
import { getSessionStorage } from '@ebuy/utils';

我们将创建一个函数来计算总计数,如下所示:

const getTotalBasketCount = (basket: any): any => {
  return Object.values(basket).reduce((a: any, b: any) => a + b, 0);
};

接下来,在 Header 组件中,我们将使用 useEffectseventListeners 的组合来确保每次向购物车添加或从购物车中删除项目时,迷你购物车都会更新。

const [miniBasketCount, setMiniBasketCount] = useState(null);
  useEffect(() => {
    const basket: any = getSessionStorage('shoppingBasket');
    const totalCount: any = getTotalBasketCount(basket);
    setMiniBasketCount(totalCount);
  }, []);
  useEventListener('session-storage', () => {
    const basket: any = getSessionStorage('shoppingBasket');
    const totalCount: any = getTotalBasketCount(basket);
    setMiniBasketCount(totalCount);
  });

最后,我们将更新购物车图标以显示 {miniBasketCount},如下所示:

<Menu.Item position="right">
          <Label>
            <Icon name="shopping cart" />
            {miniBasketCount}
          </Label>
        </Menu.Item>

运行应用并尝试使用 添加删除 按钮添加和删除项目,并查看计数如何更新。

本章的最后部分,我们将完成结账应用中的购物车组件。

我们需要做的是从sessionStorage中获取shoppingBasket键的数据,并显示添加到购物车中的产品和数量。

我们打开位于apps/checkout/src/app/app.tsxapp.tsx结账文件,按照以下步骤从sessionStorage获取数据:

首先,我们像这样导入getSessionStorage

import { getSessionStorage } from '@ebuy/utils';

然后,在App函数中,我们添加以下内容:

const basketFromStorage: any = getSessionStorage('shoppingBasket');
    console.log('Basket: ', basketFromStorage);

当我们运行应用并查看控制台时,我们将能够看到shoppingBasket中的商品数组。

由于shoppingBasket只存储产品 ID 及其数量,我们需要将产品 ID 映射到产品名称,以便在购物车中显示名称。

让我们创建另一个函数来完成这个任务。我们将称之为createCompleteBasket

const createCompleteBasket = (allItems: any, quantities: any) => {
  return allItems
    .filter((item: any) => quantities[item.id])
    .map((item: any) => {
      return {
        ...item,
        quantity: quantities[item.id],
      };
    });
};

最后,在我们的应用组件函数中,我们通过过滤和映射产品列表中的值到shoppingbasket来创建completeBasket,如下所示:

  const completeBasket = createCompleteBasket(
    PRODUCT_LIST_MOCKS,
    basketFromStorage
  );

现在,我们将更新ShoppingBasket组件,传入这个新的属性,如下所示:

 <ShoppingBasket basketList={completeBasket} />

在浏览器中测试您的应用,并尝试操作。在目录应用中添加和移除商品,然后导航到结账应用,查看所有同步并显示正确商品列表的购物车。

关于代码示例的说明

正如您所看到的,我们在多个地方使用了“any”类型定义,并跳过了一些细节(包括单元测试)。这是故意的,为了避免过度复杂化示例,以便我们专注于本章的核心方面,例如应用之间的路由和状态共享。当构建用于生产的应用时,我们鼓励您定义正确的类型和接口,以充分利用 TypeScript 的全部功能,并编写相关的测试。

有了这些,我们就来到了这个相当紧张的部分的结尾...休息一下。做得好!

我们在这里做了很多工作。我们从上一章结束的地方继续,为我们的应用添加了一个共享的头部组件。然后,我们通过代理设置路由,以便在两个不同的应用之间导航,但就像它们是同一域名和端口的组成部分一样。我们还看到了如何使用会话存储在两个小程序之间共享状态。然后,我们创建了一个公共的自定义钩子来存储和检索会话存储中的数据,在这个过程中,我们构建了一个电子商务应用的骨架,包括向购物车添加商品和在结账应用以及头部的小购物车中更新购物车信息。

概述

这是一章很长的内容,所以恭喜你坚持看到最后。我们一开始是查看多-SPA 模式的外观。我们看到了这种模式对于非常大的应用程序,如银行门户、政府门户或电子商务网站来说最为合适。我们还看到了这种架构模式,其中所有这些不同的微应用程序都可以利用共享的通用组件和实用程序库,以确保不同应用程序的一致性。

我们随后深入研究了代码,并在 Nx 单一代码库中设置了我们的两个小型应用程序,之后我们着手创建共享的 UI 标题组件,并使用 Semantic UI 构建了我们的目录和结账应用程序。这也是我们了解如何使用作用域名称的好机会,这使得我们的导入路径看起来整洁简单。

然后,我们着手设置路由,以便在两个不同的应用程序之间进行导航,最后,我们设置了一个自定义钩子来在会话存储中存储我们的应用程序状态,并了解了如何使其在两个小型应用程序之间同步。

在下一章中,我们将探讨微应用程序模式,其中我们将在同一页面上加载多个微应用程序。

第五章:实现微前端模式的微应用

在上一章中,我们看到了构建微前端的多种 SPA 模式,这对于构建每个 SPA 都包含其自身用户旅程的大型应用来说是非常理想的。

这种模式的优点是每个应用都完全独立于其他应用,并且它们通过一个外部于应用的名字空间主路由连接。作为用户,在浏览应用时,您也会注意到,当您从一个 SPA 切换到另一个 SPA 时,您会通过浏览器重定向,并且页面会重新加载。如果您想避免这种情况,并且希望获得一致的 SPA 体验,那么我们可以探索使用模块联邦的微应用模式。

在本章中,我们将构建一个微应用微前端,我们将学习以下内容:

  • 模块联邦是什么,为什么它是构建微前端的关键?

  • 设置带有主机和远程应用的微前端应用

  • 将应用拆分成更小的微应用,并通过模块联邦加载

  • 设置不同页面之间的路由

  • 在不同的微应用之间共享状态

到本章结束时,我们将使用模块联邦将我们的多 SPA 微前端转换为微应用微前端。在这个过程中,我们还将了解 Zustand,这是一个易于使用的状态管理库。

技术要求

在我们浏览本章的代码示例时,我们需要以下条件:

  • 至少 8 GB RAM 的 PC、Mac 或 Linux 桌面或笔记本电脑(16 GB 更佳)

  • 英特尔芯片组 i5+或 Mac M1+芯片组

  • 至少 256 GB 的空闲硬盘存储空间

您还需要在您的计算机上安装以下软件:

  • Node.js 版本 18+(如果需要管理不同版本的 Node.js,请使用nvm

  • 终端:iTerm2 配合 OhMyZsh(你以后会感谢我的)

  • IDE:我们强烈推荐使用 VS Code,因为我们将会利用它自带的一些插件来提升开发体验

  • NPM、Yarn 或 PNPM。我们推荐 PNPM,因为它速度快且存储效率高

  • 浏览器:Chrome、Microsoft Edge 或 Firefox

  • 对 Nx.dev 单仓库有基本理解,以及使用 VS Code 中的 NX 控制台插件的基本技能

  • 对 React 有实际操作经验

本章的代码文件可以在以下位置找到:github.com/PacktPublishing/Building-Micro-Frontends-with-React

我们还假设您对 Git 有基本的操作知识,包括分支、提交代码和发起拉取请求。

为什么微前端需要模块联邦?

在多 SPA 方法构建微前端时,你可能已经注意到我们在不同的微应用之间重复了一些公共依赖项。从大局来看,当主要目标是保持简单时,这可以是一个可接受的权衡。然而,当重复的依赖项数量和构建的应用程序数量很高时,你需要优化事情并最小化重复。在 Webpack 5 之前尝试实现这一点会导致不得不处理复杂的依赖项管理。这也会使维护和演进微前端应用程序变得困难。模块联邦帮助我们解决这些挑战。

在接下来的章节中,我们将了解模块联邦是什么以及它是如何帮助构建微前端的。

什么是模块联邦?

模块联邦是 Webpack 5 中引入的一个新特性,它允许我们在实时中加载外部 JS 包。

在模块联邦之前,导入应用程序所需的所有必要模块的标准方式仅限于构建时,此时它创建了一个大 JS 包或基于页面路由加载的小块,但无法实时动态加载应用程序包。

模块联邦为我们提供了一种彻底新的方式来构建我们的应用程序,构建和部署共享组件,并在无需重新构建整个应用程序的情况下更新它们。

传统上,我们构建大部分共享组件,例如 UI 组件库或npm模块,并在构建时将它们导入到我们的应用程序中。使用模块联邦,这些模块可以托管在独立的 URL 上,并在运行时导入到应用程序中。我们利用这一相同的功能来构建我们的微前端架构,其中我们的微应用独立托管,并实时加载到宿主或壳应用程序中。

在我们深入了解如何进行之前,让我们看看与模块联邦相关的一些基本术语。模块联邦围绕几个概念。以下是一些:

ModuleFederationPlugin

所有模块联邦的功能都通过ModuleFederationPlugin插件在 Webpack 5+中提供。这是你定义模块联邦应该如何工作的设置的地方。

此插件允许在运行时与其他独立构建提供或消费模块。

您可以在此处详细了解ModuleFederationPlugin及其规范:webpack.js.org/plugins/module-federation-plugin/

在其最简单的形式中,ModuleFederationPlugin的代码应如下所示:

const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // module federation configuration options
    }),
  ],
};

上述代码是包含启用模块联邦所需所有配置的骨架。

宿主应用程序

这是根应用,其中加载了远程或外部应用。主机应用的模块联邦配置存储了需要在其内部加载的远程应用列表。在我们的微前端用例中,主机应用还包含有关不同路由及其与相应远程应用映射的信息。

主应用中 Webpack 对模块联邦的配置应该如下所示:

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostAppName',
      remotes: {
        app1: '<app1's URL path to remoteEntry.js>',
       app2: '<app2's URL path to remoteEntry.js>',
      },
    }),
  ],
};

上述代码易于理解。我们让模块联邦知道主机应用的名字,并提供远程应用列表以及它们对应的remoteEntry文件在remotes对象中的路径。

远程应用

如你所猜,远程应用是在主机应用中动态加载的应用。在模块联邦术语中,这些远程应用也被称为容器。这些远程应用的 JS 包通常通过一个单一的.js文件暴露,通常称为remoteEntry.js,这是主机应用所寻找的。

每个远程应用都以以下方式在 Webpack 的模块联邦配置中公开:

new ModuleFederationPlugin({
      name: 'remoteAppName', // this name needs to match with the entry name
      exposes: ['./public-path/remoteEntry.js'],
      // ...
    }),

每个远程应用都需要在其name属性中定义一个唯一名称,并且这个名称需要与主机应用模块联邦配置中定义的remotes对象中的名称相匹配。

remoteEntry.js

remoteEntry.js文件是一个由模块联邦在运行时创建的小型 JS 文件。它包含每个远程应用的元数据。主机应用依赖于remoteEntry.js文件来知道要加载哪些模块。

模块联邦的使用案例不仅限于构建微前端;它们还可以用来动态加载公共库或模块,例如设计系统,例如,无需将这些公共库作为npm包发布,每次公共库发生变化时都无需重新构建和重新部署。

以下图表有助于解释模块联邦是如何工作的:

图 5.1 – 实时加载三个微应用的模块联邦

图 5.1 – 实时加载三个微应用的模块联邦

从图中,我们看到我们有在端口300130023003上运行的应用。每个应用都在其各自的remoteEntry.js文件中公开了其元数据。这些应用通过模块联邦动态加载到在端口3000上运行的主机应用中。

了解这一点可能很谨慎:这不仅仅是应用。任何类型的 JS 模块都可以动态导入到模块联邦中。

在下一节中,我们将把这些内容付诸实践。

使用主机和远程应用设置微前端

我们将把我们的多 SPA 应用转换成一个带有主机和远程应用的微前端,使用模块联邦。如前所述,这种方法的主要好处是用户可以获得真正的单页体验,同时确保每个应用都是独立构建和部署的。

让我们看看我们为此需要做什么:

图 5.2 – 模块联邦设置

图 5.2 – 模块联邦设置

你会注意到 图 5**.2图 5**.1 类似,并解释了模块联邦的实现细节。我们看到主应用程序包含头部组件,并在端口 4200 上运行。然后我们有我们的目录和检查应用程序在端口 42024201 上运行。我们的目标是动态加载这些远程应用程序,每当调用正确的路由时。

为了将我们的多 SPA 转换为模块化联邦的微前端,我们需要进行以下更改:

  1. 创建一个名为 App-shell 的新主应用程序。

  2. 从每个单页应用程序(SPA)中删除头部组件并将其移动到 App-shell。

  3. 定义需要加载到主应用程序中的远程应用程序,即目录和检查应用程序。

  4. 定义目录和检查微应用程序的远程入口。

让我们开始吧。打开您在前一章中构建的 e-buy 应用程序。

您也可以从 Git 仓库下载它:

github.com/PacktPublishing/Building-Micro-Frontends-with-React/tree/main/ch4/ebuy

在接下来的小节中,我们将看到如何创建我们的主应用程序和远程应用程序,但首先,我们将清理现有的应用程序,并准备它们使用模块联邦。

清理

使用模块联邦,主应用程序负责路由,我们不需要使用我们在 proxy.conf.json 文件中定义的代理配置。因此,我们将删除此文件,并从 project.json 文件中删除不必要的配置。

然后继续删除 /apps/catalog/proxy.conf.json,并在 catalog/project.json 文件中删除以下行:

"proxyConfig": "apps/catalog/proxy.conf.json"

在此过程中,我们还可以删除我们在 checkout/project.json 文件中定义的 baseRef。找到此行并将其删除:

"baseHref": "/checkout/"

设置 App-shell 主应用程序

通过这种方式,我们现在已准备好开始将我们的多 SPA 应用程序迁移到模块联邦。

Nx 控制台有一个用于创建模块联邦的主应用程序和远程应用程序的便捷生成器。按照以下步骤操作:

  1. 创建一个 React 主应用程序:

    Nx 控制台 | 生成 | @nrwl/react – 主生成一个主 react 应用程序

图 5.3 – 从 Nx 开发控制台选择主应用程序生成器

图 5.3 – 从 Nx 开发控制台选择主应用程序生成器

  1. 在此表单中输入以下信息:

    • 名称: app-shell

    • devServerPort: 4200

    • e2eTestrunner:

    • remotes: 由于一个不允许多个应用程序名称的 bug,我们将留空并手动添加。

  2. 在您点击 apps/app-shell/module-federation.config.js 后。

  3. 打开文件,在 remotes 数组中添加目录和检查作为远程应用程序:

    remotes: ['catalog', 'checkout']
    
  4. 现在,让我们打开 React.Suspence 和 React Router 的 Route

  5. 我们将调整此模板文件:

    import React from 'react';import { Container } from 'semantic-ui-react';
    import { Route, Routes } from 'react-router-dom';
    import 'semantic-ui-css/semantic.min.css';
    import { Header } from ‘@ebuy/ui’; 
    const Catalog = React.lazy(() => import('catalog/Module'));
    const Checkout = React.lazy(() => import('checkout/Module'));
    export function App() {
      return (
        <React.Suspense fallback={null}>
          <Container style={{ marginTop: '5rem' }}>
            <Header />
    <Routes>
              <Route path="/" element={<Catalog />} />
              <Route path="/catalog" element={<Catalog />} />
              <Route path="/checkout" element={<Checkout />} />
            </Routes>
          </Container>
        </React.Suspense>
      );
    }
    export default App;
    

    如您从前面的代码中看到的,我们首先将 Header 组件导入到 App-shell。

    您还会注意到我们正在使用动态导入来使用React.lazy导入我们的 Catalog 和 Checkout 应用。这些行目前会抛出错误,因为它无法找到模块。

  6. 为了解决这个问题,创建一个名为/apps/app-shell/src/remotes.d.ts的文件,并添加以下代码:

    declare module 'catalog/Module';declare module 'checkout/Module';
    

    remotes.d.ts文件用于在模块联邦设置中为远程提供 TypeScript 类型声明。

    在 JSX 的下方,您会注意到我们在//catalog路由上导入 Catalog 应用,而在/checkout路由上导入 Checkout 应用。

这基本上完成了宿主应用的设置。

设置我们的远程应用

设置我们的远程应用需要一些工作。让我们逐一解决这些问题。

为了将现有的 Nx 中的 React 应用转换为远程应用,我们需要做以下几步:

  • module-federation.config.js文件中创建远程入口。

  • project.json中的应用构建器更改为使用模块联邦插件。

  • 添加一个serve-static执行器。

  • 使用自定义的 Webpack 配置,该配置定义了远程入口模块。

让我们从 Catalog 应用开始执行前面的更改。按照以下步骤操作:

  1. apps/catalog文件夹中,创建一个名为module-federation.config.js的新文件,并添加以下代码:

    const moduleFederationConfig = {  name: 'catalog',
      exposes: {
        './Module': './src/app/app.tsx',
      },
    };
    module.exports = moduleFederationConfig;
    

    这是我们定义目录远程应用及其暴露的模块路径的地方。

  2. 接下来,我们需要对apps/catalog/project.json文件进行一些更改。

  3. 首先,在目标下添加一个新的命令,命名为serve-static

        "serve-static": {      "executor": "@nrwl/web:file-server",
          "defaultConfiguration": "development",
          "options": {
            "buildTarget": "catalog:build",
            "port": 4201
          }
        }
    

    注意我们打算在4201上运行我们的应用,所以让我们也确保serve命令也使用端口号4201

  4. 确保在常规serve命令下的端口号在options对象中定义:

    "serve": {      "executor": "@nrwl/web:dev-server",
          "defaultConfiguration": "development",
          "options": {
            "buildTarget": "catalog:build",
            "hmr": true,
            "port": 4201
          },
    

    这是因为模块联邦插件期望在options对象中定义端口号。如果没有定义,它将使用默认端口号,这可能导致非常有趣的错误。

    参考源代码中的这一行:github.com/nrwl/nx/blob/master/packages/react/src/module-federation/with-module-federation.ts#L29

  5. 接下来,在serve对象下,我们更新执行器以使用module-federation dev-server

    "serve": {      "executor": "@nrwl/react:module-federation-dev-server",
    
  6. 接下来,确保我们有WebpackConfig带有自定义 Webpack 配置:

    "webpackConfig": "apps/catalog/webpack.config.js"
    
  7. 现在我们更新webpack.config.js文件,添加以下代码:

    const { withModuleFederation } = require('@nrwl/react/module-federation');const baseConfig = require('./module-federation.config');
    const defaultConfig = {
      ...baseConfig,
    };
    module.exports = withModuleFederation(defaultConfig);
    

现在我们对 Checkout 应用重复相同的步骤:

  1. apps/checkout/文件夹中,创建一个名为module-federation.config.js的新文件,并添加以下代码:

    const moduleFederationConfig = {  name: 'checkout',
      exposes: {
        './Module': './src/app/app.tsx',
      },
    };
    module.exports = moduleFederationConfig;
    

    如您所见,它与 Catalog 应用中的配置相同。唯一的区别是我们将name值更改为checkout

  2. 接下来,让我们将serve-static命令添加到apps/checkout/project.json文件中的targets对象:

          "serve-static": {      "executor": "@nrwl/web:file-server",
          "defaultConfiguration": "development",
          "options": {
            "buildTarget": "checkout:build",
            "port": 4202
          }
        }
    
  3. 在同一文件中,我们继续更新执行器:

    "serve": {      "executor": "@nrwl/react:module-federation-dev-server",
    
  4. 然后在serve.options下更新端口号为4202

  5. 我们还更新了webpackConfig

    "webpackConfig": "apps/checkout/webpack.config.js"
    

    由于webpack.config.js文件没有变化,我们可以简单地从 Catalog 应用复制并粘贴此文件。

  6. 最后,我们将更新Header组件以使用来自ReactRouterLink组件,以便我们获得单页应用体验。

  7. 打开/libs/ui/src/lib/header.tsx文件,并将以下内容更新为使用而不是

    <Link to={navItem.href ?? '#'}>{navItem.label}</Link>
    
  8. 不要忘记导入命令:

    import { Link } from 'react-router-dom';
    
  9. 在我们尝试测试之前,别忘了从位于/apps/catalog/src/app/app.tsx的相应 Catalog 应用和位于/apps/checkout/src/app/app.tsx的 Checkout 应用中移除头部组件。

  10. 让我们在终端上快速测试一下。运行以下命令:

    app-shell serve command.
    
  11. 一切运行无误后,打开 App-shell 应用的/assets文件夹。

  12. 在多 SPA 方法中,Catalog 应用是默认路由,并且有点像宿主应用。由于 App-shell 现在是我们的宿主,我们需要将/catalog/src/assets文件夹中的图片复制到app-shell/src/assets文件夹中。一旦完成这个操作,图片应该会加载到应用中。

  13. 在 Catalog 应用和 Checkout 应用之间导航。将商品添加到购物车,并享受看到应用良好工作的乐趣。

    由于一切进展顺利,并且每个微应用团队都应该能够独立工作在自己的应用上,让我们也确保我们可以单独运行每个应用。

  14. 运行pnpm nx serve catalog,你会注意到你得到一个错误:

    ErrorShared module is not available for eager consumption: webpack/sharing/consume/default/react/react
    

    这是因为模块联邦将 Catalog 应用视为双向宿主,并且无法急切地加载共享模块。

    你可以在这里了解更多信息:

    webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption

    为了克服这个问题,我们需要定义一个异步边界来分割出更大块初始化代码,并避免任何额外的服务器往返。

  15. 为了解决这个问题,我们需要做一些调整。在 Catalog 应用中,我们首先将/apps/catalog/src/main.tsx重命名为bootstrap.tsx

  16. 接下来,我们在同一个src文件夹内创建一个名为main.ts的新文件,并有一个单独的行导入 bootstrap:

    import('./bootstrap');
    
  17. 接下来,我们需要确保这个新创建的main.ts文件是作为入口点使用的,因此现在,在我们的 Catalog 应用的项目.json 文件中,我们需要更新build > options对象内的main属性:

    "main": "apps/catalog/src/main.ts",
    
  18. 对 Checkout 应用重复相同的步骤。现在,你应该能够以模块联邦微前端或每个应用独立的方式运行应用。

你可能也注意到,在本章的开头,我们提到了remoteEntry.js文件作为远程应用的入口文件,并且我们实际上并没有定义一个。

然而,如果你查看你的开发工具的网络标签页,你会注意到有两个 remoteEntry.js 文件分别从端口 42014202 被调用。这是 Nx 和模块联邦在这里进行一些魔法操作。

重要提示

如果你深入查看这个文件中的源代码,你会注意到文件名被定义为 ModuleFederationPlugin 配置的一部分 (github.com/nrwl/nx/blob/master/packages/react/src/module-federation/with-module-federation.ts)。

图 5.4 中的截图显示了 remoteEntry.js 文件从相应的应用中被调用:

图 5.4 – RemoteEntry.js

图 5.4 – RemoteEntry.js

如果你渴望明确地定义文件并尽可能接近模块联邦的原生工作方式,那么请创建一个文件在 apps/catalog/src/remote-entry.js 中,包含以下行:

export { default } from './app/app';

apps/catalog/module-federation.js 文件中的 exposes 值更新为以下内容:

const moduleFederationConfig = {
  name: 'catalog',
  exposes: {
    './Module': './src/remote-entry.ts',
  },
};

通过这样,我们已经完成了关于使用模块联邦的章节,并成功将我们的多 SPA 应用转换为模块联邦微前端。

在本节中,我们看到了要使模块联邦工作所需的最小步骤,以及需要额外步骤,如定义远程和暴露模块名称,以允许每个应用独立工作。

在下一节中,我们将看到如何进一步将远程应用分解为真正的微应用微前端。

将模块联邦扩展到真正的微应用模式

想象一下,你是一个管理一个非常大的电子商务应用(比如 Amazon.com)的团队的成员。对于这样的大型网站,拥有一个拥有单个有机级组件(atomicdesign.bradfrost.com/chapter-2/#organisms)而不是整个迷你应用的团队是一种常见的做法。

例如,我们有一个专门的小组,他们专门负责产品推荐组件。这个组件被注入到,比如说,目录应用中。

在这种情况下,创建另一个名为推荐的微应用并将其动态导入到目录应用中将是明智的。这将允许实现真正的、联邦化的微应用模式架构。

图 5.5 – 使用模块联邦的远程应用树

图 5.5 – 使用模块联邦的远程应用树

如您从前面的图中所见,我们可以进一步将目录和结账应用分解为更小的有机级组件,并通过模块联邦让每个组件远程加载到目录应用中。

重要提示

记住,虽然这看起来非常酷,但我们不应该过度操作,将每个单一的组织体都转换为模块联邦的微应用程序。重要的是要遵循第 第二章 中提到的微前端原则,即 将应用程序分解为单个团队拥有的最大独立部署的应用程序,而不一定是最小的。

话虽如此,假设你确实有一个拥有 Recommendations 微应用程序的独立团队,让我们着手创建微应用程序。

创建 Recommendations 远程微应用程序

让我们使用我们信任的 Nx 开发控制台和 GENERATE 命令,按照以下步骤操作:

  1. 选择 @nrwl/react - remote Generate a remote application 并在保持其余默认设置的情况下使用以下信息:

    • 名称recommendations

    • e2eTestRunnernone

    • 主机catalog

    • devServerPort4203

  2. 使用 Generate 命令并验证 recommendations 应用程序是否已成功创建。

  3. 让我们快速编辑 apps/recommendations/src/app.tsx 文件,移除样板代码,并留下一个简单的消息:

    import 'semantic-ui-css/semantic.min.css';export function App() {
      return (
        <div className="ui raised segment">
          <h1>Recommendations</h1>
          <p>Recommendations goes here</p>
        </div>
      );
    }
    export default App;
    

注意

构建一个完整的 Recommendations 微应用程序超出了本书的范围。

  1. 运行 npx nx serve recommendations 并验证应用程序是否在端口 4203 上正确加载。

在我们添加它作为远程应用程序到 Catalog 应用程序的同时,保持其运行。

将 Recommendations 作为远程应用程序添加到 Catalog

由于我们希望 Recommendations 微应用程序作为远程应用程序加载到 Catalog 中,我们需要将 Catalog 转换为类似主机的行为。我们通过以下步骤来完成:

  1. 打开 apps/catalog/module-federation.config.js 文件,并向其中添加 remotes 条目:

    const moduleFederationConfig = {  name: 'catalog',
      remotes: ['recommendations'],
      exposes: {
        './Module': './src/app/app.tsx',
      },
    };
    module.exports = moduleFederationConfig;
    
  2. 接下来,让我们在 apps/catalog/src 文件夹内创建一个 remotes.d.ts 文件,使用以下行:

    declare module 'recommendations/Module';
    
  3. 最后,让我们将 Recommendations 应用程序导入并调用到我们的 apps/catalog/src/app/app.tsx 文件中:

    . . .import React from 'react';
    const Recommendations = React.lazy(() => import('recommendations/Module'));
    
  4. 在组件的 jsx 部分,这是我们调用 Recommendations 组件的方式:

    <Recommendations />
    
  5. 打开一个新的终端窗口并运行以下命令:

    pnpm nx serve app-shell
    

如果一切按计划进行,你将看到带有 Recommendations 组件的 Catalog 应用程序。有了这个,我们就到了本节的结尾。

在本节中,我们看到了如何使用模块联邦进一步将主应用程序分解成更小的微应用程序,并使它们作为一个远程应用程序的树状结构协同工作。

在下一节中,我们将看到如何在我们的微前端微应用程序中设置状态管理。

使用模块联邦进行状态管理

如您现在可能已经注意到的,我们的自定义状态管理系统,它使用 sessionStorage,与模块联邦无缝协作。这是因为从 React 的角度来看,它看起来就像一个普通的 React 应用程序,模块是懒加载的。因此,模块联邦的一个好处是我们可以使用任何常规的状态管理概念,例如属性钻取、上下文 API 或 Redux 或 Zustand 等库来管理状态。

在本节中,我们将使用 Zustand 状态管理库,因为它极其用户友好,且没有样板代码。

现在,从逻辑上讲,尤其是对于那些大量使用上下文 API 的人来说,我们可能会倾向于将 store 放在 App-shell 中,并让其他微应用程序消费它。然而,使用模块联邦,这并不理想,因为 store 需要作为一个远程应用程序暴露,并导入作为宿主的其他微应用程序。如果你尝试绘制这个,它有点像某种循环依赖,其中 App-shell 作为所有其他组件的宿主,但位于其中的 store 是其他组件的远程。

以下图更好地说明了这种循环流程的问题:

图 5.6 – App-shell 和 store 之间的循环流程

图 5.6 – App-shell 和 store 之间的循环流程

当使用模块联邦时,最好有一个单向流程来描述远程和宿主应用程序的加载方式。考虑到这一点,最好将我们的 store 作为其独立的微应用程序,并使其对所有使用它的其他应用程序定义为一个远程应用程序。有了这种新的结构,图 5.6 中的图可以重新绘制:

图 5.7 – Store 应用程序的单一方向远程连接

图 5.7 – Store 应用程序的单一方向远程连接

图 5.7 所示,Store 应用程序的远程单向流程看起来要干净得多,并确保 App-shell 不会不必要地膨胀业务逻辑和状态。

由于我们打算使用 Zustand 进行状态管理,这将是安装它的好时机。运行以下命令:

pnpm install zustand

现在,让我们按照创建我们的 Recommendations 远程应用程序的步骤来创建我们的 Store 远程应用程序:

  1. 使用 Nx Console 和 @nrwl/react - remote Generate a remote application 文件,创建 Store 微应用程序。

  2. 使用以下信息填写表格,其余部分保留默认值:

    • 名称: store

    • e2eTestRunner: none

    • 宿主: (留空,因为我们将会手动添加宿主)

    • devServerPort: 4204

一旦创建了应用,让我们着手设置我们的存储库。为了展示不同微应用之间状态和存储的工作情况,我们在宿主应用中添加一个 Like 按钮。点击它将增加喜欢计数。我们还会在推荐应用中显示计数。然后,在推荐微应用中我们将有一个 重置 按钮,该按钮将重置存储库并验证所有地方的喜欢计数是否已重置。

让我们开始吧:

  1. 导航到 /apps/store/src 文件夹并创建一个名为 store.tsx 的新文件。这是我们定义存储和钩子的地方。

注意

Zustand 非常易于使用。请查看 docs.pmnd.rs/zustand/getting-started/introduction 的文档。

  1. store.tsx 文件开始,导入 Zustand 并定义 LikeCount 接口:

    import {create} from 'zustand';interface LikeCount {
      count: number;
      increment: () => void;
      reset: () => void;
    }
    
  2. 接下来,我们创建我们的 useStore 钩子并定义 初始状态增加重置 函数。这是这样做的一种标准方式:

    const useStore = create<LikeCount>((set) => ({  count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      reset: () => set(() => ({ count: 0 })),
    }));
    export default useStore;
    

    就这样!我们的存储库和 useStore 钩子已经准备好被使用了。

  3. 接下来,我们需要将其作为远程应用公开。我们将通过进行两项额外更改来实现这一点。在 apps/store/src/remote-entry.ts 文件中,将以下行修改为以下文本:

    export { default } from './store';
    
  4. 接下来,我们让 App-shell 和推荐应用知道它们需要将 Store 作为远程应用使用。我们通过在 App-shell 和推荐应用的相应 module-federation.config.js 文件中的远程数组中添加 Store 来实现这一点:

    //apps/app-shell/module-federation.config.jsconst moduleFederationConfig = {
      name: 'app-shell',
      remotes: ['catalog', 'checkout', 'store'],
    };
    module.exports = moduleFederationConfig;
    

    这是 apps/recommendations/module-federation.config.js 文件中的内容:

    const moduleFederationConfig = {  name: 'recommendations',
      remotes: ['store'],
      exposes: {
        './Module': './src/remote-entry.ts',
      },
    };
    module.exports = moduleFederationConfig;
    
  5. 下一步,我们需要在 app-shell 文件的 remotes.d.ts 文件中声明 store 模块:

    declare module 'store/Module';
    
  6. 我们还需要在推荐应用中做同样的事情。由于 remotes.d.ts 文件不存在,我们可以在 /apps/recommendations/src/remotes.d.ts 中创建一个新文件,包含以下行:

    declare module 'store/Module';
    

现在我们已经将存储库连接起来,以便推荐和 App-shell 可以读取和写入我们的 Store 微应用。

将“喜欢”按钮添加到宿主应用

在本节中,我们将创建一个 Like 按钮,该按钮增加喜欢计数并将其存储在存储库中。

现在我们已经设置了远程服务器,让我们将存储库导入到我们的应用外壳中,并创建 /apps/app-shell/src/app/app.tsx 文件。按照以下步骤操作:

  1. 导入 useStore

    import { Button } from ‘semantic-ui-react’import useStore from 'store/Module';
    
  2. 然后,在 App 函数中解构 count 和 increment:

    const { count, increment } = useStore();
    
  3. 最后,在我们的 JSX 中,我们在 <****Header/> 组件之后添加我们的按钮:

    <Button onClick={increment}>{count} Likes </Button>
    
  4. 重新启动所有应用。您还可以使用我们创建的以下自定义命令来服务所有应用:

    pnpm serve:all
    

    我们在主应用中通过 Zustand 实现了状态管理,如您所见,它非常简单,没有多余的模板代码。但设置状态和存储库的真正目的是确保这个状态可以被其他微应用共享。在我们的例子中,它将是位于联盟层次结构底部的推荐应用。

  5. apps/recommendations/src/app/app.tsx文件中,我们的代码应该看起来非常类似于以下内容:

    import 'semantic-ui-css/semantic.min.css';import { Button } from ‘semantic-ui-react’
    import useStore from 'store/Module';
    export function App() {
      const { count, reset } = useStore();
      return (
        <div className="ui raised segment">
          <h1>Recommendations</h1>
          <p>Recommendations goes here</p>
          <p> {count} people liked the recommendations</p>
          <Button onClick={reset}>reset</Button>
        </div>
      );
    }
    export default App;
    

    就这样!

  6. 运行您的应用,并尝试使用喜欢按钮。重置它并验证主应用和推荐应用之间的计数是否保持同步:

图 5.8 – 微应用间共享状态的完整工作应用

图 5.8 – 微应用间共享状态的完整工作应用

我们的应用运行得很好,但让我们确保它也具有高性能!

避免不必要的重新渲染

在处理状态时,一个非常重要的性能相关点是避免不必要的重新渲染。这在状态在不同组件之间共享或通过属性钻取时尤其正确。

验证这一点的办法是进入开发者工具,打开渲染面板,选择闪烁绘制帧渲染统计,并验证当您点击按钮时,只有组件内的必要项目会更新。

在 Chrome 浏览器中,您可以通过打开开发者工具,进入更多工具,然后选择渲染面板来访问此面板:

图 5.9 – 开发者控制台中的渲染面板

图 5.9 – 开发者控制台中的渲染面板

如您从图 5.9中看到,一旦我们启用了闪烁绘制,您将看到绿色矩形包围了由于变化或用户交互而重新渲染的页面部分。理想的状态是当用户与之交互时,只有页面的一小部分闪烁。帧渲染统计显示了用户与页面交互时的帧率。帧率理想情况下应接近 60 fps,以提供流畅的用户体验。

有了这些,我们就来到了本节关于模块联盟应用中 Zustand 状态管理的结束。在本节中,我们学习了将 Store 应用定义为单独的微应用的优点。然后它被动态导入到其他微应用中。我们学习了如何设置存储库作为模块联盟模块。然后我们看到主应用和推荐应用如何通过共享存储库共享状态。最后,我们还能够打开闪烁绘制和帧渲染,以验证当状态变化时,只有应用内的必要元素更新,并且不会导致未更改的组件重新渲染。

摘要

我们终于来到了另一个有趣章节的结尾。我们开始学习模块联邦以及它如何改变我们构建和维护应用的方式。我们学习了模块联邦的一些基本概念,例如宿主应用、远程应用、remoteEntry等。

然后,我们看到了如何将我们的多 SPA 应用转换为具有应用壳的模块联邦应用,以及如何将目录和结账应用作为远程应用加载。接着,我们将这些应用进一步拆分,包括更小的微应用,以创建模块联邦微应用的树状结构。最后,我们看到了一些管理状态的最佳实践,并了解了如何使用诸如 Zustand 这样的工具来管理不同微应用之间的状态。

在接下来的章节中,我们将了解如何构建这些应用以供生产使用,以及如何将它们部署到云端的静态存储上。

第六章:服务器端渲染微前端

大多数 JavaScript 框架,包括 React,主要用于构建客户端渲染CSR)应用。客户端渲染应用在某些用例中非常好,例如管理仪表板或银行应用,用户在登录区域与应用交互。CSR 应用对于用户通过搜索引擎访问网站或进行匿名简短用户旅程(如新闻网站、博客或电子商务网站的客户结账)的用例并不理想。这是因为许多搜索引擎机器人无法索引基于 CSR 的 Web 应用。CSR 应用也有较差的最大内容渲染时间LCP)得分——即它们首次页面加载的性能得分很差,导致更高的跳出率。

为了克服这些缺点,现在有一个被广泛接受的做法,即在 Node.js 服务器上渲染 Web 应用的页面,并将渲染后的 HTML 页面提供给浏览器。这通常被称为服务器端渲染SSR),或服务器端渲染应用(SSR)。

在本章中,我们将探讨如何为服务器端渲染应用构建一个模块联邦微前端。虽然实现模块联邦的过程与上一章中看到的过程非常相似,但由于页面是服务器端渲染的,这带来了一些复杂性,我们将探讨在实现具有 SSR 的微前端时需要处理的一些细微差别。

在本章中,我们将涵盖以下主题:

  • 快速了解 CSR 和 SSR 应用的区别

  • 了解 Next.js 和 Turborepo

  • 学习如何使用 Next.js 和模块联邦设置主机和远程应用

  • 看看如何将多个组件暴露为远程组件,以便可以被不同的应用消费

  • 探讨与 SSR(静态服务器渲染)中状态保持相关的问题,以及如何在主应用中反映一个微应用所做的更改

到本章结束时,我们将使用 Next.js 构建一个服务器端渲染的微前端。

技术要求

在我们浏览本章中的代码示例时,我们需要以下条件:

  • 一台至少有 8 GB RAM(16 GB 更佳)的 PC、Mac 或 Linux 桌面/笔记本电脑

  • 英特尔 i5+芯片组或 Mac M1+芯片组

  • 至少 256 GB 的空闲硬盘存储空间

  • 对 Next.js 和 Turborepo 有基本了解是理想的

  • 对 Node.js 有基本了解将有所帮助

你还需要在你的计算机上安装以下软件。

  • Node.js 版本 18+(如果你必须管理不同版本的 Node.js,请使用nvm)。

  • 终端:iTerm2 与 OhMyZsh(你以后会感谢我的)。

  • IDE:我们强烈推荐 VS Code,因为我们将会使用它的一些插件来提升开发者体验。

  • NPM、Yarn 或 PNPM;我们推荐 PNPM,因为它速度快且存储效率高。

  • 浏览器:Chrome、Microsoft Edge 或 Firefox。

本章的代码文件可以在以下位置找到:https://github.com/PacktPublishing/Building-Micro-Frontends-with-React。

我们还假设您对 Git 有基本的了解,例如分支、提交代码和发起拉取请求。

客户端渲染和服务器渲染应用有何不同?

当涉及到使用 JavaScript 构建 Web 应用时,在用户界面构建和向用户提供服务方面有两种主要方法。它们被称为客户端渲染CSR)和服务器端渲染SSR)。

从开发角度来看,编写 CSR 或 SSR 应用的主要过程基本相同,只是 SSR 需要一些额外的步骤。然而,这些应用在渲染方式和云部署方式上存在一些差异。

在本节中,我们将更深入地探讨这些差异。

客户端渲染应用 (CSR)

让我们来了解一下客户端应用是如何工作的。正如其全名所暗示的,CSR 应用是在客户端“渲染”的。简而言之,应用在用户的浏览器中运行,调用数据获取,并在浏览器中生成页面。以下图表更好地说明了这一点:

图 6.1 – CSR 应用的请求和响应流程

图 6.1 – CSR 应用的请求和响应流程

前面的图 6**.1展示了 CSR 应用中的请求流程。在这里,浏览器对给定 URL 进行第一次调用,服务器(有时是 CDN 本身)将返回一个几乎为空的 HTML 外壳,其中包含应用 JavaScript 包的链接。浏览器解析包,然后对服务器 API 进行第二次 AJAX 调用,接收给定 URL 的 JSON 响应。浏览器然后解析响应,并根据客户端应用中的视图,在浏览器中渲染 HTML 页面,然后再将其提供给用户。对于其他每次调用,浏览器继续对 API 端点进行 AJAX 调用,并在浏览器中解析页面。

在这个流程中,请注意,对于用户的第一次请求,有两个往返服务器的过程——首先,获取 JavaScript 包,其次,获取页面数据和渲染页面。

由于 CSR 应用的工作性质,它们非常适合用户体验,用户通常在应用中保持登录状态,并在会话中浏览多个页面。

客户端渲染应用的缺点如下:

  • 对于第一次请求,由于额外的往返服务器,用户需要等待更长的时间。

  • 由于服务器响应不包含任何实际的 HTML 数据,未针对解析 JavaScript 进行优化的搜索引擎爬虫在索引客户端渲染应用的内容时将遇到困难。

CSR 应用不适合用户旅程短的场景,例如用户通过搜索结果链接到达的电子商务网站,购买一两个产品后离开,或者博客网站,用户通常一次只阅读一到两篇文章。

现在,让我们看看一个服务器端渲染的应用程序是如何工作的。

服务器端渲染应用程序(SSR)

在服务器端渲染的应用程序中,正如其全名所暗示的,对于第一次请求,页面是在服务器上生成的,并且渲染的 HTML 页面被发送到浏览器。让我们更详细地看看:

图 6.2 – 服务器端渲染应用程序的请求和响应流程

图 6.2 – 服务器端渲染应用程序的请求和响应流程

SSR 应用程序的工作原理在先前的图 6.2 中进行了说明。我们在这里看到的是,当浏览器从浏览器向 Node.js 服务器发出对页面的第一个请求时,它反过来调用 API 服务器以获取数据。然后,HTML 页面在服务器本身上生成并发送到浏览器,包括初始状态和 JavaScript 包。状态在浏览器上恢复,然后所有后续调用都是从浏览器到 API 服务器的,页面在浏览器本身上渲染。

由于浏览器在第一次请求时就接收到了完全渲染的 HTML 页面,因此最终用户感知的性能良好。它还有助于搜索引擎优化SEO),尤其是在搜索引擎爬虫不太擅长解析 CSR 页面的情况下。

服务器端渲染的应用程序适用于用户旅程短的网络应用程序,例如 B2C 电子商务应用程序,或者内容丰富的应用程序,如新闻网站或博客。

我们现在对 SSR 和 CSR 应用程序的工作原理有了很好的理解,了解了它们的优缺点,以及哪些用例最适合它们。有了这些信息,让我们在下一节开始构建我们的 SSR 微前端。

构建我们的服务器端渲染微前端

在本节中,我们将探讨如何使用像 Next.js 这样的元框架构建 SSR 应用程序,然后我们将进一步构建一个使用 webpack 的模块联邦插件的模块联邦微前端。在这个过程中,我们将探索另一个 monorepo 工具,即 Turborepo。

重要提示

在撰写本书时,模块联邦插件不支持 Next.js 13 和应用程序路由,因此在本章中,我们将使用 Next.js 版本 12

当涉及到在 React 中构建 SSR 应用程序时,有两种常见的方法:

  • 使用 Node.js 进行自定义构建:在这里,我们设置一个 Node.js 服务器,在 Node.js 上渲染 React 应用程序,使用renderToStringrenderToPipeableStream方法将响应字符串化,然后使用hydrateRoot方法,这些都是react-dom/server模块的一部分,将 React 附加到渲染的 HTML 上。

  • 使用服务器端渲染元框架:例如 Next.js、Remix 或 Shopify 的 Hydrogen 这样的元框架可以抽象出设置服务器端渲染应用程序的所有复杂性,并提供一个简单的接口来构建高性能的 SSR React 应用程序

对于本章,我们将使用 Next.js 来构建我们的 SSR 应用程序。Next.js 是构建 SSR React 应用程序中最古老和最受欢迎的框架之一。

对于单一代码仓库,我们将使用另一个名为 Turborepo 的工具。虽然我们也可以使用 Nx 单一代码仓库构建 Next.js 应用程序,但我们将选择 Turborepo,这样我们也可以了解不同单一代码仓库工具的细微差别以及它们是如何运作的。

Turborepo 和 Next.js 入门

Next.js 是最流行的元框架,允许你使用 React 构建 SSR 应用程序。Turborepo 是另一个正在获得流行的新单一代码仓库框架,它最近被 Next.js 的构建和维护公司 Vercel 收购。

尽管我们将在本章中介绍 Turborepo 和 Next.js 的基础知识,但我强烈建议你花时间阅读它们的文档,以更深入地了解这些框架是如何工作的。

我们将从一张白纸开始;让我们首先使用 Turborepo 创建我们的单一代码仓库:

  1. 在终端中运行以下命令:

    pnpx create-turbo@1.6
    

    或者,你也可以运行以下命令:

    npx create-turbo@1.6
    
  2. 这将下载一些库,然后提示你选择你希望创建单一代码仓库的位置。让我们称它为 ebuy-ssr

  3. 在下一个分配包管理器的提示中,你可以选择你喜欢的。为了本章的目的,我们将选择 pnpm

  4. 让 Turborepo 去执行其任务,并在过程完成后,你可以在 ebuy-ssr 文件夹中执行以下命令:

    pnpm dev
    
  5. 注意它会在 30003001 端口分别启动两个应用程序,webdocs。在浏览器中打开 http://localhost:3000http://localhost:3001,查看真正简约的默认页面。

  6. 在你的 IDE 中打开 ebuy-ssr 文件夹,查看文件夹结构。

    它看起来可能像这样:

    .└── ebuy-ssr/
    ├── apps/
    │   ├── docs
    │   └── web
    ├── packages/
    │   ├── eslint-config-custom
    │   ├── tsconfig
    │   └── ui
    ├── package.json
    └── turbo.json
    

    我们需要考虑的关键文件和文件夹如下:

    • apps:这个文件夹将存放我们所有的微应用。

    • packages:这是我们存放所有实用工具、共享组件、库等的文件夹。它相当于 Nx 中的 libs 文件夹。

    • package.jsonpackage.json 文件在 turbo 单一代码仓库的功能中起着至关重要的作用。

    • turbo.json:这是我们定义 Turborepo 配置的文件。

Turborepo 和 Nx 之间的差异

虽然 Turborepo 和 Nx 都为我们管理单一代码仓库,但它们的方法有所不同。Nx 像是一层薄薄的抽象层,使我们能够通过配置来管理我们的单一代码仓库;我们倾向于高度依赖 NX 及其命令来构建和管理我们的单一代码仓库;NX 真正为我们承担了所有的重活。另一方面,Turborepo 非常轻量级,更多地依赖于 npm 包管理器的标准来管理单一代码仓库。Turborepo 的方法是在后台保持隐形,让开发者完全控制他们如何管理单一代码仓库。这也意味着当你使用 Turborepo 管理单一代码仓库时,你需要做更多的工作。

设置我们的微应用

如我们所见,在我们的apps文件夹中默认创建了两个应用,webdocs。我们将首先将web文件夹重命名为home,暂时删除docs文件夹:

  1. web文件夹重命名为home,删除docs文件夹。确保您在apps/home/package.json中更新名称属性为"name": "home",因为这是 Turborepo 用来识别应用的方式。

  2. 当我们打开文件时,让我们定义它在开发模式中运行的端口。更新apps/home/package.json中的开发脚本为"dev": "next dev --****port 3000"

注意,使用 Turborepo,我们有多个package.json文件。根文件夹中的package.json文件用于管理管理 monorepo 所需的dev依赖项,以及 monorepo 中所有应用所需的通用dev依赖项。我们也可以在那里定义我们的通用脚本命令。

每个应用文件夹中的package.json文件用于管理每个应用的工作空间和依赖项。这里的主要优势是每个微应用都有自己的npm_modules文件夹,从而确保每个团队在管理他们的包和依赖项方面完全独立。

在 Next.js 中创建页面和组件

让我们开始在我们的各自的微应用中创建一些组件。

  1. 使用 Next.js 创建组件与在其他 React 应用中创建组件的方式非常相似;我们通常创建一个components文件夹,并将我们的组件保存在其中。

  2. 当涉及到路由时,Next.js 12 使用基于文件系统的路由器;这意味着要创建一个新的路由。我们需要在/``pages/about-us.tsx中创建一个带有路由名称的文件。

  3. 让我们创建我们的组件。由于我们将使用semantic-ui来构建我们的组件,让我们继续在我们的微应用包管理器中添加它们作为依赖项。

  4. 在微应用的apps/home文件夹中运行pnpm add semantic-ui-react semantic-ui-css

  5. 然后,在主页文件夹中创建一个名为/components的文件夹,并在其中创建Header组件。

  6. /apps/components/Header.tsx文件中,添加以下代码:

    import { Menu, Container, Icon, Label } from "semantic-ui-react";import Link from "next/link";
    export function Header() {
      return (
        <Menu fixed="top" inverted>
          <Container>
            <Menu.Item as="a" header>
              eBuy.com
            </Menu.Item>
            <MenuItems />
            <Menu.Item position="right">
              <Label>
                <Icon name="shopping cart" />0
              </Label>
            </Menu.Item>
          </Container>
        </Menu>
      );
    }
    const MenuItems = () => {
      return (
        <>
          {NAV_ITEMS.map((navItem, index) => (
            <Menu.Item key={index}>
              <Link href={navItem.href ?? "#"}>{navItem.label}</Link>
            </Menu.Item>
          ))}
        </>
      );
    };
    interface NavItem {
      label: string;
      href?: string;
    }
    const NAV_ITEMS: Array<NavItem> = [
      {
        label: "Catalog",
        href: "/catalog",
      },
      {
        label: "Checkout",
        href: "/checkout",
      },
    ];
    export default Header;
    

    上述代码与我们之前章节中使用的Header组件的代码非常相似。它只是用于显示菜单项和迷你篮子的标记。

  7. 接下来,让我们将标题包含在我们的主页应用中。

    使用 Next.js,如果我们想让代码在所有页面中可用,我们可以在/pages文件夹中创建一个名为_app.tsx的文件,并将我们的相关代码放在那里,这正是我们将要做的,以便让Header组件在所有页面中显示。

  8. apps/home/pages文件夹中创建一个名为_app.tsx的新文件,并包含以下代码:

    import { AppProps } from "next/app";import Head from "next/head";
    import { Container } from "semantic-ui-react";
    import "semantic-ui-css/semantic.min.css";
    import Header from “../components/Header”;
    function CustomApp({ Component, pageProps }: AppProps) {
      return (
        <>
          <Head>
            <title>Welcome to ebuy!</title>
          </Head>
          <main>
            <Header />
            <Container style={{ marginTop: "5rem" }}>
              <Component {...pageProps} />
            </Container>
          </main>
        </>
      );
    }
    export default CustomApp;
    
  9. 运行pnpm dev并验证Header组件是否显示在http://localhost:3000上。

  10. 现在,我们将创建我们的目录微应用。只需复制主页应用并重命名文件夹为catalog

  11. 打开位于 apps/catalog/package.json 文件中的 catalog 的 package.json 文件,并进行一些小的修改。

  12. 将应用名称更改为 "name": "catalog";同时,让我们也将端口更改为运行在 3001

    "dev": "next dev --port 3001".
    
  13. 现在,让我们在 components 文件夹中创建我们的产品卡片组件。

  14. apps/catalog/components/ProductCard.tsx 文件中创建一个新文件,以下是其代码:

    import { Button, Card, Image } from "semantic-ui-react";export function ProductCard(productData: any) {
      const { product } = productData;
      return (
        <Card>
          <Card.Content>
            <Image alt ={product.title}
     src={product.image} />
            <Card.Header>{product.title}</Card.Header>
            <Card.Description>{product.description}</Card.Description>
            <Card.Header>${product.price}</Card.Header>
          </Card.Content>
          <Card.Content extra>
            <div className="ui three buttons">
              <Button basic color="red">
                Remove
              </Button>
              <Button basic color="blue"></Button>
              <Button basic color="green">
                Add
              </Button>
            </div>
          </Card.Content>
        </Card>
      );
    }
    export default ProductCard;
    

    再次强调,这与我们在 第五章 中创建的 ProductCard 组件非常相似。这是一个基本的标记,用于显示产品图片、产品名称和价格,以及添加到购物车按钮。

  15. 随意删除 catalog/components 中的 Header.tsx 文件,并从 _app.tsx 文件中移除其引用,因为我们已经在主页应用中有了它,并且在这里不会使用它。

  16. 接下来,为了节省我们一些时间,让我们将 product-list-mocks.tsx 文件从 第四章 复制粘贴到 apps/catalog/mocks 文件夹中。当我们在这里时,也让我们将包含产品图片的 assets 文件夹从 github.com/PacktPublishing/Building-Micro-Frontends-with-React-18/tree/main/ch4/ebuy/apps/catalog/src/assets 复制并粘贴到 /apps/catalog/public/assets

  17. 接下来,在 apps/catalog/pages/index.tsx 文件中,让我们添加以下代码:

    import { Card } from "semantic-ui-react";import ProductCard from "../components/ProductCard";
    import { PRODUCT_LIST_MOCKS } from "../mocks/product-list-mocks";
    export function ProductList() {
      return (
        <Card.Group>
          {PRODUCT_LIST_MOCKS.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </Card.Group>
      );
    }
    export default ProductList;
    
  18. ebuy-ssr 文件夹的根目录运行 pnpm dev 并验证 homecatalog 应用是否按预期工作。以下是我们的应用 URL:

图 6.3 – 在端口 3000 上运行的首页微应用

图 6.3 – 在端口 3000 上运行的首页微应用

图 6.4 – 在端口 3001 上运行的目录微应用

图 6.4 – 在端口 3001 上运行的目录微应用

现在我们已经让我们的独立应用运行,让我们努力通过模块联邦将目录微应用加载到主页应用中。

设置模块联邦

现在我们已经让我们的应用独立运行,是时候通过模块联邦将目录应用嵌入到主页应用中了。对于使用 Next.js 的模块联邦,我们将使用专门的 nextjs-mf npm 模块。按照以下步骤操作:

  1. 让我们首先在目录应用中安装 nextjs-mf npm 模块以及 webpack:

    pnpm add @module-federation/nextjs-mf webpack
    
  2. 我们现在需要将目录应用作为远程暴露出来;我们在 app/catalog/next.config.js 文件中这样做。

  3. 我们用以下内容替换 next.config.js 文件的内容:

    const NextFederationPlugin = require("@module-federation/nextjs-mf");// this enables you to use import() and the webpack parser
    // loading remotes on demand, not ideal for SSR
    const remotes = (isServer) => {
      const location = isServer ? "ssr" : "chunks";
      return {
        catalog: `catalog@http://localhost:3001/_next/static/${location}/remoteEntry.js`,
      };
    };
    module.exports = {
      webpack(config, options) {
        config.plugins.push(
          new NextFederationPlugin({
            name: "catalog",
            filename: "static/chunks/remoteEntry.js",
            exposes: {
              "./Module": "./pages/index.tsx",
            },
            remotes: remotes(options.isServer),
            shared: {},
            extraOptions: {
              automaticAsyncBoundary: true,
            },
          })
        );
        return config;
      },
    };
    

    查看代码,我们首先导入NextFederationPlugin,然后定义远程,包括其名称和remoteEntry.js文件所在路径。Next.js 为其应用创建两个构建版本——一个用于服务器,另一个用于客户端。请注意,我们根据执行位置从ssrchunks文件夹有条件地加载remoteEntry.js文件。

  4. 接下来,我们定义 webpack 配置,在其中设置NextFederationPlugin的属性,即名称和它暴露的内容,如下所示:

    exposes: {  "./Module": "./pages/index.tsx",
       },
    

我们可以定义远程数组,并让目录微应用中的不同组件或页面在其他应用中加载。这完成了目录端的设置。

创建结账微应用

为了完整性,让我们通过复制目录应用并重命名文件夹为checkout来创建checkout微应用。按照以下步骤操作:

  1. 让我们对apps/checkout/package.json文件进行必要的更改,如下所示:

    "name": "checkout",
    
  2. 然后,更新端口号:

    "dev": "next dev --port 3002",
    
  3. 现在,在apps/checkout/components/Basket.tsx中创建一个名为Basket.tsx的文件,并包含以下代码:

    import { Table, Image, Container } from "semantic-ui-react";export function ShoppingBasket(basketListData: any) {
      const { basketList } = basketListData;
      return (
        <Container textAlign="center">
          <Table basic="very" rowed=”true”>
            <Table.Header>
              <Table.Row>
                <Table.HeaderCell>Items</Table.HeaderCell>
                <Table.HeaderCell>Amount</Table.HeaderCell>
                <Table.HeaderCell>Quantity</Table.HeaderCell>
                <Table.HeaderCell>Price</Table.HeaderCell>
              </Table.Row>
            </Table.Header>
            <Table.Body>
              {basketList.map((basketItem: any) => (
                <Table.Row key={basketItem.id}>
                  <Table.Cell>
                    <Image alt={ basketItem.title } src={basketItem.image} rounded size=”mini” />
                  </Table.Cell>
                  <Table.Cell> {basketItem.title}</Table.Cell>
                  <Table.Cell>{basketItem.quantity || 1}</Table.Cell>
                  <Table.Cell>£{basketItem.price||1 * basketItem.quantity}</Table.Cell>
                </Table.Row>
              ))}
            </Table.Body>
          </Table>
        </Container>
      );
    }
    export default ShoppingBasket;
    
  4. 让我们还将apps/checkout/pages/index.tsx文件的内容进行更改,以确保结账应用通过传递正确的信息集加载basket组件:

    import { Container, Header as Text } from "semantic-ui-react";import ShoppingBasket from "../components/Basket";
    import "semantic-ui-css/semantic.min.css";
    import { PRODUCT_LIST_MOCKS } from "../mocks/product-list-mocks";
    export function App() {
      return (
        <Container style={{ marginTop: "5rem" }}>
          <Text size="huge">Checkout</Text>
          <ShoppingBasket basketList={PRODUCT_LIST_MOCKS} />
        </Container>
      );
    }
    export default App;
    
  5. 现在,让我们更新apps/checkout/next.config.js中的模块联邦配置,以将结账应用设置为远程。

  6. 让我们更新远程数组以反映名称结账,并将端口更新为3002,如下代码片段所示:

    return {    checkout: `checkout@http://localhost:3002/_next/static/${location}/remoteEntry.js`,
      };
    The next set of changes in the same file are here
      new NextFederationPlugin({
            name: "checkout",
            filename: "static/chunks/remoteEntry.js",
            exposes: {
              "./Module": "./pages/index.tsx",
            },
    . . .
    

让我们快速检查应用,看看结账应用是否正确加载,通过在根目录运行pnpm dev并在浏览器中访问以下 URL——http://localhost:3002

设置主机应用

现在,让我们专注于主应用:

  1. 我们需要再次安装module-federation/nextjs-mf npm包和 webpack:

    pnpm add @module-federation/nextjs-mf webpack
    
  2. 完成后,通过更新apps/home/next.config.js文件,将主机应用设置为主机,如下所示:

    const NextFederationPlugin = require("@module-federation/nextjs-mf");const remotes = (isServer) => {
      const location = isServer ? "ssr" : "chunks";
      return {
        catalog: `catalog@http://localhost:3001/_next/static/${location}/remoteEntry.js`,
        checkout: `checkout@http://localhost:3002/_next/static/${location}/remoteEntry.js`,
      };
    };
    module.exports = {
      webpack(config, options) {
        config.plugins.push(
          new NextFederationPlugin({
            name: "home",
            filename: "static/chunks/remoteEntry.js",
            exposes: {},
            remotes: remotes(options.isServer),
            shared: {},
            extraOptions: {
              automaticAsyncBoundary: true,
            },
          })
        );
        return config;
      },
    };
    
  3. 由于我们希望在目录路由中加载目录微应用,我们将在apps/home/pages/下创建一个名为catalog.tsx的新文件,并包含以下代码:

    import dynamic from "next/dynamic";const Catalog = dynamic(() => import("catalog/Module"), {
      ssr: true,
    });
    export default function catalog() {
      return <Catalog />;
    }
    
  4. 让我们在apps/home/pages/checkout.tsx中创建一个类似的文件名为checkout,并包含以下类似代码:

    import dynamic from "next/dynamic";const Checkout = dynamic(() => import("checkout/Module"), {
      ssr: true,
    });
    export default function checkout() {
      return <Checkout />;
    }
    

    如您所见,我们首次导入 Next.js 的动态模块,这是使用 Next.js 动态导入的推荐方式。

    您可以选择动态导入模块以执行客户端,通过设置ssr:false;这将使模块在客户端执行并绕过 SSR。这适用于您的模块包含个性化内容时,例如推荐、订单历史等。

    然后,我们定义名为Catalogconst并从catalog/Module导入它。请注意,TypeScript 抛出一个错误。这是因为我们没有为其定义类型。

  5. 因此,让我们快速创建以下内容的 /apps/home/remotes.d.ts 文件:

    declare module "catalog/Module";declare module "checkout/Module";
    
  6. 让我们通过关闭所有运行的服务器来测试一切。

    killall node 是一个非常有用的命令,可以杀死所有节点进程。

  7. 运行 pnpm dev 并访问 http://localhost:3000。点击目录并检查应用以查看相应的微应用加载。

注意

你可能需要将目录中的 public/assets 文件夹复制到宿主应用中。

图 6.5 – 在目录路由上加载的目录微应用

图 6.5 – 在目录路由上加载的目录微应用

以下截图显示了在检查路由上加载的检查微应用:

图 6.6 – 在检查路由上加载的检查微应用

图 6.6 – 在检查路由上加载的检查微应用

恭喜!!我们现在拥有了一个完整的服务器端渲染微前端。

让我们回顾一下到目前为止我们所学的。我们首先使用 Turborepo 和 Next.js 创建了各自的微应用,并了解了 Turborepo 的文件夹结构以及它与 Nx 的区别。然后我们使用 Next.js 创建了微应用,最后我们看到了如何设置模块联邦以在不同的路由中加载不同的微应用。

摘要

我们已经到达了本章的结尾,我们学习了客户端渲染和服务器端渲染应用之间的区别,以及哪种类型的应用适合哪种。我们探讨了构建 SSR 应用程序的各种选项,并专注于使用 Next.js 和 Turborepo 构建我们的模块联邦应用。然后我们看到了如何使用 next.js-mf 插件设置模块联邦,并着手设置我们的远程和宿主应用。最后,我们看到了如何将这些模块动态导入到宿主应用中,并设置不同应用之间的路由。

作为本章的一个挑战目标,你可以探索设置共享状态管理解决方案或共享组件库,按照我们在 第五章 中采取的方法进行。

在下一章中,我们将学习如何将我们的应用部署到云端。另一边见!

第三部分:部署微前端

本部分讨论了部署微前端的策略,包括部署到静态托管平台以及在 Azure 上使用 Kubernetes 进行容器编排。它涵盖了部署的实际考虑因素。

本部分包含以下章节:

  • 第七章将微前端部署到静态存储

  • 第八章将微前端部署到 Kubernetes

第七章:将微前端部署到静态存储

从本章开始,事情开始变得有趣,因为我们现在正走出前端/React 世界,进入云和全生命周期工程的领域。

如您从本书的早期章节所回忆的那样,微前端架构的一个主要目标是确保我们不需要在每次进行小幅度修改时部署整个应用,而是只需部署已更改的微应用。因此,一本关于微前端的书籍如果不涵盖将我们的微前端正确部署到生产环境的关键主题,就不能被认为是完整的。

当涉及到部署单页应用(SPAs)时,我们通常运行 webpack 的 build 命令来生成我们的 JavaScript 打包文件和资产,存放在 /build/dist 文件夹中,然后我们只需将其复制到静态网站托管提供商那里,以便我们的应用对用户可用。然而,部署微前端要复杂得多。

在本章中,我们将看到如何将我们在 第五章 中构建的客户端渲染微前端部署到静态存储云提供商,如 Firebase。我们将涵盖以下主题:

  • 理解什么是静态存储

  • 设置 Firebase 托管

  • 学习如何使用 Nx 构建 production 打包

  • 学习如何仅构建和部署已修改的应用

到本章结束时,我们将使我们的微前端应用在 Firebase 上运行,并且我们还将创建仅构建和部署已修改应用的脚本。

技术要求

在我们浏览本章中的代码示例时,我们需要以下内容:

  • 一台至少有 8 GB RAM(16 GB 更佳)的 PC、Mac 或 Linux 桌面/笔记本电脑

  • 英特尔芯片组 i5+ 或 Mac M1+ 芯片组

  • 至少 256 GB 的空闲硬盘存储空间

您还需要在您的计算机上安装以下软件:

  • Node.js 版本 18+(如果您必须管理不同版本的 Node.js,请使用 nvm

  • 终端:iTerm2 配合 OhMyZsh(您会感谢我的)

  • IDE:我们强烈推荐 VS Code,因为我们将会使用一些 VS Code 内置的插件来提升开发者体验

  • npm、Yarn 或 pnpm:我们推荐 pnpm,因为它速度快且存储效率高

  • 浏览器:Chrome/Microsoft Edge,Firefox

  • 对 Nx.dev 单一仓库有基本理解

  • 对 Firebase 和静态网站托管有基本理解会有所帮助

本章的代码文件可以在以下位置找到:

github.com/PacktPublishing/Building-Micro-Frontends-with-React

我们还假设您对 Git 有基本的操作知识,例如分支、提交代码和发起拉取请求。

什么是静态存储?

云托管提供商,如 AWS、Google 和 Azure,提供各种托管解决方案。静态存储,也称为 blob 存储,是指一种针对存储大量非结构化数据优化的存储服务,例如 二进制大对象Blob)。这些数据可以是任何类型,包括图像、视频、音频文件以及 HTML、CSS 和 JavaScript 等文本文件格式。

静态存储旨在高度可扩展,通常通过 内容分发网络CDN)提供服务。这使得它能够处理大量数据而不会降低性能,并且也使其非常耐用,通过在不同节点上的数据复制来确保数据不会因为硬件故障或其他中断而丢失。

关于静态存储的一个关键点是它没有任何计算能力;也就是说,它没有 CPU 或 RAM 资源。它只能提供静态文件。把它想象成一个连接到云的非常大的外部硬盘。

历史上,静态存储被用于存储和提供图像、JavaScript 或 CSS 文件,或者作为备份存储。它从未是托管 Web 应用的选择。然而,随着在浏览器上执行的单页应用(SPAs)的出现,前端工程师意识到他们可以使用存储来托管 JavaScript 和 CSS 包,并让应用在浏览器上执行和运行。现在,大多数托管提供商都正式提供静态站点托管服务。以下是一些流行的静态站点托管提供商:

  • Firebase

  • Netlify

  • Cloudflare

  • Azure 静态 Web 应用

  • Google Cloud Storage

  • Amazon S3

由于其简单性和非常低的成本,静态存储非常适合提供客户端渲染(CSR)的 React 应用。由于缺乏计算能力,它们不能用于提供后端或基于节点的基础 API,或执行 服务器端 渲染SSR)。

在我们的案例中,由于我们的微前端是客户端渲染的,我们将使用它来部署我们的应用。

在可用的各种托管选项中,我们将选择 Firebase 作为我们的托管解决方案,在下一节中,我们将介绍如何设置我们的 Firebase 应用程序。

顺便提一下,将微前端部署到任何其他托管提供商的过程将与本章其余部分中我们将要经历的类似过程相似。

设置 Firebase

Firebase,作为 Google 云平台的一部分,是一个极其易于使用且对开发者友好的托管提供商。Firebase 提供了大量的构建和管理 Web 和移动应用的服务和产品。

许多这些服务都有免费层,这使得它们非常适合构建和测试。您可以通过访问 www.firebase.com 并使用您的 Google 账户登录来访问所有产品和服务。

一旦您登录到 Firebase,请转到 管理 控制台console.firebase.google.com/)。

创建一个新的项目。让我们称它为ebuy。在下一节中,我们将在这个项目中设置我们的站点。

设置具有多个站点的项目

我们将使用 Firebase 的托管服务来部署我们的应用程序。如果您不熟悉 Firebase 托管,我们强烈建议您访问firebase.google.com/docs/hosting并阅读相关内容:

  1. 一旦进入控制台,选择ebuy项目。

  2. 转到左侧导航面板上的构建 | 托管链接。点击开始按钮以启动向导,并按照步骤在ebuy项目中创建一个新的站点。

  3. 我们为每个构建的微应用都需要一个新的站点,所以在仪表板页面上使用添加另一个站点,然后继续创建五个站点。为了保持本章的一致性,让我们按照以下名称命名:

    • ebuy-app-shell.web.app

    • ebuy-catalog.web.app

    • ebuy-checkout.web.app

    • ebuy-recommendations.web.app

    • ebuy-datastore.web.app

注意,这些名称在整个 Firebase 中必须是唯一的。如果名称已被占用(这很可能会发生),您可以选择合适的名称或接受 Firebase 提供的建议。

一旦创建了这五个站点,记下这些站点将可用的 URL,因为我们稍后会需要它们。

安装和配置 Firebase CLI

接下来,我们需要安装 Firebase 工具并将它们连接到我们的项目和站点:

  1. 在终端中运行npm install -g firebase-tools

  2. 然后,运行firebase login。这将打开一个浏览器窗口并请求您登录到您的 Firebase 账户。

  3. 运行firebase init hosting。这将带您完成一系列步骤。如果一切顺利,您将看到新创建的.firebasercfirebase.json文件。

  4. 接下来,我们需要让 Firebase 知道哪个微应用应该部署到哪个目标站点。我们通过运行以下命令来实现。语法如下:

    firebase target:apply hosting <micro-app-name> <firebase-site-name
    
  5. 因此,在我们的情况下,考虑到我们的微应用名称和 Firebase 内创建的网站,我们的命令如下所示:

    1. firebase target:apply hosting app-shell ebuy-app-shell

    2. firebase target:apply hosting catalog ebuy-catalog

    3. firebase target:apply hosting checkout ebuy-checkout

    4. firebase target:apply hosting ebuy-recommendations

    5. firebase target:apply hosting store ebuy-datastore

一旦成功执行了这些命令,您会注意到在.firebaserc文件中创建了这些条目。

这完成了我们在 Firebase 方面的设置。在下一节中,我们将为生产构建准备我们的微前端。

创建微前端生产构建

如您所回忆的,到目前为止,我们只以开发模式运行并测试了我们的微前端,使用的是nx serve命令。为了将应用程序部署到托管服务器,它们需要以生产模式构建。

这通常在常规 React 应用程序中相当直接,但与我们的微前端相比,需要做更多的工作。

打开我们构建的 ebuy 应用程序,见 第五章,并按照以下步骤操作。让我们首先创建一个脚本来构建所有我们的应用程序:

  1. 在根目录下打开 package.json 文件,就像 serve:all 命令一样,让我们创建一个新的命令 build:all,如下所示:

    "build:all": "nx run-many --target=build"
    
  2. 运行 pnpm build:all 命令,让我们看看是否所有应用程序都构建成功。哎呀!你会注意到,尽管所有其他应用程序都构建得很好,app-shell 抛出了一些关于找不到 catalog/Modulecheckout/Module 等错误,等等。

    让我们深入探讨一下。

  3. 打开 remotes 数组是空的。这就是我们的应用程序外壳构建命令失败的原因,因为 webpack 不知道它需要从哪里获取 remoteEntry.js 文件的路径。

  4. 让我们将我们的远程列表添加到这个数组中。这应该与我们的 remoteEntry 文件中远程数组中的应用程序列表相匹配。

  5. 我们更新了 apps/app-shell/webpack.config.prod.js 文件中的远程数组如下:

      remotes: [
        ['catalog', 'https://ebuy-catalog.web.app/'],
        ['checkout', 'https://ebuy-checkout.web.app/'],
        ['store', 'https://ebuy-datastore.web.app/'],
      ]
    
  6. 现在,重新运行 catalogrecommendations 应用程序也需要在它们的 webpack.config.prod.js 文件中的远程数组。

    我们还注意到,由于我们的目录和结账应用程序最初并不是作为微前端远程应用程序构建的,它们有略微不同的配置,并且缺少 webpack.config.prod.js 文件。让我们首先修复这个问题。

  7. 首先,让我们在构建生产构建时复制并粘贴 .prod.js 文件。

  8. 因此,在它们各自的 project.json 文件中,我们在 build > configuration > production 对象内添加以下行,如下所示:

    //apps/catalog/project.json
     . . .
    "vendorChunk": false,
      "webpackConfig": "apps/catalog/webpack.config.prod.js"
            },
    //apps/checkout/project.json
     . . .
    "vendorChunk": false,
      "webpackConfig": "apps/checkout/webpack.config.prod.js"
            },
    

    这将确保所有应用程序都使用它们各自的 webpack.config.prod.js 文件来运行它们的生产构建。

  9. 现在,让我们更新我们的 apps/catalog/webpack.config.prod.js 文件中的远程路径数组。由于目录应用程序只有一个远程,即推荐微应用程序,我们的远程数组看起来如下:

      remotes: [
        ['recommendations', 'https://ebuy-recommendations.web.app/'],
      ],
    

    接下来,让我们为我们的推荐应用程序做同样的事情,这些应用程序使用 store 微应用程序作为远程。因此,在 apps/recommendations/webpack.config.prod.js 文件中,我们更新远程数组如下:

      remotes: [
        [‘store’, 'https://ebuy-datastore.web.app/'],
      ],
    
    1. 由于结账应用程序也需要使用存储作为远程,我们更新了 apps/checkout/webpack.config.prod.js 文件如下:
      remotes: [
        [‘store’, 'https://ebuy-datastore.web.app/'],
      ],
    
  10. 再次运行我们的 pnpm build:all 命令,以根据我们最新创建的 webpack 配置生成生产构建。

当构建成功时,查看项目根目录下的 /dist 文件夹,并验证所有我们的微前端文件夹是否位于 /dist/apps 内。注意它们的路径,因为我们将在下一节中使用它们。

在本节中,我们通过确保所有应用程序都使用了正确的 webpack 配置,包括 remoteEntry.js 文件的正确公共 URL,成功生成了我们的微前端的生产构建。

在下一节中,我们将看到如何将这些应用到 Firebase 上部署。

将我们的应用部署到 Firebase

使用 Firebase CLI 的deploy命令将我们的应用部署到 Firebase 相当简单。然而,在我们运行 Firebase 的deploy命令之前,我们需要让 Firebase 知道哪些微应用将部署到相应的 Firebase 网站上。我们在/firebase.json文件中完成这项操作。

将默认配置替换为以下内容:

 {
  "hosting": [
    {
      "target": "app-shell",
      "public": "dist/apps/app-shell",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
    {
      "target": "catalog",
      "public": "dist/apps/catalog",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "checkout",
      "public": "dist/apps/checkout",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    },
    {
      "target": "recommendations",
      "public": "dist/apps/recommendations",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ],
    },
    {
      "target": "store",
      "public": "dist/apps/store",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
  ]
}

如您所见,前面的代码是一个配置,其中我们有一个目标应用数组,并定义了 Firebase 应该查找每个微应用的包的文件夹路径。我们还设置了一些关于忽略不部署node_modules和重写规则的设置,这对于你想要每个微应用也能作为其各自网站内的独立 SPA 可用是至关重要的。

现在,我们已经准备好将我们的应用部署到 Firebase。让我们先手动运行以确保一切正常。

在项目的终端中运行以下命令:

firebase deploy --only hosting

这将生成一个包含许多文件的.firebase文件夹。别忘了将.firebase添加到你的.gitignore文件中。

让 Firebase 完成其工作,如果一切顺利,它应该会显示一个成功消息并打印出已部署的网站 URL 列表,如下所示:

图 7.1 – Firebase 成功部署后发布的网站 URL 列表

图 7.1 – Firebase 成功部署后发布的网站 URL 列表

太好了!让我们点击app-shell链接,检查我们是否可以看到我们的微前端。

哎,我们看到一个空白页...看看浏览器开发者工具的控制台,你会注意到问题所在。我们的浏览器因为跨源资源共享CORS)的原因阻止了对remoteEntry.js文件的调用。

图 7.2 – 由于缺少头部导致的 CORS 策略头部

图 7.2 – 由于缺少Access-Control-Allow-Origin头部导致的 CORS 策略头部

我们将在下一节中看到如何解决这个问题。

解决 CORS 问题

如果你曾经构建过 React 或其他任何 Web 应用,你将熟悉令人讨厌的 CORS 问题。这是浏览器出于安全原因阻止对外部域的调用,除非它看到明确的'Access-Control-Allow-Origin'头部。访问控制是在决定是否允许其域的资产在其他域上被消费和执行的 app 上设置的。

因此,为了使我们的微前端应用能够正常工作,宿主应用需要能够从每个微应用托管在的公共 URL 加载remoteEntry.js文件。这就是我们将在接下来的步骤中设置的。

使用 Firebase Hosting,这相当简单,我们可以在firebase.json文件中定义一个头部数组。

打开除app-shell以外的所有应用的/firebase.json文件,并在每个目标对象中定义我们想要设置的头部信息:

      "headers": [
        {
          "source": "**/*.@(eot|otf|ttf|ttc|woff||woff2|js|font.css|remoteEntry.js)",
          "headers": [
            {
              "key": "Access-Control-Allow-Origin",
              "value": "https://ebuy-app-shell.web.app"
            }
          ]
        }
https://ebuy-app-shell.web.app.
			Note that we need to add the headers array for every target app defined within the `firebase.json` file.
			Rerun `firebase deploy --only hosting` and now, you should be able to view all the sites working on `https://ebuy-app-shell.web-app/`.
			Deploying only the selected target
			Currently, the `firebase` command deploys all the micro-apps. If we wanted to deploy only one of the micro apps, we’d simply need to pass the target name as an argument:

firebase deploy --only hosting:


			So, if we wanted to deploy only `app-shell`, our command would look as follows:

firebase deploy --only hosting:app-shell


			This will be critical in the next section.
			Looking back at this section, we were able to deploy our apps to Firebase, and we also managed to fix the CORSs issue by setting `Access-Control-Allow-Origin` headers. We also saw the CLI syntaxes that allow us to deploy only the apps that we need.
			In the next section, we will use these CLI commands in combination with another nifty command from Nx to control and deploy only the apps that changed.
			Deploying only Micro Apps that changed
			To be able to deploy only the micro apps that have been impacted by modifications to a file, we basically need to be able to do two things:

				1.  Identify which apps have been impacted due to changes to a given set of files
				2.  Only build and deploy the micro-apps that have been impacted

			For the second point, from the previous section, we now know how to let the Firebase CLI know which micro-app we would like to be deployed. We will look at how to achieve the first point in the next subsection.
			NX Affected
			The NX dev tools come with a handy command called `nx affected`, which is able to keep track of what files changed from the previous commit and highlight the apps that have been impacted due to the changes to these files.
			This is a nifty feature that can be used for various purposes, such as speeding up the execution of tests by running unit tests or build commands only against projects that have been impacted by changes to certain files – or, in our case, deploying only the micro-apps that have changed.
			To give it a quick try, run `git add.`  and `git commit` to commit all the changes we have made so far. Try and make a small visual change to `apps/app-shell/src/app/app.tsx`. Save the file and run the following command:

pnpm nx print-affected --type=app --select=projects


			It should print out `app-shell` as the app that was modified. Now, try and make changes to `libs/mocks/src/lib/product-list-mocks.tsx` and run the same command. You will see the catalog and checkout apps also added to the list of apps that are affected.
			The way the `nx affected` command works is by comparing the difference between the SHAs of the main branch and the current `HEAD`. You can pass in additional parameters to the affected command to compare the difference between any base and head and run a command passed to the target flag:

pnpm nx affected --target=deploy --base=main --head=HEAD


			`--target` is the custom command to run, `--base` is the base you want to compare against, and `--head` is the tip of your Git branch.
			This will probably return a message saying **Nx successfully ran target deploy on 0 projects**. This is because we haven’t created our custom deploy command yet.
			To get a deeper understanding of the various options for `nx affected`, have a read here: [`nx.dev/nx/affected#affected`](https://nx.dev/nx/affected#affected).
			In addition to affected, you may also find the `nx graph` command useful for getting a nice, visual representation of the various micro-apps consuming the different shared components and utilities form the `libs` folder.
			Try running `pnpm nx affected:dep-graph` to get a visual graph of how the modified files impact the micro-apps.
			Here is an example of how changes to the `libs/mocks/src/lib/product-list-mocks.tsx` file impact both the catalog and checkout apps, because both these apps import the product list from the `product-list-mocks` file:
			![Figure 7.3 – nx affected:dep-graph highlighting the projects impacted due to a change in mocks](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/bd-mfe-rct18/img/Figure_7.03_B18987.jpg)

			Figure 7.3 – nx affected:dep-graph highlighting the projects impacted due to a change in mocks
			Note
			nx graph or nx affected doesn’t take into account the host and remote features of module federation.
			Creating an Nx custom command executor to deploy
			Executors in Nx allow you to create custom script commands for a project, which you can run via the Nx command system.
			Please do take the time to read more about Nx custom command executors here: [`nx.dev/recipes/executors/run-commands-executor#3.-run-the-command`](https://nx.dev/recipes/executors/run-commands-executor#3.-run-the-command).
			Let's create a custom command to deploy an individual micro app.
			In `apps/app-shell/project.json`, add the following code within the target attribute:

"deploy": {

"executor": "@nrwl/workspace:run-commands",

"options": {

"commands": ["firebase deploy --only hosting:app-shell"],

"parallel": true

}

}


			Add the deploy custom command to each of the micro-app’s `project.json` files. Pass the correct micro-app name in the argument.
			Once that is done, try making a small change in the mocks file and run the following two commands:

pnpm nx affected -–target=build

pnpm nx affected -–target=deploy


			Assuming Nx has detected the difference correctly, it will only build the catalog and checkout apps and you will also notice that these are the only two apps that deployed to Firebase.
			You can verify that by going into Firebase Console’s hosting dashboard and checking the timestamp of when the apps were last deployed:
			![Figure 7.4 – Firebase Console displaying the deployed timestamp of modified apps](https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/bd-mfe-rct18/img/Figure_7.04_B18987.jpg)

			Figure 7.4 – Firebase Console displaying the deployed timestamp of modified apps
			Navigate to [`ebuy-app-shell.web-app/`](https://ebuy-app-shell.web-app/) (use the correct URL as displayed in your Firebase Console) and verify that everything continues to work fine and that the changes you’ve made reflect on the app. You may need to do a hard reload on your browser to view the updates.
			And with this, we’ve successfully managed to deploy only the apps that have changed while ensuring that the rest of the app works as expected.
			Summary
			With that, we come to the end of this chapter, where we learned about static storage hosting and why it is ideal for deploying and serving client-side-rendered React apps. We saw how to build production bundles for our module-federated micro app. We then saw how to set up a multi-site project in Firebase and used Firebase CLI commands to deploy our apps. We also saw how to address CORS issues by setting the right header values for the `Access-Control-Allow-Origin` header, and then finally, we saw how to combine the `nx affected` command and Firebase’s `hosting:<app-name>` command to detect the micro-apps that have been impacted by a change and only build and deploy them to Firebase. We also used this as an opportunity to create a custom command executor to deploy these affected apps.
			In the next chapter, we will go deeper into DevOps and cloud territory by seeing how to deploy our microfrontends to a managed Kubernetes cluster.

第八章:将微前端部署到 Kubernetes

在上一章中,我们学习了如何手动将我们的微前端部署到静态存储提供商,如 Firebase。

在本章中,我们将通过学习如何将我们的应用程序部署到托管 Kubernetes 集群来深入了解云和 DevOps 领域。Kubernetes 已成为将企业级 Web 应用程序(包括后端和前端)部署到云中的事实标准。

当涉及到部署单页应用程序(SPAs)时,我们通常运行 webpack 的build命令来生成我们的 JavaScript 包和资产在/build/dist文件夹中,然后我们只需将其复制到静态网站托管提供商,以便我们的应用程序可供用户使用。然而,部署微前端要复杂得多。

在本章中,我们将看到如何将我们的模块联邦微前端部署到托管 Kubernetes 集群。

我们将涵盖以下主题:

  • 如何使用 Docker 容器化我们的应用程序

  • Kubernetes 及其各种组件的基本知识

  • 一些基本命令来管理我们的 Kubernetes 集群

  • DevOps 以及如何自动化部署我们的微应用程序到 Kubernetes

到本章结束时,我们将使我们的微前端应用程序在 Azure 上的 Kubernetes 集群中运行。我们将通过自动化的持续集成CI)和持续交付CD)管道来部署它们,该管道将在代码合并时自动构建和部署必要的应用程序。

技术要求

除了我们在前几章中提到的所有标准技术要求之外,你还需要以下内容:

  • Azure 云订阅

  • 访问 GitHub 和 GitHub Actions

  • 对 CI 和 CD 概念的高级理解

  • 了解 Docker 和应用程序容器化将有所帮助

本章的代码文件可以在以下 URL 找到,我们基本上从第六章中构建的微前端开始:github.com/PacktPublishing/Building-Micro-Frontends-with-React

我们还假设你具备 Git 的基本操作知识,例如分支、提交代码和发起拉取请求。

Kubernetes 简介

Kubernetes,也称为K8s,已经席卷了云和 DevOps 世界。最初由谷歌开发,现在成为云原生计算基金会的一部分,Kubernetes 提供所有必要的工具,通过单一界面在云上部署和管理大规模、关键任务应用程序。

传统上,在云上管理大规模、生产级应用程序意味着必须处理诸如 Web 服务器、负载均衡器、自动扩展以及内部和外部流量路由等问题。现在,Kubernetes 将所有这些整合到一个统一的框架下,并提供了一种一致的方式来管理云环境中的所有组件。

Kubernetes 的前提是您通过规范文件告诉它您想要的最终状态,然后 Kubernetes 会着手为您完成。例如,如果您告诉 Kubernetes 您想要三个具有服务负载均衡器的应用程序副本,Kubernetes 将会计算出如何启动三个副本,并确保流量在三个副本之间均匀分布。如果由于某种原因,其中一个 Pod 重新启动或关闭,Kubernetes 将自动启动一个新的 Pod,以确保在任何给定时间都有三个 Pod 副本的服务流量。同样,当您部署应用程序的新版本时,Kubernetes 将负责逐步启动具有最新应用程序版本的新的 Pod,同时优雅地关闭具有旧版本应用程序的 Pod。

在本节的剩余部分,我们将探讨 Kubernetes 的关键组件,以及如何在 Kubernetes 上部署我们的微前端架构。

什么是 Kubernetes?

Kubernetes 是一个平台无关的容器编排平台,它能够在机器集群中部署、扩展和管理容器化应用程序。

它抽象了底层基础设施,允许您在各种环境中运行应用程序,包括本地数据中心、公共云提供商(如微软 Azure、谷歌云平台和亚马逊网络服务),甚至在自己的笔记本电脑上。

Kubernetes 被设计成高度模块化和可扩展的,并且它集成了各种工具和服务,以支持应用程序的完整生命周期,包括部署、扩展、监控和维护。它在业界得到了广泛的应用,并已成为容器编排的事实标准。

Kubernetes 的关键概念

Kubernetes 可以是一个非常广泛的话题,需要专门的领域来深入探讨。您可以在以下链接中查看 Kubernetes 各个组件的详细信息:kubernetes.io/docs/concepts/overview/components。然而,作为一个前端工程师,以及本书的范围,您需要了解以下六个基本概念和术语:

  • Nodes:节点是 Kubernetes 集群中的工作机器。它可以是物理机或虚拟机,负责运行部署到其上的容器化应用程序。

  • Pods:Pod 是 Kubernetes 应用程序的基本执行单元。它是一个或多个容器的逻辑宿主,Pod 中的所有容器都在同一节点上运行。Pod 为容器提供了一个共享的上下文,例如共享存储和网络。

  • Services:服务是一组 Pod 上的逻辑抽象。它定义了访问 Pod 的策略,通常是通过一个稳定的 IP 地址或 DNS 名称。服务允许您解耦应用程序之间的依赖关系,使您能够在不影响服务消费者的情况下扩展或更新一组 Pod。

  • Deployments:部署是一种声明式管理 ReplicaSet 的方式,ReplicaSet 是一组部署到集群中的相同 Pod。部署允许您指定应用程序的期望状态,Kubernetes 将确保实际状态与期望状态相匹配。这包括滚动更新、回滚和自我修复。

  • Ingress:Ingress 是将您的服务暴露给外部世界的一种方式。它提供了一种将外部流量映射到集群中特定服务的方法,通常是通过一个稳定的 IP 地址或 DNS 名称。Ingress 还可以提供额外的功能,例如 SSL 终止和负载均衡。将其想象成一个将 URL 映射到服务的路由器。

  • Namespaces:命名空间是 Kubernetes 集群中的一个逻辑分区。它允许您在不同的上下文中使用相同的资源(例如名称),并且可以用于在集群内隔离资源。

Kubernetes 微前端架构

当我们在 Kubernetes 上部署微前端时,为每个微应用创建一个 Pod,并且这个微应用通过 Ingress 服务内部暴露。

主应用模块将所有这些微应用联邦起来。以下图表有助于更好地解释架构:

图 8.1 – 部署微前端使用的 Kubernetes 拓扑架构

图 8.1 – 部署微前端使用的 Kubernetes 拓扑架构

如您在 图 8**.1 中所见,我们的每个微应用都部署在其自己的 Pod 中。这些 Pod 可以根据流量增加进行复制或设置为自动扩展。这由围绕 Pod 的虚线框表示。这些 Pod 通过服务暴露,该服务充当一种负载均衡器。因此,主页应用服务是所有主页微应用 Pod 复制的单一端点。

每个服务都通过 Ingress 路由暴露。这是我们定义微应用 URL 的地方,这最终将用于我们的模块联邦配置。这就是整体 Kubernetes 架构将看起来像什么。

通过这一点,我们来到了本节的结尾,在这里我们了解了一些 Kubernetes 的关键概念,例如节点、Pod、服务、Ingress 以及我们 Kubernetes 集群内微应用的架构。在下一节中,我们将看到如何将我们的应用容器化,以便它可以部署到 Kubernetes 集群中。

使用 Docker 容器化我们的微应用

容器是一种以标准化和可移植的方式打包软件应用程序的方法,允许它们在不同的环境中一致地运行。它们提供了一种轻量级且高效的运行应用程序的方式,特别适合于由多个独立可部署服务组成的微服务架构。

在本节中,我们将探讨如何通过创建 Dockerfile 来安装 Docker 并创建 Docker 镜像。

安装 Docker

Docker Engine 可以通过 Docker Desktop 在多个 Linux、Mac 和 Windows 系统上供个人使用。您可以按照以下说明安装 Docker 引擎:docs.docker.com/engine/install/

注意

如果你不想在 Windows 或 Mac 上使用 Docker Desktop,有一些替代方案,例如 Rancher Desktop、Podman 和 Colima。

一旦安装了 Docker,请在终端中运行以下命令以验证它:

docker -v

如果它返回 Docker 的版本,那么你就设置好了,这意味着 Docker 已经在你的系统上成功安装。

创建独立应用构建

在我们开始创建 Docker 镜像之前,我们首先需要确保我们的微应用的构建输出是自包含的,并且可以以独立模式运行。我们通过在每个next.config.js文件中添加以下行来实现这一点,如下所示:

const path = require("path");
module.exports = {
  output: "standalone",
  experimental: {
    outputFileTracingRoot: path.join(__dirname, "../../"),
  },
…
}

outputFileTracingRoot是 Next.js 12+版本中引入的一个实验性功能;这有助于减少构建输出的体积,尤其是在我们想要尝试减少 Docker 镜像大小时。

确保将这些行添加到每个微应用的next.config.js文件中。

创建 Dockerfile

下一步是创建我们的 Dockerfile,其中包含 Docker 创建我们的 Docker 镜像的指令。

由于我们需要为每个微应用创建一个 Docker 镜像,我们将在apps/home中创建一个 Dockerfile。我们通常给这个文件取的名字是Dockerfile

让我们在 Dockerfile 中添加以下命令。我们将使用 Turborepo 和 Next.js 提供的默认 Dockerfile。

我们将构建一个多阶段 Dockerfile,这允许我们利用层的缓存,同时也确保 Docker 镜像的大小尽可能小。

我们将分三个阶段来构建它,首先是构建阶段:

FROM node:18-alpine AS base
FROM base AS builder
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=home --docker

如您所见,我们使用 Node Alpine 18.14 作为基础镜像,并将其称为构建阶段。Alpine 是 Node.js 最简约的版本。

现在,我们安装libc6-compact库并运行update命令。然后,我们设置应用程序的工作目录并安装 turbo。

然后,我们将从我们的 repo 复制所有内容(注意COPY命令中两个点之间的空格)。

最后,我们运行turbo prune命令以提取所有必要的文件,用于主微应用。

现在,我们将进入安装阶段,并在上一段代码之后立即编写以下代码:

FROM base AS installer
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN yarn global add pnpm
RUN pnpm install --no-frozen-lockfile
# Build the project
COPY --from=builder /app/out/full/ ./
COPY turbo.json turbo.json
RUN ENV=PROD yarn turbo run build --filter=home...

再次,我们首先定义基础镜像为安装程序,运行常规的apk addupdate命令,并设置工作目录。

然后,我们将.gitignore文件以及从/app/out文件夹中的相关文件从构建阶段复制过来。

我们接着安装pnpm并运行pnpm install 命令。

然后,我们将从我们的构建阶段的app/out/full文件夹中复制所有文件并运行turbo build命令。

然后,我们继续到最后一个运行阶段,在那里我们编写以下代码:

FROM base AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --from=installer /app/apps/home/next.config.js .
COPY --from=installer /app/apps/home/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/home/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/home/.next/static ./apps/home/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/home/public ./apps/home/public
CMD node apps/home/server.js

在前面的代码中,我们基本上创建了一个用户组来避免以 root 身份运行代码的安全风险,然后我们复制了从我们的安装阶段的相关文件并运行了node命令。

现在,我们需要在仓库的根目录中创建一个.dockerignore文件,其中我们列出我们不希望 Docker 复制到镜像中的文件和文件夹:

node_modules
npm-debug.log
**/node_modules
.next
**/.next

让我们测试 Dockerfile,看看它是否能够构建。从应用程序的根目录,在终端中运行以下命令:

docker build -t home -f apps/home/Dockerfile .

-t代表标签名称,它将创建一个名为home的 Docker 镜像。-f部分是 Dockerfile 的路径。

注意命令末尾的空格和句号,这是很重要的。命令末尾的句号表示构建上下文——即 Docker 构建镜像时应使用的文件和文件夹集合。句号还表示我们想要打包当前目录中的所有文件和文件夹。

这个命令在第一次运行时可能需要几分钟,因为 Docker 将下载基础节点镜像和其他依赖项。后续的构建将会快得多,因为 Docker 将缓存层并重用它们,如果层没有变化的话。

您可以通过运行以下命令在本地运行 Docker 镜像:

docker run -p 3000:3000 home

一旦我们验证了它运行正常,我们还需要为我们的每个应用程序创建类似的 Dockerfile。

因此,在apps/catalogapps/checkout中,复制并粘贴 Dockerfile,并将所有home实例替换为相关的微应用名称。

注意,这些微应用都在相同的端口3000上运行,因此为了在本地测试它们,我们一次只能测试一个镜像,除非您将hostPort值更改为不同的值或使用 docker-compose 文件。

现在我们已经学会了如何将我们的微应用 docker 化并在本地运行,我们将继续到下一节,即设置 Docker Hub。

设置 Docker Hub 以存储 Docker 镜像

在上一节中,我们创建了应用程序的 Docker 镜像并能够在本地运行它们。为了能够在 Kubernetes 上部署它们,我们需要将它们存储在一个容器库中,以便我们的 DevOps 管道可以从中拉取镜像。我们将使用像 Docker Hub 这样的免费工件注册解决方案来完成这项工作。或者,您可以使用各种托管提供商提供的其他容器注册解决方案,例如 Azure Container Registry、Google Container Registry 和 Amazon Elastic Container Registry:

  1. hub.docker.com登录/注册,然后为每个微应用创建三个公共仓库。我们将它们命名为以下:

    • ebuy-home

    • ebuy-catalog

    • ebuy-checkout

  2. 记下 Docker 注册表的路径,通常为<你的用户名>/ebuy-home格式,<你的用户名>/ebuy-catalog格式,等等。

  3. 然后,让我们创建一个访问令牌,它将用于我们的 CI 和 CD 管道。转到账户设置,在安全页面创建一个新的访问令牌,并为其提供描述。在访问权限下,选择读取和写入,因为我们的管道需要推送和拉取 Docker 镜像。

  4. 一旦生成令牌,请复制并妥善保管,因为它将不会再显示。(如果您丢失了旧令牌,您始终可以生成新令牌。)

我们在 Docker Hub 上的工作完成了!

在下一节中,我们将创建用于启动我们的 Kubernetes 集群的 Kubernetes 配置文件。

创建 Kubernetes 配置文件

在本章的早期部分,在Kubernetes 简介部分,我们学习了我们将使用来部署我们的微前端的各种 Kubernetes 服务。

在 Kubernetes 上部署这些服务通常是通过在.yaml文件中定义各种配置设置,然后将配置应用到 Kubernetes 集群来完成的。

在本节中,我们将了解这些 Kubernetes 规范文件的结构以及如何为我们的部署、服务和 Ingress 创建它们。

Kubernetes 规范文件的结构

Kubernetes 规范文件是一个 YAML 文档,它描述了 Kubernetes 对象(如 Deployment、Pod、Service 或 ConfigMap)的期望状态。Kubernetes 规范文件的结构通常由两个主要部分组成——元数据部分和规范部分。每个文件始终从定义apiVersion和规范文件的kind开始。

元数据部分包括有关对象的信息,例如其名称、标签和注解。该部分由 Kubernetes 用于管理对象并允许其他对象引用它。

规范部分包括对象期望的状态,例如容器镜像、资源请求和限制、网络配置以及任何其他相关设置。该部分由 Kubernetes 用于根据其期望状态创建和管理对象。

创建部署我们的微前端的规范文件

如我们之前所见,Kubernetes 规范文件的结构遵循分层格式,每个部分及其相应的属性都嵌套在适当的标题下。此外,许多 Kubernetes 对象具有特定于其类型的属性,因此规范文件的结构可能因描述的对象而异。

让我们从在每个微应用文件夹中的k8s文件夹内创建这些文件开始。

让我们从创建一个名为/apps/home/k8s/deployment.yml的文件开始,并包含以下代码。deployment.yml文件包含了设置和配置 Kubernetes pods 的配置,我们的微应用将在其中运行:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: home
  namespace: default
  labels:
    app: home
spec:
  replicas: 1
  selector:
    matchLabels:
      app: home
  template:
    metadata:
      labels:
        app: home
    spec:
      containers:
        - name: home
          image: <dockerUserID>/ebuy-home:latest
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 3000
              protocol: TCP

当你阅读deployment.yml配置文件时,你会看到我们将应用程序标记为home,并使用相同的名称来定义我们的容器名称。我们定义副本数量为 1,这意味着它将启动一个 pod;如果你想有多个 pod 副本,请将此数字增加到 2 或更多。然后,在文件的容器部分,我们定义它应使用的 Docker 镜像路径名称以及它应使用的端口和协议。用你的 Docker 仓库的值替换这些值。注意 Docker 镜像值末尾的:latest;这是我们添加的,以确保 Kubernetes 总是获取 Docker 镜像的最新版本。

现在,我们定义服务,它作为负载均衡器,在 pod 的一个或多个副本上工作。

创建一个新文件,名为/apps/home/k8s/service.yml,并包含以下代码:

kind: Service
apiVersion: v1
metadata:
  name: home
  namespace: default
  labels:
    app: home
spec:
  type: LoadBalancer
  selector:
    app: home
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
      name: home

service.yml文件相当简单,其中我们提供了必要的元数据,例如 Kubernetes 集群的namelabelnamespace

然后,在规格中,我们定义了这种服务的类型;我们将将其设置为LoadBalancer。这将帮助我们暴露一个公共 IP 地址,我们稍后会用到,最后,在ports部分,我们定义了我们将在此上暴露服务的协议和端口号。

最后,我们需要定义一个ingress.yml文件,我们将在这里为服务分配一个 URL。创建一个名为/apps/home/k8s/ingress.yml的文件,并包含以下代码。

Kubernetes 中的 Ingress 本质上在底层运行 nginx,所以如果你熟悉 nginx,配置这个应该很容易:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: home
  namespace: default
  labels:
    app: home
  annotations:
    # nginx.ingress.kubernetes.io/enable-cors: 'true'
    # nginx.ingress.kubernetes.io/cors-allow-origin: '*'
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /home(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: home
            port:
              number: 80

这个文件配置起来通常有点棘手,因为这是你定义 URL 结构和重写规则以及其他 nginx 配置的地方,就像为 Web 服务器做的那样。正如你所看到的,我们在注释下定义了常规元数据信息,并定义了各种重写规则和 nginx 配置,例如 CORS。然后,我们设置regex路径,它告诉 Kubernetes 通过哪些 URL 将流量导向此服务和 pod。最后,我们需要将 K8s 文件夹复制粘贴到我们的每个微应用中,并更新相关路径和应用程序名称以匹配微应用的名称。

当我们进入本节的结尾时,我们已经看到了如何创建 Kubernetes 规范文件来部署 pod,如何设置位于这些 pod 之上的服务,以及最后提供这些 pod 路由的 ingress。在下一节中,我们将创建一个 Azure Kubernetes 集群,我们将针对这些规范执行。

在 Azure 上设置托管 Kubernetes 集群

在本节中,我们将学习如何在 Azure 上设置一个托管 Kubernetes 集群。之所以称之为托管,是因为 Kubernetes 的大脑,即主节点,由 Azure 管理,我们只需要启动工作节点。我们将了解如何登录 Azure 并创建订阅密钥,以及我们将安装 Azure CLI 并收集我们 DevOps 管道所需的各项凭证。

对于本章,我们将使用Azure Kubernetes 服务AKS)来设置我们的基于云的托管 Kubernetes 集群。您也可以在 Google Cloud 上使用Google Kubernetes EngineGKE)设置托管 Kubernetes 集群,或者您可以在 AWS 上使用 Amazon Elastic Kubernetes ServiceEKS)。

无论您使用哪个托管提供商来设置您的 Kubernetes 集群,Dockerfile 和 Kubernetes 配置.yaml文件都保持不变。

登录 Azure 门户并设置订阅密钥

要在 Azure 平台上执行任何活动,您需要该平台的登录凭据和订阅密钥。我们在 Azure 中创建的所有资源都需要映射到一个订阅密钥,该密钥最终由 Azure 用来计算托管费用。为此,请按照以下步骤操作:

  1. 转到portal.azure.com并使用 Microsoft 登录;如果您没有,您始终可以注册一个。

  2. 登录到门户后,搜索订阅并添加一个按量付费订阅。如果您列表中有Azure for Student或免费试用订阅,也可以随意选择其中一个。此订阅将用于您在 Azure 中运行的各项服务所产生的所有托管费用。

  3. 然后,在搜索框中搜索资源组并创建一个资源组。让我们称它为ebuy-rgrg后缀代表资源组。它将选择您在早期步骤中创建的默认订阅。对于区域,您可以选择美国东部或您选择的区域;为了保持本章的一致性,我们将坚持使用美国东部

    在 Azure 中,为项目创建一个资源组,然后在该资源组中拥有与该项目相关的所有各种服务,始终是一个好习惯。这使我们能够轻松管理资源组内的资源,尤其是在我们想要关闭项目中的所有服务时。

  4. 接下来,我们将创建我们的 AKS 集群;搜索Azure Kubernetes Service (AKS),点击左上角的创建按钮,然后选择创建 Kubernetes 集群菜单项。您将看到一个屏幕,如下面的截图所示:

图 8.2 – 创建 Kubernetes 集群屏幕

图 8.2 – 创建 Kubernetes 集群屏幕

  1. 选择我们在早期步骤中创建的订阅和资源组,然后在集群预设配置中,选择生产标准作为预设配置。您也可以选择其他更高的配置;然而,请注意,AKS 集群是您 Azure 每月账单中最昂贵的组件。

  2. 将 Kubernetes 集群名称指定为ebuy,并选择您创建资源组的相同区域;在我们的案例中,它是(US) East US。对于 Kubernetes 版本,您可以选择将其保留为默认值或选择1.26.6以确保设置与章节中定义的代码和配置保持一致。对于缩放方法,将其设置为自动缩放,对于最大节点数,保留为12。最后,点击审查 + 创建,然后在进行验证检查后,点击创建

我们现在已经在 AKS 中运行了我们的 Kubernetes 集群。

通过 Azure CLI 访问您的 Kubernetes 集群

与 Azure 上的 Kubernetes 集群交互的事实上方法是使用 Azure CLI,您可以在learn.microsoft.com/en-us/cli/azure/找到它。如果您正在使用 Kubernetes,最好也安装 kubectl,安装说明请在此处查看kubernetes.io/docs/tasks/tools/install-kubectl-macos/

按照前述 URL 的文档在您的系统上设置 Azure CLI。

一旦 Azure CLI 启动并运行,下一步就是使用以下命令登录:

az login

成功登录后,它将显示您的订阅和租户详情。

运行以下命令中的几个,以了解 Azure CLI 和基本 Kubernetes 命令:

  • az aks list //: 获取所有 aks 集群的列表

  • az aks get-credentials --resource-group ebuy-rg --name ebuy //: 连接到您的 aks 集群

  • kubectl get nodes //: 获取所有节点的列表

  • kubectl get pods //: 获取所有正在运行的 Pods 的列表(我们还没有运行任何 Pods,所以如果您收到错误消息请不要担心)

这些只是一些帮助您开始的命令;如果您想了解 kubectl 命令的其余部分,请前往官方kubectl Cheatsheetkubernetes.io/docs/reference/kubectl/cheatsheet/.

一旦您对不同的 kubectl 命令感到满意,并且与您的 Kubernetes 集群交互感到舒适,我们将继续下一步,收集自动化部署所需的必要凭证。

为您的 DevOps 管道生成凭证

对于任何 DevOps 管道要访问 Azure 上的各种资源以启动 Kubernetes 集群,它将需要访问权限。

我们现在将收集必要的访问权限。请确保您已登录到portal.azure.com,或者通过az login CLI 命令登录。

以下是我们从 Azure 获取的 ID 和密钥列表以及如何在 Azure 门户中找到它们的过程:

  • 订阅 ID:搜索订阅并选择您的订阅以显示订阅 ID。

  • 租户 ID:搜索Azure Active Directory并记录显示的 Tenant_ID

  • 然后,我们需要创建一个服务主体,以便在我们的资源组内创建和管理资源;我们使用 az CLI 来完成这项工作。在终端中,执行以下命令,将{subscriptionid}替换为上一步骤中记录的值,将{resource-group}替换为资源组的名称;在这种情况下,它是ebuy-rg

    az ad sp create-for-rbac --name “MyApp” --role Contributor --scopes /subscriptions/{subscriptionid}/resourceGroups/ebuy-rg --sdk-auth
    

    执行命令,如果一切顺利,它将发布一个配置变量的列表,如图图 8.3所示,您可以轻松保存以供进一步使用。

图 8.3 – 创建服务主体命令的输出

图 8.3 – 创建服务主体命令的输出

记录前一步输出的配置,因为我们将在后续步骤中使用它。

现在我们已经拥有了所有必要的凭证,让我们继续下一节,设置 CI 和 CD 管道,我们将使用这些凭证。

使用 GitHub Actions 设置 CI/CD

在本节中,我们将学习如何使用 GitHub Actions 设置 DevOps 管道。DevOps 管道是我们定义的一系列步骤,用于自动化我们应用程序的构建和部署。在本节中,我们将学习如何设置 GitHub 密钥和.yml工作流程文件。

GitHub Actions 是 GitHub 提供的一个自动化和工作流程工具,允许开发者自动化软件开发工作流程并简化软件开发过程。使用 GitHub Actions,您可以直接从 GitHub 仓库创建自定义工作流程来自动化构建、测试、部署和发布代码。我们还可以使用 Jenkins、Azure DevOps、Google Cloud Build 等工具进行持续集成和持续部署。在本章中,我们将使用 GitHub Actions。

设置 GitHub 密钥

作为 CI 和 CD 步骤的一部分,GitHub Actions 需要将 Docker 镜像推送到 Docker Hub 并启动新的 Kubernetes pod 等。对于所有这些活动,它需要能够使用正确的凭证登录到系统。作为规则和安全起见,我们绝不应该直接将用户名或密码硬编码到 DevOps 管道中。正确的方式是创建 GitHub 密钥并在您的管道中使用它们。

首先,确保您已将我们迄今为止所做的最新更改提交并推送到 GitHub。

让我们首先转到 GitHub 仓库的 设置 选项卡,然后转到 秘密和变量 部分,来创建我们的 GitHub secrets。然后,在 动作 下,我们将创建以下秘密,以及之前从 Docker 和 Azure 订阅中记录的相应值:

AZ_CLIENT_ID
AZ_CLIENT_SECRET
AZ_SUBSCRIPTION_ID
AZ_TENANT_ID
DOCKERHUB_USERNAME
DOCKERHUB_TOKEN

我们将在 DevOps 管道中创建这些作为秘密。这些秘密可以在管道中通过 ${{ secrets.<variable-name> }} 访问。

开始使用 GitHub Actions

GitHub Actions 是 GitHub 提供的一个相对较新的功能,允许您创建工作流程来自动化任务。它也可以用来设置自动化的 CI/CD 管道,这正是我们在本章中将要使用的。

注意

您可以在此处详细了解 GitHub Actions:docs.github.com/en/actions

创建 GitHub action 非常简单。我们只需要在我们的项目文件夹根目录下创建一个名为 .github/workflows 的文件夹,然后创建一个 .yaml 文件。一旦推送到 GitHub,它将自动检测到您有一个工作流程文件,并按照触发器执行它:

  1. 让我们在 .github/workflows/home-build-deploy.yml 中创建我们的 .yaml 文件,并在其中编写以下代码:

    name: home-build-deploy
    on:
      workflow_dispatch:
      push:
        branches:
          - main
        paths:
          - apps/home/**
    

    我们将为我们的 GitHub action 提供一个名称;这是在 GitHub Actions 中显示的名称。然后,我们定义触发器,on push:on:workflow_dispatchworkflow_dispatch 触发器允许您在需要时手动触发管道(尤其是在测试您的管道时),如您所见,on push 有进一步的选项 branches: mainpaths: apps/catalog/**。这意味着对推送到 main 分支的 home micro-app 中的任何文件的更改将触发此管道。paths 部分对于确保管道只构建和部署更改的微应用至关重要。

  2. 现在,我们需要定义 GitHub actions 应该运行的作业列表;我们将如下进行:

    jobs:
      build-and-deploy:
        runs-on: ubuntu-latest
        strategy:
        permissions:
        steps:
    

    对于我们在管道中定义的每个作业,我们需要定义 DevOps 管道需要运行的操作系统、任何策略、提供的权限,以及最后需要运行的步骤。

    现在,我们将扩展到这些部分的每一个。

  3. 由于构建和部署微应用的命令保持不变,我们将使用一种矩阵策略,允许我们定义可以在这些步骤中使用的变量。在策略部分,编写以下代码:

    strategy:
          fail-fast: false
          matrix:
            include:
              - dockerfile: './apps/home/Dockerfile'
                image: areai51/ebuy-home
                k8sManifestPath: './apps/home/k8s/'
    

    我们将 fail-fast 选项设置为 false,这样即使其中一个微应用失败,GitHub action 也会继续运行其他微应用的管道。然后,我们定义我们的变量矩阵,如下所示:

    • Dockerfile: 微应用的 Dockerfile 在您的代码库中的路径

    • 图像: Docker Hub 中 Docker 图像的路径

    • k8sManifestPath: 启动微应用 pod、服务和 ingress 所需的 Kubernetes 清单文件的存储位置

对于权限,我们设置以下内容:

    permissions:
      contents: read
      packages: write

我们将contents范围设置为读取,将packages范围设置为写入。

接下来的步骤系列是实际工作发生的地方。

正如我们将看到的,每个步骤都有两个到三个属性——第一个是name;然后是uses,这是执行步骤所使用的组件;最后是with,它是可选的,定义了执行步骤所需的附加属性。

以下步骤中的所有代码都将位于.yml文件的steps:部分:

  1. 我们首先检出仓库:

        - name: Checkout Repository
          uses: actions/checkout@v3.3.0
    
  2. 然后,我们登录到 Docker Hub,使用用户名和访问令牌作为密码。注意,我们将它们作为秘密传递,这是我们之前定义的:

          - name: Login to Docker Hub
            uses: docker/login-action@v2
            with:
              username: ${{ secrets.DOCKERHUB_USERNAME }}
              password: ${{ secrets.DOCKERHUB_TOKEN }}
    
  3. 在下一步中,我们提取git SHA值,我们将使用它来标记我们的 Docker 镜像:

          - name: Extract git SHA
            id: meta
            uses: docker/metadata-action@v4
            with:
              images: ${{ matrix.image }}
              tags: |
                type=sha
    
  4. 下一个步骤是构建和推送命令,通过传递矩阵变量中的微应用名称来构建 Docker 镜像,然后使用git SHA 值作为镜像标签将其构建的 Docker 镜像推送到 Docker Hub:

          - name: Build and push micro app docker image
            uses: docker/build-push-action@v4.0.0
            with:
              context: "."
              file: ${{ matrix.dockerfile }}
              push: true
              tags: ${{ steps.meta.outputs.tags }}
    
  5. 一旦 Docker 镜像推送到 Docker Hub,我们就需要设置我们的 Kubernetes pods 和 services,为此我们首先需要设置Kubectl

          - name: Setup Kubectl
            uses: azure/setup-kubectl@v3
    
  6. 首先,我们使用客户端 ID 和客户端密钥登录 Azure:

          - name: Azure Login
            uses: Azure/login@v1
            with:
              creds: '{"clientId":"${{ secrets.AZ_CLIENT_ID }}","clientSecret":"${{ secrets.AZ_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZ_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZ_TENANT_ID }}"}'
    
  7. 接下来,我们设置 Kubernetes 上下文:

          - name: Set K8s Context
            uses: Azure/aks-set-context@v3
            with:
              cluster-name: ebuy
              resource-group: ebuy-rg
    
  8. 最后,我们运行 Kubernetes 部署命令:

          - name: Deploy to K8s
            uses: Azure/k8s-deploy@v4
            with:
              namespace: "default"
              action: deploy
              manifests: |
                ${{ matrix.k8sManifestPath }}
              images: |
                ${{ steps.meta.outputs.tags }}
    

    一旦你验证了文件中的所有缩进都是正确的,就提交文件到主分支。

    然后,对 home 应用中的任何一个代码文件进行微小修改,提交更改,并将其推送到 GitHub。提交更改后,转到github.com上的操作标签页,你应该能看到 GitHub 管道开始运行。

按照 GitHub Actions 的步骤逐步通过作业。如果有任何错误,作业将失败,因此请检查错误并进行必要的修复。在导航这个关键步骤时,随时向朋友和社区寻求帮助,并不断测试直到管道成功运行。

一旦管道成功构建,复制.github/workflows文件夹内的工作流程文件以构建和部署其他微应用。我们将这些文件命名为.github/workflows/catalog-build-deploy.yml.github/workflows/checkout-build-deploy.yml

在相应的文件中,将所有出现home一词的地方更改为catalogcheckout。例如,在你的catalog-build-deploy.yml文件中,你将看到以下内容:

name: catalog-build-deploy
on:
  workflow_dispatch:
  push:
    branches:
      - main
    paths:
      - apps/catalog/**

策略下的matrix部分将如下所示:

      matrix:
        include:
          - dockerfile: "./apps/catalog/Dockerfile"
            image: areai51/ebuy-ssr-catalog
            k8sManifestPath: "./apps/catalog/k8s/"

类似地,checkout-build-deploy.yml文件将进行以下更改:

name: checkout-build-deploy
on:
   workflow_dispatch:
  push:
    branches:
      - main
    paths:
      - apps/checkout/**

此外,strategies下的matrix部分将如下所示:

      matrix:
        include:
          - dockerfile: "./apps/checkout/Dockerfile"
            image: areai51/ebuy-ssr-checkout
            k8sManifestPath: "./apps/checkout/k8s/"

然后,进行微小修改,提交 checkout 和 catalog 应用的文件,并验证是否只触发了相关的管道。

我们还可以通过在终端中运行以下kubectl get pods命令来验证在ebuy-ssr Kubernetes 集群内微应用 Pod 是否已成功创建。

如果任何 Pod 没有显示就绪状态或重启次数过高,您可以使用终端中的kubectl logs <pod-name>命令查看 Pod 日志。

通过这种方式,我们已成功使用 GitHub Actions 创建了我们的 DevOps 管道,我们学习了如何安全地将凭据保存为 GitHub 操作密钥,为每个微应用创建了单独的工作流程.yml文件,并配置了它们仅在相应的微应用发生变化时触发。

当这些微应用单独运行时,它们将无法与模块联邦一起工作,因为 Kubernetes 上的远程与我们本地运行的不同。在下一节中,我们将更新远程以确保它也能在云上工作。

更新远程

一旦您的管道成功部署,请登录到portal.azure.com,转到 Kubernetes 服务,选择您的 Kubernetes 集群,转到服务和入口链接,并记录微应用服务的公网 IP 地址。

您可以通过在终端中运行kubectl get services命令来实现相同的功能。

一旦我们有了 IP 地址,我们需要更新我们的模块联邦远程以包含更新的 URL。

现在,如您所想,我们的微应用的 URL 在本地和 Kubernetes 上是不同的。由于我们希望能够在本地以及 Kubernetes 上运行我们的应用,我们需要根据应用是在devproduction模式下运行来有条件地加载远程。我们这样做如下:

apps/home/next.config.js文件中的remotes对象内,我们按照以下方式更新代码:

const remotes = (isServer) => {
  const location = isServer ? "ssr" : "chunks";
  const ENV = process.env.ENV;
  const CATALOG_URL_LOCAL = 'http://localhost:3001';
const CHECKOUT_URL_LOCAL = 'http://localhost:3002’;
  const CATALOG_URL_PROD = 'http://<your-k8s-ip-address>’
  const CHECKOUT_URL_PROD = 'http://<your-k8s-ip-address>’
  const CATALOG_REMOTE_HOST = ENV === 'PROD' ? CATALOG_URL_PROD : CATALOG_URL_LOCAL;
  const CHECKOUT_REMOTE_HOST = ENV === 'PROD' ? CHECKOUT_URL_PROD : CHECKOUT_URL_LOCAL;
  return {
    catalog: `catalog@${CATALOG_REMOTE_HOST}/_next/static/${location}/remoteEntry.js`,
    checkout: `checkout@${CHECKOUT_REMOTE_HOST}/_next/static/${location}/remoteEntry.js`,
  };
};

我们在这里定义了一个名为ENV的新变量,用于捕获应用是在开发模式还是生产模式下运行,然后我们为微应用创建了LOCAL URLPROD URLS常量,并根据ENV值有条件地设置CATALOG_REMOTE_HOSTCHECKOUT_REMOTE_HOST的值。

将相同的更改应用到检查和目录应用的next.config.js文件中,然后保存更改。

现在,我们可以本地构建应用以验证一切是否正常。

从项目的根目录运行pnpm dev命令。

一旦本地运行成功,让我们将更改提交到 Git,并让 GitHub Actions 自动触发并部署新应用到我们的 Kubernetes 集群。

一切完成后,前往主微应用的 URL(http://<your-k8s-ip-address>/)并验证应用是否正常工作。

重要提示

确保在主应用管道开始之前先部署目录和检查应用。这是因为,在生产模式下,主应用现在期望remoteEntry.js文件存在于我们在CATALOG_URL_PRODCHECKOUT_URL_PROD常量中定义的 URL 上。

摘要

就这样,我们来到了这一章的结尾。希望您能够跟上进度,并享受作为一名 DevOps 工程师帽子的喜悦与痛苦。

如您所见,我们在这一章中涵盖了大量的内容。我们学习了 Kubernetes 及其各种关键组件。我们看到了如何在 Azure 上启动一个空的 Kubernetes 集群,并了解了部署我们的微应用到 Kubernetes 集群中的 Kubernetes 规范文件。我们学习了如何使用 Docker 容器化我们的微应用,以及如何设置 Docker Hub 作为远程镜像仓库。然后,我们详细介绍了使用 GitHub Actions 设置 CI/CD 管道的步骤,最后,我们对代码库进行了必要的调整,以便我们可以在 Kubernetes 上运行我们的模块联邦微前端。现在,您已经成功完成了这一章,给自己鼓掌吧,在开始下一章之前,好好休息一下,下一章我们将探讨如何在生产中管理我们的微前端。

第四部分:管理微前端

这一部分主要关注在生产环境中管理微前端,包括版本控制、功能开关、回滚和分支策略。它提供了操作最佳实践的指导。

这一部分包含以下章节:

  • 第九章在生产环境中管理微前端

  • 第十章构建微前端时需要避免的常见陷阱

第九章:在生产环境中管理微前端

能够在你的本地计算机上开发和测试 Web 应用程序是很好的;然而,当你的应用程序被成百上千的访客访问时,将它们部署到生产环境、维护它们并发布新功能,这需要你的软件开发技能提升到下一个层次。本章将涵盖围绕在生产环境中部署和维护你的微前端的一些关键概念。

在本章中,我们将涵盖以下主题:

  • 分支策略

  • 版本控制

  • 回滚策略

  • 功能开关

到本章结束时,你将迈出可靠维护你的微前端应用在生产环境中的第一步。

强大软件交付模型的基础组件

当涉及到在生产环境中部署和维护应用程序时,我建议使用 DevOps 研究和评估的DORA)软件交付成熟度模型来帮助确定重点关注的领域和你的生产部署流程中应该优化的方面。

重要提示

软件交付成熟度模型讨论了四个关键领域——即 部署频率变更的领先时间恢复服务的时间变更失败率,这些被分类为 精英。你可以在 DevOps 2021 状态报告 中了解更多详细信息:dora.dev/publications/pdf/state-of-devops-2021.pdf。你还可以在 dora.dev/publications/cloud.google.com/devops/state-of-devops 注册并查看其他报告。

我们将探讨几个关键组件,这些组件有助于你创建正确的基石,确保随着你的团队在部署和管理微前端方面获得更多信心,你能够提升成熟度模型。

分支策略

我认为,分支策略是帮助你提高 部署频率变更的领先时间 这两个关键指标的最重要组成部分。

GitFlow 和 GitHub Flow 是两种基于 Git 的版本控制系统流行的分支策略,每种策略都有其优势和劣势。

GitFlow 是一种分支模型,使用两个长期存在的分支,maindevelop,以及功能、发布和热修复分支。

另一方面,GitHub Flow 是一种更简单、更灵活的分支策略,适用于较小的团队和项目。它围绕一个单一的 main 分支,通常是 mastermain,并鼓励开发者在功能分支中进行更改,然后通过拉取请求合并到 main 分支。

在我们看来,当与微前端和单仓库一起工作时,GitHub Flow 是唯一可行的分支策略。这主要是因为以下原因:

  • 简单性:GitHub Flow 通过在main分支上强制执行单一、线性的历史记录来简化开发过程。每个功能、错误修复或改进都是在从main创建的单独分支上开发的,一旦准备就绪,就将其合并回main。除了main之外,没有长期存在的分支,避免了管理、同步和维护多个长期分支的复杂性。

  • 更改隔离:在单仓库中,确保对某个项目的更改不会无意中影响另一个项目至关重要。GitHub Flow 为每个新功能或错误修复使用隔离分支的做法有助于限制更改的范围,减少跨项目干扰的风险。

  • 持续集成/持续部署CI/CD):GitHub Flow 的设计考虑了持续部署(CD)。在单仓库(monorepos)中,这可以带来更大的益处。由于所有项目都生活在同一个仓库中,因此更容易确保所有更改都经过测试并一致部署。

  • 减少合并冲突:在 Git Flow 这样的策略中,更改通常在合并到main之前先合并到developrelease分支,这可能导致代码编写和部署之间的显著延迟。在快速发展的单仓库中,这可能导致复杂的合并冲突。GitHub Flow 通过鼓励频繁直接合并到main来减轻这一点。

当使用 GitFlow 工作时,我们也看到,根据你是在积极开发还是发布第一次生产部署后,稍微改变代码合并到main的方式是有益的。

在积极开发期间

在项目的积极开发阶段,团队成员勤奋地生成功能分支,只有在拉取请求(pull requests)获得必要的批准后,才将它们合并回main分支。在合并到main分支之前,对每个拉取请求启动一系列自动单元测试是常见做法,这可能是 CI 构建的一部分。此外,每晚在main分支上执行一系列集成和端到端测试也是首选的做法。这种常规做法有助于确保main分支上的任何中断都能被及时识别和纠正。

让我们看看 GitHub Flow 在积极开发期间的分支和合并工作流程:

图 9.1 – 积极开发期间的分支和合并策略

图 9.1 – 积极开发期间的分支和合并策略

如您所见,使用 GitHub Flow,分支和合并相当直接。开发者从main分支分叉,然后合并回main

在第一次发布到生产后

在您首次生产版本发布之后,合并策略经历了一种微妙的变化。团队继续从main分支创建功能分支,就像以前一样;然而,在功能测试和批准之后,过程发生了分歧。经过测试的功能版本与main分支进行 rebase,打上标签并直接从功能分支部署到生产环境中。只有当生产环境中的稳定性得到确认后,功能分支才会合并到main分支。

以下工作流程将有助于说明这个过程:

图 9.2 – 首次发布后的分支和发布

图 9.2 – 首次发布后的分支和发布

正如您在图 9.2中可以看到的,部署功能、错误修复或热补丁的过程是相同的,保持了简单性。这里的一个关键步骤是在您部署功能或错误之前重新 basemain分支。

虽然这可能听起来不寻常,但坚持这种方法有几个好处,如下所示:

  • main分支始终反映生产环境中的稳定、当前版本。

  • 团队有机会迅速解决和解决在功能部署期间遇到的任何小问题,在将其合并到main分支之前稳定发布。

  • 这消除了在发布稳定之前禁止向main分支提交的必要性,这在执行多个每日部署时是一种不切实际的战略。

  • 由于main分支始终与当前生产版本保持一致,因此部署功能或热补丁的过程保持一致。

  • 由于所有合并到main分支的操作都是在发布之后,因此破坏main分支的可能性显著降低。这种中断可能会阻碍大型开发团队,并停止进一步的部署,直到问题得到解决。

在 GitHub Flow 的背景下,重要的是要注意以下几点:

  • 在部署您的功能分支之前,请确保从main分支进行最终的 rebase。这确保了您的功能分支包含了在您的功能开发期间执行的所有先前部署。

与公众的看法相反,GitHub Flow 开发并不仅限于小型 2-3 人团队。实际上,对于在小型、专注的小队或 pod 中运作的大型团队来说,它具有明显的优势。

微应用的版本控制

将应用程序部署到生产环境的版本控制是标准做法,并且有不同方式来定义版本控制策略。版本控制从许多方面都很重要;它有助于管理变更日志并将不同的功能和错误映射到构建中。它还有助于回滚策略。

MAJOR.MINOR.PATCH

这种结构不仅帮助用户理解新发布中变化的本性,还有助于软件系统中的依赖关系管理。

我们推荐的战略是使用 SemVer 作为指南。每个微应用都应该遵守自己的版本规则,确保任何对应用程序公共特性的更改都反映在其版本号中。微应用可以在每次有重大功能发布、小功能发布或错误修复更改时,按照 MAJOR.MINOR.PATCH 模式增加其版本号。我们建议在语义版本前加上应用程序名称——例如,catalog-2.8.3 将意味着以下内容:

  • 微应用名称catalog

  • 主版本号:2

  • 次版本号:8

  • 路径/错误修复版本号:3

对于管理标签的简单方法,每次你在处理一个发布版本时,我们建议创建一个发布分支,例如 releases/catalog-2.0.1

一旦发布经过测试并准备部署,我们可以这样标记它:

  • git tag catalog-2.0.0

  • git push origin catalog-2****.0.0

使用 CI/CD 管道,我们可以设置它在新标签检测到时自动触发测试环境的部署。

注意,虽然 SemVer 的官方定义使用术语 重大变更 来定义主版本号,但在微前端和模块联邦中,我们实际上无法有重大变更。因此,对于我们来说,包含重大功能发布的微应用将需要增加主版本号。

在多个微应用和频繁发布的常见场景中,团队中的每个人都知道哪个微应用的哪个版本目前在生产中可能会变得具有挑战性。一个简单的解决方案是在每个微应用中有一个 /versions 路由,它会显示当前版本号、发布日期、发布分支等信息。这对于试图在生产环境中调试问题的开发者来说非常有帮助。

这里是一个 /versions 路由上的信息示例:

{
appName: Catalog
branchName: release/catalog-2.0.0
tagName: catalog-2.0.4
deployedDate: Thu 25 May 2023 13:34:04 GMT
}

对于大量微应用或频繁发布,手动标记每个版本可能会变得繁琐。你可能想要考虑使用脚本来自动化这个过程。以下是一个 bash 脚本的示例:

# Microfrontend names
MICROFRONTENDS=(home catalog checkout)
for i in ${MICROFRONTENDS[@]}
do
  cd apps/$i
  # Fetch latest tags
  git fetch --tags
  # Get latest version from Git
  VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)
  # Increment version
  npm version patch
  # Add release notes
  git commit -am "Release v$VERSION [skip ci]"
  # Tag commit
  git tag v$VERSION
  # Push changes
  git push --follow-tags origin main
  # Build microfrontend
  pnpm run build
done

在前面的代码中,你会注意到脚本会遍历 apps 文件夹中的每个微应用,使用 git describegit rev-list 命令从 git 中获取最新的标签,运行 npm version 命令来更新版本号,然后将更新的标签提交并推送到 git

我们还可以使用 semantic-releasestandard-version 等工具。这些工具根据提交信息自动化版本管理和变更日志的生成。

版本控制和标记微应用对于确保所有利益相关者对当前生产中每个微应用的版本状态有清晰的认识至关重要。正如我们将在下一节中看到的,它在回滚策略中也发挥着关键作用。

回滚微应用

回滚策略是管理任何生产软件的关键组成部分。这影响了恢复服务时间指标。

微前端的回滚策略集中在当部署期间或部署后出现问题时,能够将特定的微应用或整个系统回滚到之前稳定状态的能力。得益于微前端的独立性,回滚不一定影响整个应用程序,但可以针对有问题的组件,从而减少整体 系统中断

最简单的回滚策略涉及利用版本控制系统,如 Git,以及 CI/CD 管道。在这种配置中,每个微前端都有特定的标记版本,这些版本被存储起来,如果需要可以重新部署。例如,如果当前版本的微前端是catalog-1.2.3,并且检测到问题,您可以通过触发 CI/CD 管道中的相应部署,快速回滚到之前的稳定版本catalog-1.2.2

此外,利用蓝绿部署策略可能也很有效。在这种方法中,维护两个环境——蓝色和绿色。当其中一个环境(蓝色)服务于实时流量时,另一个环境(绿色)处于空闲状态或正在为下一个发布做准备。如果绿色环境在部署后出现问题,您可以快速切换回蓝色环境,有效地回滚更改。

由于 Kubernetes 声明式特性和内置的版本控制机制,Kubernetes 中的回滚非常简单。当创建新的部署时,Kubernetes 会自动为其版本化并存储其详细信息。如果新版本出现问题时,您可以使用kubectl rollout undo命令快速回滚到之前的版本。例如,如果您发现名为deployment/catalog的部署存在问题,您可以使用kubectl rollout undo deployment/catalog命令进行回滚。Kubernetes 将优雅地回滚部署到之前的稳定版本,而无需停机,使其成为管理微前端架构中回滚的强大工具。

在回滚微应用时,重要的是要意识到与后端 API 的任何不兼容性,以及相应的后端 API 是否也需要回滚。

有时回滚可能会很痛苦,而通过发布新功能或微应用的版本,并使用功能开关来减轻回滚的需求,这将在下一节中介绍。

带有功能开关的微应用部署

功能开关,也称为功能标志,是一种强大的技术,允许在运行时打开或关闭单个功能,而无需重新部署。这在微前端架构中尤其有用,因为它使得可以在多个微应用之间独立发布和控制微应用。

使用功能开关,团队可以将新功能部署到生产环境中,但它们在准备好发布之前“隐藏”在开关后面。这允许在实时环境中进行广泛的测试,并启用渐进式交付技术,如金丝雀发布或 A/B 测试。如果新功能出现任何问题,可以通过功能开关快速“关闭”,有效地减轻影响,而无需进行全面回滚或重新部署。

Unleash (www.getunleash.io/) 是一个流行的开源功能开关工具。

功能开关可用于为不同用户提供不同的体验。例如,您可以使用它们有选择地启用特定用户组的特征,如测试人员或付费用户。

然而,功能开关需要谨慎管理,以避免过时开关的积累,这可能导致代码复杂性和技术债务。定期审计和清理功能开关应成为开发过程的一部分。

通过这一点,我们来到了本节的结尾,它涵盖了管理生产中微前端的一些基础元素。这与其他章节中我们看到的内容相辅相成,关于将微前端部署到云中,并最终帮助减少在生产中部署和维护应用程序的整体压力。

摘要

在我们结束本章之前,让我们快速总结一下到目前为止我们所学的知识。我们学习了 DORA 的软件交付性能指标:部署频率变更的领先时间恢复服务的时间变更失败率。然后我们查看了一些团队需要关注的基础元素,以确保他们为成功做好准备。

我们学习了分支策略,并了解到 GitHub Flow 是首选的分支策略。我们还了解了在软件构建时与部署时的流程细微差别。

我们学习了正确版本化我们的微应用的方法。我们还学习了回滚策略的重要性以及微前端如何帮助最小化爆炸半径。最后,我们学习了功能开关以及我们如何可以通过功能开关逐步将新微应用发布到生产环境中,更重要的是,如果存在问题。

在下一章中,我们将探讨在构建微前端时需要避免的一些常见陷阱。

第十章:构建微前端时需要避免的常见陷阱

我们已经走了很长的路!我们已经学会了如何构建微前端,如何将它们部署到原生云,以及如何在生产中管理它们。

当我们开始使用微前端时,我们将会犯错误,但我们会从错误中学习,并最终建立我们自己的最佳实践集,发现对我们用例最有效的方法。然而,从他人的错误中学习始终是一个明智的选择。在本章中,我们将介绍早期团队在处理微前端时遇到的一些陷阱。

我们将教你一些常见的陷阱以及如何避免它们,如下所述:

  • 不要让你的微应用太小

  • 避免过度使用常见的共享代码/库

  • 避免在微前端中使用多个框架

  • 无法部署单个微应用

  • 过度依赖状态

  • 避免在构建时编译来组装微前端

  • 避免将微应用打包到 NPM 包中

到本章结束时,你将了解到开发者从单页应用过渡到微前端时可能会陷入的各种陷阱。

不要让你的微应用太小

我们在本书的开头提到了这一点,但重要的是再次强调。太多的开发者认为,在微前端架构中,微应用需要非常小。这并不正确,因为创建非常小的微应用会大大增加复杂性和维护难题,而不会带来任何好处。

在尝试确定你的微应用合适的尺寸时,我们发现考虑以下因素是有帮助的:

  1. 这是否是可以独立存在的可能最大的微应用?

  2. 这是否是单个敏捷 Scrum 团队拥有的可能最大的微应用?

  3. 这个应用是否经历了与其他应用不同步的速度的变化和更新?

  4. 另一个需要考虑的点是基于领域驱动设计原则,从领域角度思考,以确定一个特定的微应用应该支持或不应支持哪些业务功能。

如果你对所有前面的问题的回答都是肯定的,那么微应用的尺寸是合适的。如果对任何一个问题的回答是否定的,那么要么是我们没有正确地分解微应用,要么是微前端可能不是正确的架构选择。

另一个帮助确定应用合适尺寸的指南是查看原子设计模式(bradfrost.com/blog/post/atomic-web-design/),它定义了组件在应用中的结构。

图 10.1 – 生命体和模板可以被转换为微应用

图 10.1 – 生命体和模板可以被转换为微应用

如果您查看图 10.1中的原子设计模式,将您的应用程序分解成微应用的理想级别将是生物体级别或模板级别;其他任何级别都可能是太小或太大。

将应用程序分解成适当大小的微应用是构建高性能和可扩展的微前端架构的关键,而且投入更多时间来确保这一点,随着我们向前发展,将会带来巨大的回报。

避免过度使用共享组件代码

当涉及到构建微服务或微前端时,团队独立性是最高的优先级。任何使一个团队依赖另一个团队的事情都应该被强烈反对。

在我们作为软件开发者的经验中,我们始终会遇到诸如可重用性不要重复自己DRY)等原则。事实上,大多数高级开发者都在不断寻找如何创建通用工具、辅助共享组件等,以帮助团队提高生产力。

然而,当涉及到微服务和微前端的世界时,过度使用这些共享库可能导致所谓的“依赖地狱”或“分布式单体”,这是两种情况的糟糕结合。

这对微前端来说很不利,因为使用共享库或代码立即剥夺了团队的独立性,因为现在两个或更多团队需要依赖对这个共享库的更新或错误修复,才能继续前进。

随着越来越多的团队开始使用共享库,它往往会变得越来越大,因为它现在需要适应不同团队的使用案例。此外,还存在一个持续的风险,即对这个共享代码的更改或更新可能会破坏一个或多个团队的功能。

因此,当涉及到微前端时,我们需要严格避免陷入这个陷阱。作为一个经验法则,我们应该避免创建任何业务或应用程序逻辑作为共享通用代码。一个理想上可以在微应用之间共享的项目是 UI 组件库,因为我们希望确保所有微应用都有一致的外观和感觉。另一个可以放入共享库的项目是任何不包含业务逻辑的低级实用函数。这些示例包括 HTTP 客户端、错误处理实用工具或其他用于格式化日期或操作字符串的实用工具。

记住,在单仓库中,与处理分布式单体的挑战相比,“查找和替换”要容易得多。

虽然最初将团队独立性优先于代码重用的整个想法可能听起来像是一种反模式,并不是一个明智的做法,但从经验来看,这是您希望团队快速行动并频繁将代码部署到生产环境时需要牢记的第二个重要点。

避免在微前端中使用多个框架

微前端的一个好处是,从技术上讲,每个应用都可以使用不同的框架构建。然而,尽管这是可能的,但这并不意味着你必须这样做。在单个微前端中使用多个框架存在许多缺点:

  • 随着团队成员可能随着时间的推移从一个团队切换到另一个团队,认知负荷非常高。

  • 由于每个框架都附带自己的 JavaScript 包,并且每个框架都将使用不同的 NPM 模块集,因此传输到用户设备上的 JavaScript 代码量将很高。因此,我们无法充分利用浏览器缓存或服务工作者缓存,因为每个应用都使用自己的包。

  • 不同的框架将面临不同的性能挑战和问题,每个团队都必须单独处理这些问题,而无法利用更广泛团队中的集体知识。

话虽如此,在评估新框架或逐步升级到新版本时,拥有多个框架或它们的多个版本作为短暂的过渡阶段是可以接受的。总的来说,将多个框架作为架构原则应该避免。

无法部署单个微应用

采用微前端架构的一个主要原因是允许应用程序的某些部分独立更新,而不会影响其余部分。

这显然意味着我们需要有能力独立构建和部署每个微应用。如果你的 DevOps 构建和发布管道无法做到这一点,那么选择单页应用SPA)架构会更好。

在过去,许多 DevOps 工具还不够成熟,无法与 monorepos 或 microfrontends 协同工作;然而,大多数最新的工具都配备了更好的功能,能够检测哪些文件夹已更改,并且只触发必要的应用构建。

因此,当你在微前端架构上工作时,至关重要的是你必须彻底思考,包括它的部署方式,因为这会影响你为 DevOps 管道或 monorepo 选择的工具。

例如,如果你的 DevOps 管道可以根据哪个微应用已更改进行条件触发,那么你可以自由选择任何 monorepo 工具。

然而,如果你的 DevOps 管道无法检测到更改,或者你被限制为为所有微前端使用单个管道,那么选择具有内置更改检测功能的单仓库工具,如 Nx,将更为合适。

过度依赖状态

随着 React 的出现,状态管理成为了一件事,随之而来的是像 Redux 这样的工具的流行,这些工具提倡使用单一中央数据存储来管理状态。随着时间的推移,开发者似乎对状态管理变得着迷,过度依赖这些状态管理库。当开发者从 SPA 转向微前端时,他们继续对状态着迷,花费大量时间尝试持久化状态,使其在不同的微应用之间工作。在 SPA 和微前端中,重要的是要谨慎使用这些应用级状态。在处理微前端时,我们鼓励探索 Pub/Sub 或事件发射器方法等概念,以在不同的微应用之间共享数据。或者,可以考虑使用原生的浏览器数据存储,如会话存储、IndexedDB 或本地存储来管理持久状态,或者如果这些都不适用,那么可以探索轻量级的状态管理库,如 Zustand 或 React 的 Context API。

如您现在可能已经意识到的,在构建微前端时,涉及相当多的重新学习和学习,尤其是如果您已经构建了很长时间的 SPA。在微前端中使用状态管理是需要理解和掌握的,也是一些开发者偶尔必须应对的最困难的变化,尤其是那些习惯了过度依赖状态的开发者。

避免在构建时间编译以组装微前端

前端社区中目前有一个趋势,即将尽可能多的任务移动到应用程序编译的构建时间阶段,而不是运行时。这些例子包括静态站点生成,其中 HTML 页面是在构建时生成的,或者在 Angular 中的提前编译AoT),这可以提高应用程序的整体性能。

虽然,总的来说,构建时间编译是一种良好的实践,可以在运行时阶段减少浏览器和 JavaScript 引擎的负载,但在组装微前端时并没有帮助。这是因为每次任何微前端发生变化时,都需要重新构建组装层,这违背了独立微应用部署的原则。

我们可以选择让单个微应用在构建时间做更多的工作(例如,生成静态页面),但微应用或模块联邦的组装始终应该在服务器或运行时进行。

这是需要记住的另一个关键点,以确保我们不会盲目跟随“流行趋势”。始终记住您架构模式的关键原则,并且您已经全面思考了您的模式,从端到端,一直到它如何部署到生产环境。

避免将微应用打包进 NPM 包

在单页应用(SPA)的世界中,另一个常见的趋势是将任何可共享的模块转换为 NPM 包,以便更容易地分发,然后将其导入到其他应用中。

根据我们的经验,我们见过一些团队在将微应用程序打包和版本化为 NPM 模块后,再将其导入到宿主或组装应用程序中。我们强烈反对这种做法,主要原因在于每次发布一个新的微应用程序版本作为 npm 模块时,所有使用该微应用程序的宿主都需要更新他们的package.json文件,并重新构建和重新部署他们的应用程序,这违背了独立部署的基本原则。我们已在第二章“微前端的关键原则和组件”中的“偏好运行时集成”部分对此进行了详细说明。

摘要

有了这些,我们就来到了本章的结尾。作为一个相对较新的架构模式,微前端的概念和最佳实践正在不断演变。

在本章中,我们看到了一些团队在构建微前端时常见的陷阱——例如,无法确定将应用程序拆分为微应用程序的正确层级,过度使用状态管理库,在微应用程序中使用多个框架,无法单独部署微应用程序,过度使用共享通用代码,以及最终导致构建时集成。希望本章能帮助你避免重蹈覆辙,你的同行在过去犯过的错误。

另一个需要记住的重要点是理解这些最佳实践背后的原因,通过你具体用例的视角来看待它们。遵循适用于你用例的最佳实践,并调整那些不太适合的实践。

正如那句著名的话所说,“对于每一个架构问题,答案都是… 视具体情况而定

在下一章中,我们将探讨微前端世界中的一些新兴趋势,这些趋势是你应该关注的。

第五部分:新兴趋势

本部分探讨了微前端领域,并分析了适用于微前端的前沿技术和方法,例如生成式 AI、边缘函数和岛屿架构模式。

本部分包含以下章节:

  • 第十一章微前端最新趋势

第十一章:微前端的最新趋势

前端工程的世界不断演变,当我们根据目前可用的工具、方法和最佳实践构建微前端时,关注这个领域正在演变的最新的趋势,并不断探索和实验它们,以了解它们如何帮助我们提高效率并构建更好的应用程序,这一点非常重要。

在本章中,我们将探讨一些可能影响我们未来构建微前端趋势。我们将探讨的一些趋势如下:

  • 术语微前端本身以及更好的术语是什么

  • 混合静态内容和动态内容的孤岛模式

  • 查看 Webpack 之外的构建工具

  • WebAssembly

  • 云或边缘函数

  • 生成式 AI 如何影响我们的工作

到本章结束时,我们将了解影响我们构建微前端的前端工程领域的最新趋势。

微前端 – 解耦模块化前端

术语微前端显然已经变得非常流行,整本书都在使用它,但说实话,我一直觉得这个术语用得不好,而且不幸的是,它已经在社区中根深蒂固。正如提到过几次,微前端这个词导致了很多误解,导致了不良的架构模式,反而比好的影响更大。已经提出了一个新的建议,开始称它们为可组合解耦前端(microfrontend.dev/),我认为这是恰当的,并且清楚地解释了我们正在构建的内容的意图和目的。我真心希望社区开始采用这个术语,并且我们共同开始转向构建和称呼微前端为它们真正是的样子,并定义它们真正应该做什么。

我相信你们中的许多人会想知道仅仅改变名称如何有帮助,以及名称中真正包含的是什么;然而,我觉得在这种情况下,一个清楚地阐述架构模式的名称可以大大减少由错误架构的系统引起的误解、误释和复杂性。正如你们通过本书的学习过程所意识到的那样,这全部关于构建模块化应用程序,这些应用程序相互解耦,因此它们应该正确地被称为解耦 模块化前端

孤岛模式

静态生成的页面越来越受欢迎,因为它们几乎不包含 JavaScript;然而,它们的挑战始终在于如何提供动态内容。

孤岛模式旨在解决这个问题。它由 Astro 构建框架推广,其中我们的应用程序被发布为一系列静态生成的 HTML 页面,页面的动态部分作为孤岛导入。

这里有一个使用 Astro 的例子,Astro 是一个用于构建静态网站的流行框架,展示如何实现这一点。

你可以在 docs.astro.build/en/concepts/islands/ 上了解更多关于此的信息。

//index file
---
// Example: Use a dynamic React component on the page.
import MyReactComponent from '../components/MyReactComponent.jsx';
---
<!-- This component is now interactive on the page!
     The rest of your website remains static and zero JS. -->
<MyReactComponent client:load />

运行 Astro 构建命令,在本地测试应用,并查看你的 Inspect 命令;你会注意到,尽管页面的其余部分是带有少量或没有 JavaScript 的纯 HTML,但 MyReactComponent 是一个小的 JavaScript 元素,并在客户端执行。

如你所见,使用岛屿模式,我们可以在静态和动态内容之间获得清晰的区分,并且可能带来不将所有应用部分锁定到单个框架的额外好处。

话虽如此,岛屿模式和微前端之间有一些差异,包括以下内容:

  • Astro 中的岛屿是客户端的水合/渲染组件,而微前端是具有自己的代码库、路由和后端的独立应用。微前端更加隔离和松耦合。

  • Astro 在构建时构建整个应用和岛屿。微前端是独立构建和部署的。Astro 有统一的构建,而微前端可以有独立的构建。

  • Astro 中的路由发生在外壳中,而每个微前端管理自己的路由。Astro 岛屿没有独立的路由。

  • Astro 岛屿可以通过 Astro 集成相互通信,而微前端通常通过定义良好的 API 和事件进行通信。岛屿与 Astro 应用有更紧密的耦合和集成。

超越 Webpack 与 ES 模块

随着基于 JavaScript 的框架的兴起,Webpack 的受欢迎程度上升,并成为所有 JavaScript 框架的事实上的模块打包器。然而,使用 Webpack 打包/编译大型应用可能会非常慢,手动配置以高效打包应用非常复杂。最近,一种利用 ES 模块的新一代打包工具在前端世界掀起了一场风暴,承诺编译速度比 Webpack 快 20 倍以上。

ES 模块是定义和导入 JavaScript 模块的标准方式。它们允许模块化代码组织,这可以使开发大型应用更容易,也更容易维护。ES 模块还提供了清晰和明确的语法来导入和导出代码,使得推理不同模块之间的依赖关系更容易。

我们可以将每个微应用导出为 ES 模块,并通过使用动态导入,将它们嵌入到我们的宿主应用中。

整个微前端应用可以使用基于 ES 构建的模块打包器,如 Vite (vitejs.dev/) 进行打包。Monorepo 框架如 Nx 允许你轻松配置使用 Vite 作为模块打包器。

我们可以使用 Vite 搭建一个 React 应用,如下所示:

pnpm create vite microfrontend-app --template react

这里有一个如何实现这一点的粗略示例:

// Catalog App
function CatalogApp() { 
  return <h1>Hello World</h1>;
}
export default CatalogApp;

在宿主应用中,我们使用经典的 React suspenselazy 函数在运行时加载 CatalogApp

// Host App
import React, { lazy, Suspense } from 'react';
const CatalogApp = lazy(() => import('./catalog'));
function App() {
  return (
    <>
      <Suspense fallback={<div>Loading...</div>}>
        <CatalogApp />
      </Suspense>
    </>
  );
}

正如你所注意到的,我们已经设法在不使用 Webpack 或 Webpack 的模块联邦的情况下使我们的应用工作,我相信你也会注意到你做出任何更改后应用构建的速度有多快。

我们相信 ES 模块和 ES 构建系统将很快取代 Webpack,成为构建所有现代前端的事实上首选工具。值得注意的是,虽然 React 的 lazysuspense 函数通常被认为是性能优化技术,但我们利用它们实时加载模块的能力来构建微前端。

使用 WebAssembly 模块

WebAssembly (Wasm) 已经存在很多年了。尽管它在性能和低包大小方面具有巨大的优势,但它并没有获得太多的普及,主要是因为开发者构建 WASM 模块并不容易。然而,现在随着人们开始使用 Rust 等工具,使用 Rust 构建 WebAssembly 模块变得相当容易。我们预计,当构建需要在浏览器上进行高度计算的应用时,WebAssembly 将成为主流。

WASM 模块在微前端架构中可以工作得非常好,其中关键的计算密集型模块是用 WASM 封装作为微应用构建的,并导入到微前端架构中,在该架构中,微前端中的其余微应用使用标准的 React 构建。

这里有一个大致的方法,你可以用它来设置你的模块联邦 Next.js 应用。使用我们来自 第六章 的模块联邦代码。首先,在 /rust 文件夹内使用 wasm_bindgen 构建一个 Rust 应用。

要将 Rust 应用编译为 wasm,我们需要安装 wasm-pack-plugin,使用 pnpm install @wasm-tool/wasm-pack-plugin 安装它,并在 next.config.js 配置中使用它,如下所示:

const NextFederationPlugin = require("@module-federation/nextjs-mf");
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const path= require("path")
const remotes = (isServer) => {
  const location = isServer ? "ssr" : "chunks";
  return {
    catalog: `catalog@http://localhost:3001/_next/static/${location}/remoteEntry.js`,
  };
};
module.exports = {
  webpack(config, options) {
    config.plugins.push(
      new WasmPackPlugin({
        crateDirectory: ('./rust'),
    }),
      new NextFederationPlugin({
        name: "catalog",
        filename: "static/chunks/remoteEntry.js",
        exposes: {
          "./Module": "./pages/index.tsx",
        },
        remotes: remotes(options.isServer),
        shared: {},
        extraOptions: {
          automaticAsyncBoundary: true,
        },

      })
    );
    config.experiments = {
      syncWebAssembly: true,
    };
    config.module.rules.push({
      test: /\.wasm$/,
      type: 'webassembly/sync',
    });
    return config;
  },
};

然后使用动态导入,将 wasm 模块导入远程应用的索引页面。最后,使用我们在 第六章 中使用的方法将远程应用导入主机应用。

WASM 已经被用于一些非常流行的基于网络的工具中,如 Figma、AutoCAD、Google Earth、Unity 游戏引擎等。将 WebAssembly 模块与微前端结合使用,有助于将两者的优点结合起来:WASM 的力量和性能,以及微前端的易用性和模块化。

Edge 函数或云函数

Edge 函数越来越受欢迎,因为它们提供了在边缘进行计算的能力。想象一下,它们就像一个 内容分发网络 (CDN),但具有运行计算的能力和力量。

Edge 函数的主要好处是它们提供了非常低的延迟,这极大地帮助提高了性能,并且它们使用自动分布式部署,这有助于减轻单点故障并提高可扩展性。

边缘函数和微前端配合得相当好,你可以将每个微应用部署在云函数中;这自动允许模块化部署,每个团队可以独立管理其云函数。

Cloudflare 是最受欢迎的云函数提供商之一。Cloudflare Workers 和最近推出的 Cloudflare Pages 支持边缘计算。以下是如何使用 Edge Runtime 在 Cloudflare Pages 上部署 Next.js 应用的示例。

  1. 从我们构建的任何现有 Next.js 应用开始。

    npm install --save-dev @cloudflare/next-on-pages
    
  2. 提交你的更改并将它们推送到 Git 仓库。

  3. 登录 Cloudflare 仪表板,转到Workers & Pages | 创建应用 | Pages | 连接到 Git

    1. 选择你推送代码的仓库,在设置构建和部署中,选择 Next.js 作为你的框架。其余的设置保持默认。

接下来,我们需要设置兼容性标志,这通过进入nodejs_compat来实现。

从部署详情部分进入管理部署,从下拉菜单中选择重试部署

由于成本低廉和部署简便,我们相信在 Vercel、Cloudflare、Fastly 等平台上部署所有前端应用,无论它们是否是微前端,都具有巨大的潜力。

大多数边缘函数提供商对 JavaScript 生态系统都有非常好的支持;然而,重要的是要记住,根据你正在工作的供应商/平台,可能会有某些限制。例如,Cloudflare 限制每个 worker 的大小不超过 1 MB,或者它明确支持与更广泛的 Node.js 运行时环境兼容的包版本。对于 Cloudflare,你可以在这里了解更多关于 Node.js 兼容性的信息:developers.cloudflare.com/pages/framework-guides/

生成式 AI 和微前端

生成式 AI 显然已经席卷了整个世界。我们看到许多令人惊叹的例子,生成式 AI 能够生成完整的端到端应用。

当谈到构建微前端时,看到事情如何发展将非常有趣。虽然我相信生成式 AI 不能取代开发者的工作,但我确实看到了生成式 AI 如何与微前端携手共建独特客户体验的有趣用例。

生成式 AI 可以被用来动态生成和组装 Web 应用的各个部分。通过智能分析用户行为、偏好和实时上下文,AI 可以创建为单个用户量身定制的微前端,从而实现高度个性化和优化的用户体验。这种方法还通过允许开发者专注于创建模块化、可组合的微应用,同时 AI 系统负责整个 Web 应用的组装和渲染,从而简化了开发过程。

新的 AI 工具,如 GPT-Engineer、smol-ai 和 Auto-GPT 正在出现,这些工具允许开发者使用纯文本或 Markdown 描述应用程序需求。然后,这些工具根据开发者的规格构建和生成整个应用程序的代码。这消除了手动编写所有代码的需要,相反,让 AI 处理大部分初始设置。这类 AI 开发者助手还处于相当初级的阶段;开发者需要学习如何制作有效的提示,以从 AI 中获得最一致和最准确的结果,但 AI 显著增强和加速开发工作流程的潜力是存在的。关键在于继续提高 AI 的代码生成能力,同时帮助开发者提供正确的输入和指导。

在微前端中使用人工智能可以导致更有效的资源利用和性能提升,因为系统可以根据用户交互和需求自适应地加载和卸载组件。这种人工智能与微前端的创新集成有可能彻底改变网络应用的设计、开发和交付给用户的方式。

摘要

有了这些,我们已经到达了本章和本书的结尾。我们真心希望您已经享受了这次旅程。

在本章中,我们探讨了几个将影响我们构建和部署微前端的新趋势。我们看到了如何使用岛屿模式帮助将动态内容块交织在静态生成的多页应用中。我们看到了基于新 Rust 打包器的工具可以比 Webpack 快许多倍。我们了解了 WebAssembly 及其在微前端中的应用,最后,我们探讨了云函数,它们有可能成为部署所有现代前端应用的默认解决方案。

我对技术发展之快以及它如何影响我们构建应用程序的方式感到非常兴奋。我迫不及待地想看到您走出家门,构建出让这个世界变得更美好的事物。

最后,我们必须记住,微前端的世界,就像我们动态的数字景观一样,始终处于不断演变的状态。我们在这次旅程中揭示的概念、技术和方法,例如模块联邦和将微前端部署到云端的引人入胜实践,只是这个不断演变的画卷的起点。它们为我们提供了构建高性能、可扩展和可维护的前端架构的基石。然而,未来充满了新的趋势和进步的承诺,这些将继续重新定义我们的视野。

我鼓励你们,新一代的开发者,踏进这个激动人心的旅程,并在此基础上构建这本书所尝试提供的知识基础。挑战现状,尝试最新的趋势,并将它们塑造成适合你们项目独特需求的形式。现在是成为一名前端工程师的伟大时刻,世界等待着你们使用 React 和微前端创造的革新性解决方案。记住,你写的每一行代码都是一个改进、创新和启发的机会。所以,勇往直前,为未来而构建。

posted @ 2025-09-07 09:18  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报