介绍
Airbnb 的前端最近达到了一个重要的里程碑:我们所有的 Web 界面都已从 React 16 升级到 React 18,这是 React 的当前主要版本。对于具有许多界面的产品来说,这是一个大项目,包括访客和主机页面以及许多内部工具。为了安全地执行此升级,我们创建了 React 升级系统:可重复使用的基础设施,使我们能够在 monorepo 中逐步推出新版本的 React,并衡量升级结果。在这篇博文中,我们将讨论我们的升级理念、我们创建的系统以及我们从执行此升级中学到的东西。 虽然这篇文章主要关注 React,但该过程适用于许多需要定期升级的 Web 框架和库。
升级的挑战
升级依赖项是任何长期项目中的常见任务。升级可修复错误、提高性能并解锁新 API。有些升级很简单,但当大量产品代码依赖于更改的 API 或对行为的微妙假设时,升级会变得更加困难。在 Airbnb 的 Web Monorepo 中,我们只允许每个顶级依赖项有一个版本(有一些罕见的例外),并且存储库的根目录中有一个 package.json。这可确保 Monorepo 中的代码在内部兼容且一致,并且我们避免向用户发送重复的包。在升级系统出现之前,每个依赖项只有一个_版本_意味着执行原子更新,这需要大量的前期迁移工作、长期运行的升级分支以及最终部署给用户时的单个里程碑。这种方法容易出错且有风险,因此需要“英雄”般的工程努力才能交付干净的升级。
理想情况下,我们会发布没有问题的小型增量升级。如果没有某种方式来测试并逐步将该系统推广到大型 monorepo,我们通常需要多次尝试升级,一旦发现任何问题就降级。使用这种升级策略很难发现性能回归。由于在发布之前无法收集性能数据,我们在部署时直接从 0% 到 100% 的推广。
我们的目标是让 React 升级系统更加无缝,不再那么繁琐,而是更加常规。具体来说,我们的目标是能够:
设计 React 升级系统
从这些目标开始,我们开始了解我们的理想架构是什么样的。我们希望避免长期运行升级分支,以便我们可以逐步升级,并且我们希望能够对升级进行 A/B 测试,以便我们能够从生产中获得反馈,为发货决策提供参考。
我们的理想升级系统简化图
该系统最简单的实现有几个问题需要解决:我们需要选择一个 React 版本进行渲染,并且在运行时动态切换两个版本非常困难。以下是使用这种简单方法渲染基本应用程序的代码:
import React18 from 'react';
import React16 from 'react';
if (shouldEnableReact18()) {
const root = React18.createRoot(container);
root.render(<App />);
} else {
React16.render(<App />, container);
}
这有两个问题:
- 我们不想在应用程序中捆绑两个版本的 React,否则我们的框架包大小将增加一倍。此外,我们可能需要更改构建时使用的 JSX 转换,从而导致我们的
<App />
版本与其中一个版本不兼容。 - 不清楚导入应该来自哪里。'react' 依赖项将指向 React 16 或 React 18,但不会同时指向两者。
为了解决这些问题,我们使用模块别名来分割版本,并使用环境定位来构建和运行两个分割版本的 React。
模块别名
我们利用模块别名解决了这些导入来自何处的问题。使用 yarn,我们在 package.json 中添加了另一个 React 依赖项,例如, “react-18” : “npm:react@18” 这使我们能够从“react-18”包导入 React。这让我们完成了部分工作。许多工具(例如自定义解析器和构建系统)需要知道使用哪个版本。为了集中逻辑,我们将所有自定义工具连接到一个中央“全局别名”配置中。此全局别名配置使我们能够在一个地方为所有不同的工具设置别名。Babel、Jest™、Webpack™ 和其他自定义解析逻辑都需要知道我们想要将导入从“react”重定向到“react-18”的条件。使用我们的“全局别名”配置对模块进行别名意味着用户代码根本不需要更改,并且我们能够在后台处理此重定向。
TypeScript 差异
鉴于任何组件都可以在 React 16 或 18 中运行,我们希望在升级期间使用适用于两个版本的每个组件的类型。值得庆幸的是,React 团队保持了向后兼容性,即使在主要版本之间也是如此。 我们安装了适用于 React 18 的类型,并且对于 React 18 中新添加的 API,我们为这些 API 创建了一个可在 React 16 和 18 中运行的填充层(例如,useTransition在 16 中充当无操作)。对于没有可能填充的 API(例如useId),我们通过类型增强指示此钩子在运行时可能未定义。 对于React 18 中仅限 TypeScript 的重大更改,我们等到 React 18 升级完成后才逐步修复这些更改。我们扩充了类型以修补差异,以便在 monorepo 中逐步修复这些新的 Typescript 错误。
环境定位
为了解决重复导入的问题,我们需要生成两个不同的构建工件:一个包含 React 16,另一个包含 React 18。我们分别将它们称为“控制”和“处理”工件。由于 Airbnb 使用服务器端渲染 (SSR),我们还需要在服务器上的不同节点进程中运行这两个不同的工件。使用 Kubernetes®,我们设置了两个不同的 Kubernetes 环境来运行这些控制和处理工件。我们将此设置环境称为定位。
模块别名**和环境定位**一起使用,在生产中一起部署不同版本的框架
我们还在构建时将环境变量 (REACT_UPGRADE) 写入我们的资产,并在节点 SSR 服务中在运行时设置此变量。这使我们能够执行可能仅在升级系统的一侧或另一侧发生的条件逻辑。
此设置也适用于我们的本地开发。我们的“本地”开发环境也已部署,因此我们能够使用此设置以与生产相同的方式为本地开发配置 React 版本。随着每个 SSR 服务升级到 React 18,我们还将该服务的开发环境切换为 React 18,以保持生产和本地开发版本同步。
测试升级
Airbnb 拥有一套全面的测试套件,这有助于在向用户公布升级之前建立对此次升级安全性的信心。我们的测试套件包括视觉回归测试、集成测试和单元测试。在向用户发布之前,我们修复了每个套件中的所有新故障。 单元测试是最难从框架内部抽象出来的。因为我们使用了Enzyme 和 React Testing Library 的组合,所以我们需要在单元测试、垫片和适配器中修复有关 API 和框架内部的假设。为了实现这一点,我们在 React 16 和 18 下运行了所有单元测试,允许 React 18 测试套件中现有的故障,并逐步修复它们。我们使用这个“允许的故障”列表来逐步减少测试失败的数量,从而防止倒退,因为列表上不允许出现新的故障。这种方法使我们能够逐步修复组件和测试环境中的问题。 我们通过仪表板跟踪解决数百个测试失败的工作,使用升级系统逐步合并修复,并将工作分派给少数开发人员。这使得迁移工作对更广泛的前端团队基本透明,并帮助我们在推出之前对升级充满信心。
逐步推广
一旦我们有了模块别名和环境定位,我们就有能力为 React 的两个不同版本编写和交付代码,所有这些都来自同一个代码库。为了确保安全性和可测试性,我们还需要一种逐步推出这个新环境的方法。为了减少同时发生的变化量,我们希望控制流量和产品界面的推出。我们的实验基础设施允许我们随意将流量引导到我们的两个生产环境(控制和处理)中的每一个。这种设置还允许我们首先在内部测试升级,并在发现问题时完全关闭升级。 控制向不同界面的部署更加困难。在单页应用中,管理多个 React 版本意味着卸载和安装 React 根。这会导致性能不佳并降低用户体验。 因此,我们在应用程序级别管理了表面推出升级。Airbnb 的 monorepo 包含许多单页应用程序,因此建立 React 升级系统非常有用,可以为每个应用程序打开和关闭升级。使用我们的 React 升级系统,我们能够首先在内部将其推广到单个应用程序,让开发人员可以在开发和我们的暂存站点上选择加入和退出升级以进行测试。这种方法让我们避免了长期运行的功能分支,帮助我们实现增量升级的目标。
功能采用和未来工作
使用该系统,我们将 React 18 完全推广到 Airbnb 的所有 Web 界面,无需回滚。升级后,我们能够开始测试新的 API,例如新的根 API和并发渲染功能。我们故意推迟了几周才采用这些功能,直到升级完成。这样我们就可以确信我们不需要降级并必须恢复代码更改。 看到采用这些新功能所带来的性能提升是令人兴奋的,并且我们正在继续尝试将它们扩展到能够受益的关键 UI 界面。 为了确保我们的升级目标经常实现,我们将使用 React 升级系统来测试React 的金丝雀通道。我们不必指向 React 18,只需指向金丝雀标签,即可预览 React 19现在需要进行哪些迁移工作。为了使升级不需要付出巨大的努力,保持最新状态应该是一项持续的努力,而不是一次性的大规模变革。
结论
React 升级系统的目标是使我们能够逐步升级、测试升级和**经常升级。**结合环境定位和我们的别名系统,我们可以逐步升级并测试升级。我们开始针对 React 19 beta 运行我们的前端,抢先体验 React 19。 我们要感谢 React 团队为实现 React 版本(甚至是主要版本)之间的向后兼容性所付出的努力。如果没有他们的努力,这种升级方法就不可能实现。 使用 React 升级系统,我们对 React 18 的推出充满信心,并将在未来的升级中使用此方法。我们认为投资升级系统是值得的,因为随着时间的推移,升级将继续存在。React 升级系统使我们能够逐步测试和推出升级,确保我们为用户提供最佳的用户体验和性能。