亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

Airbnb如何輕松升級React

逐步升级我们的前端基础架构,以推出最新的React特性而不会倒退

开头

Airbnb的前端最近迎来了一个重要的里程碑:我们所有的前端界面已从React 16升级到了React的最新版本¹。对于一个包含多个界面的产品(包括客人和房东的页面以及许多内部工具),这是一个大型项目。为了安全地完成这个升级,我们创建了React升级系统工具:可重用的基础架构,使我们能够逐步在单代码库中推出React的新版本,并评估升级的效果。在本文中,我们将讨论我们的升级理念,创建的系统,以及从这次升级中学到的经验。

虽然这篇帖子主要关注于 React,但这些系统和教训适用于许多需要定期更新的 web 框架和库。

升级过程中遇到的难题

升级依赖项是任何长期项目中的常见任务。升级可以修复错误、提升性能并打开新的 API 接口。一些升级很简单,但当大量产品代码依赖于变更的 API 或细微的行为假设时,升级就会变得很棘手。在 Airbnb 的 web 单一代码库中,我们只允许每个顶级依赖项的一个版本(有一些罕见的例外情况),并在代码库的根目录中只有一个 package.json。这确保了库内的代码在内部保持兼容和一致,并确保不会向用户推送重复的包。在升级系统之前,每个依赖项有一个 单一版本 意味着需要进行一次原子更新,这需要大量的前期迁移工作,一个长期运行的升级分支,以及一个最终交付给用户的单一里程碑。这种做法容易出错且风险较高,因此需要“英雄般的工程努力”才能发布干净的升级。

理想情况下,我们会发布小型的、无问题的增量升级。没有一种方法来测试并逐步将此系统部署到大型单一代码库中,我们常常需要多次尝试升级,一旦发现问题就需要回退。性能退化尤其难以用这种升级策略来捕获。因为无法在发布前收集性能数据,我们在部署时直接从0%升级到100%,没有逐步测试的过程。

理想与实际的对比图表,展示了 React 的主版本和次版本随时间的变化。

我们的目标是通过React升级系统使升级过程更平滑,更常规,不再像英雄般艰难。具体来说,我们的目标是能够:

(注:原文中的具体目标未列出,因此此处保持一致。如果后续有具体目标,可以在翻译中补充。)

  1. 逐步地升级以便我们尽快获得反馈并吸取教训。
  2. 频繁升级以便我们的版本与升级后的版本差异尽可能小。
  3. 测试升级以便我们可以精确测量升级对性能的影响,并根据这些数据做出明智的升级决策。
设计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 /> 组件。

这里有两件事需要注意:

  1. 我们不想在应用程序中捆绑React的两个版本,这将使我们的框架捆绑包大小翻倍。此外,我们可能需要在构建时更改JSX转换,导致 <App /> 无法与其中一个版本兼容。
  2. 不清楚导入应该从哪里来。'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。

所有产品名称、标志和品牌归其各自所有者所有。本网站中提及的所有公司、产品和服务名称仅为标识目的。使用名称、标志和品牌并不表示推荐或认可。

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消