Airbnb的前端最近迎来了一个重要的里程碑:我们所有的前端界面已从React 16升级到了React的最新版本¹。对于一个包含多个界面的产品(包括客人和房东的页面以及许多内部工具),这是一个大型项目。为了安全地完成这个升级,我们创建了React升级系统工具:可重用的基础架构,使我们能够逐步在单代码库中推出React的新版本,并评估升级的效果。在本文中,我们将讨论我们的升级理念,创建的系统,以及从这次升级中学到的经验。
虽然这篇帖子主要关注于 React,但这些系统和教训适用于许多需要定期更新的 web 框架和库。
升级过程中遇到的难题升级依赖项是任何长期项目中的常见任务。升级可以修复错误、提升性能并打开新的 API 接口。一些升级很简单,但当大量产品代码依赖于变更的 API 或细微的行为假设时,升级就会变得很棘手。在 Airbnb 的 web 单一代码库中,我们只允许每个顶级依赖项的一个版本(有一些罕见的例外情况),并在代码库的根目录中只有一个 package.json。这确保了库内的代码在内部保持兼容和一致,并确保不会向用户推送重复的包。在升级系统之前,每个依赖项有一个 单一版本 意味着需要进行一次原子更新,这需要大量的前期迁移工作,一个长期运行的升级分支,以及一个最终交付给用户的单一里程碑。这种做法容易出错且风险较高,因此需要“英雄般的工程努力”才能发布干净的升级。
理想情况下,我们会发布小型的、无问题的增量升级。没有一种方法来测试并逐步将此系统部署到大型单一代码库中,我们常常需要多次尝试升级,一旦发现问题就需要回退。性能退化尤其难以用这种升级策略来捕获。因为无法在发布前收集性能数据,我们在部署时直接从0%升级到100%,没有逐步测试的过程。
理想与实际的对比图表,展示了 React 的主版本和次版本随时间的变化。
我们的目标是通过React升级系统使升级过程更平滑,更常规,不再像英雄般艰难。具体来说,我们的目标是能够:
(注:原文中的具体目标未列出,因此此处保持一致。如果后续有具体目标,可以在翻译中补充。)
- 逐步地升级以便我们尽快获得反馈并吸取教训。
- 频繁升级以便我们的版本与升级后的版本差异尽可能小。
- 测试升级以便我们可以精确测量升级对性能的影响,并根据这些数据做出明智的升级决策。
从这些目标倒着推回去,我们开始有了一个大概的想法。我们希望避免长期的升级过程,以便我们可以逐步地升级,并希望能够进行A/B测试,以便从生产环境中获取反馈来帮助做出发布决定。
这是我们理想中的升级系统的简化图
这个系统的最简单实现遇到一些问题:我们需要选择一个 React 版本来渲染,并且在运行时动态切换这两个版本很有挑战性。这里是使用这种简单方法渲染一个基本应用的代码示例:
import React18 from 'react';
import React16 from 'react'; // 重复导入 React16?
if (shouldEnableReact18()) {
const root = React18.createRoot(container);
root.render(<App />);
} else {
React16.render(<App />, container);
}
// 如果启用了 React18 版本,则使用 createRoot 方法创建根节点并渲染 <App /> 组件;否则,使用旧版 React16 渲染 <App /> 组件。
这里有两件事需要注意:
- 我们不想在应用程序中捆绑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 创建了一层适配层(shim 层),使得这些 API 在 React 16 和 18 中都能工作(例如,useTransition 在 16 中不执行任何操作)。对于无法添加适配层的 API(例如,useId),我们通过类型定义表明这个钩子在运行时可能未被定义。
对于仅影响 TypeScript 的 React 18 的破坏性变更,我们在完成 React 18 升级后才开始逐步解决这些问题。我们扩展了类型来解决这些差异,以便在我们的单仓库中逐步解决这些新的 TypeScript 错误。
环境定位为了解决重复导入的问题,我们需要生成两个不同的构建产物:一个包含 React 16,另一个包含 React 18。让我们分别称这两个产物为“标准”和“测试”产物。由于 Airbnb 使用了服务器端渲染 (SSR),我们还需要在服务器的不同节点进程中分别运行这两个不同的产物。我们使用 Kubernetes® 设置了两个不同的 Kubernetes 环境来运行这些标准和测试产物。让我们称这种设置为环境配置。
模块别名和环境目标一起使用,以便同时部署不同版本的框架代码到生产环境。
我们在构建时将环境变量(REACT_UPGRADE)写入我们的资产中,并在Node SSR(服务端渲染)服务运行时设置此变量。这使我们能够执行仅在升级系统一端必要的条件逻辑。
这个设置在本地开发中也很有效,对我们来说。我们的本地开发环境也被部署了,因此,我们能够像在生产环境中一样,使用此设置配置本地开发的 React 版本。随着每个 SSR 服务都升级到 React 18,我们也把该服务的开发环境切换到 React 18,以保持生产环境与本地开发环境的 React 版本同步。
测试升级功能Airbnb 有一套全面的测试套件,这有助于建立对升级安全性的信心。我们的测试套件包括视觉回归测试、集成测试和单元测试。在向用户推出前,我们修复了每个测试套件中出现的所有新失败。
单元测试是最难从框架内部抽象出来的。因为我们使用了Enzyme和React Testing Library的组合,这需要我们在单元测试、适配器和shims中修复关于API和框架内部的假设。为了实现这一目标,我们在React 16和18下运行所有的单元测试,并在逐步修复过程中,我们允许React 18测试套件中存在的失败。我们使用这个“允许失败”列表,随着时间推移逐步减少测试失败的数量,这样可以防止倒退,因为列表不允许新的失败出现。这种方法使我们能够逐步解决组件及其测试环境中的问题。
我们使用仪表板追踪了数百个测试失败的解决过程,逐步通过升级系统合并修复,并将任务分配给几位开发者处理。这使得迁移工作对更广泛的前端团队来说几乎是透明,并帮助我们在发布前增强了对升级的信心。
逐步推出注:原文中的 "rollout" 似乎是一个专有名词或术语,未作翻译。
一旦我们有了模块别名和环境目标功能,我们就能够从同一个代码库中编写并提供两个不同版本的 React 的代码。为了确保安全性和可测试性,我们还需要逐步推出新环境的方法。为了减少一次发生的更改量,我们希望逐步在不同的流量和产品界面中分阶段推出。我们的实验基础设施可以自由地将流量分配到两个生产环境,这使我们能够将流量分配到控制组和实验组。这种设置还允许我们先在内部测试升级,并在发现问题时完全停止升级。
控制不同界面的 rollout 更加困难。在一个单页面应用中,管理多个 React 版本意味着要卸载和重新挂载 React 根。这会导致性能变差并影响用户体验。
因此,我们在应用层面管理了这次表面版本升级。Airbnb的单代码仓库中包含了许多单页面应用,因此能够分别控制每个应用的升级开关非常有用。通过我们的React升级系统,我们首先在一个应用内内部部署了此升级,让开发人员可以选择在开发环境和预发布站点上加入或退出升级。这种方法使我们能够避免长时间存在的功能分支,帮助我们实现了渐进升级的目标。
功能采用情况及未来计划通过该系统,我们将 React 18 完全部署到了 Airbnb 的所有网页界面,无需回滚。升级后,我们能够开始测试新的 API,例如 新的根 API 和 并发渲染特性。我们故意在升级后等了几周才采用这些功能,以便我们可以确信无需回退代码更改。
看到这些新功能带来的性能提升真是太令人兴奋了,我们仍在继续试验将这些功能扩展到更多关键的UI区域,它们将带来更大的好处。
为了确保我们的升级目标经常达成,我们将使用 React 升级系统来测试 React 的 Canary 通道。我们直接指向 Canary 标签,以预览为 React 19 做迁移工作需要进行哪些准备工作。为了使升级不需要付出巨大努力,保持与最新版本同步应该是一个持续的过程,而不是一次性的大规模改动。
结论:下面是我们得出的结论我们的React升级系统的目标是使我们能够逐步升级,测试每个升级,频繁地进行升级。结合环境目标和我们的别名系统,我们已经能够逐步升级并测试每个升级。我们正在逐步升级并测试每个升级,以提前为React 19做准备。我们现在已经开始让前端与React 19 beta版一起运行,以进一步为React 19做准备。
我们非常感谢React团队在React不同版本(甚至是大版本)之间的兼容性方面所付出的努力。如果没有这些努力,这种方式就不可能实现了。
通过React升级系统,我们对React 18的推出充满信心,并且我们会用这种方法处理未来的升级。我们觉得投资升级系统是值得的,因为随着时间推移,升级需求将持续存在。React升级系统让我们能够逐步测试并推出升级,确保我们用户获得最佳体验和性能。
如果你对这种工作感兴趣,可以看看我们的职位——我们正在招人!
致谢特别感谢乔舒亚·内尔森带领团队创建React升级系统,还撰写了这篇博客文章。
特别感谢 Kim Nguyen、Callie Riggins Zetino、James Robinson、Dan Beam、Kaeson Ho、Rae Liu、Michael James、Noah Sugarman、Laurie Jin、Brie Bunge、Matt Mulder、Victor Lin 在此系统及其相关工作中提供的帮助。谢谢。
[1]: React 17 作为一个“过渡版本”于 2020 年发布,没有太多新功能,改动也很小。当时我们开始升级时,React 18 已经发布了,所以我们就直接升级到 React 18。写这篇文章时,React 19 正处于测试阶段,我们也在重用之前的升级系统来应对 React 19。
所有产品名称、标志和品牌归其各自所有者所有。本网站中提及的所有公司、产品和服务名称仅为标识目的。使用名称、标志和品牌并不表示推荐或认可。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章