首页
Preview

重新架构Airbnb前端

概述:最近,Airbnb 重新考虑了其 JavaScript 代码库的架构。本文将探讨 (1) 促使这些更改的产品驱动因素,(2) 我们采取的步骤,以摆脱我们遗留的 Rails 解决方案,以及 (3) 新堆栈的一些关键支柱。奖励: 我们将讨论接下来的计划!

Airbnb 每天的搜索次数超过 7500 万次,这使搜索页面成为我们最高流量的页面。近十年来,工程师一直在改进、增强和优化 Rails 提供页面的方式。

最近,我们进入了除“房屋”以外的领域,推出了体验和地点。为了将这些新产品带到 Web 上,我们花了时间重新思考搜索体验本身。

在广泛搜索中转换路线

与其通过 Rails 的独立页面从我们的登陆页面 www.airbnb.com (1) 导航到搜索结果页面 (2),再到单个列表 (3),最后到预订流程 (4),我们希望用户体验是流畅的,随着他们探索和缩小搜索范围,调整用户的体验。

探索三种状态下的搜索设计:新用户、返回用户和营销 Marquee

在选项卡之间导航和与列表互动应该感觉豪华和毫不费力。实际上,现在没有什么阻止我们在小屏幕和中屏幕上提供与原生应用程序相媲美的体验。

在考虑内容的异步加载时,导航选项卡的未来概念

为了启动这种类型的体验,我们需要摆脱让我们走到这里的遗留的逐页方式,最终我们的前端代码进行了基本的重新架构。

Leland Richardson

最近在 React Conf 上讲述了在现有高流量本机应用程序的“棕地”中使用 React Native。本文将探讨我们如何在 Web 上进行类似的约束性升级。如果你发现自己处于类似的境地,希望你会发现它有用!

摆脱 Rails

在为我们的路线图上所有有趣的 渐进式 Web 应用程序 工作准备烧烤之前,我们需要与 Rails 分离(或至少在 Airbnb 中使用 Rails 提供独立页面的方式)。

不幸的是,仅几个月前,我们的搜索页面包含一些非常老的代码……像是《指环王》,触碰那个代码就可能招致灾难。有趣的是,我曾经用一个简单的 React 组件替换了一个由 Rails presenter 支持的小型 Handlebars 模板,结果页面的其他部分突然开始崩溃了,甚至在我们的 API 响应中也是如此!事实证明,Presenter 正在改变支持 Rails 模型,这已经影响了所有下游数据多年,即使没有呈现 UI 也是如此。

简而言之,我们在这个项目中像印第安纳·琼斯一样交换了偶像和沙袋,然后立刻寺庙开始崩溃,我们从滚石中奔跑而出。

步骤 1:对齐 API 数据

当 Rails 服务器渲染你的页面时,你可以以任何喜欢的方式向服务器渲染的 React 组件提供数据。控制器、助手和 presenter 可以生成任何形状的数据,即使你将页面的某些部分迁移到 React,每个组件也可以消耗其所需的任何数据。

但是,一旦你试图在客户端呈现路由,你就需要能够以预定的形状动态请求所需的数据。将来,我们可能会使用类似 GraphQL 的东西来解决这个问题,但是现在我们先把它放在一边,因为这不是我们进行此重构时的选项。相反,我们选择对我们的 API 进行“v2”的对齐,并且我们需要所有组件开始使用规范化的数据形状。

如果你发现自己处于类似的困境中,拥有一个大型应用程序,你可能会发现我们所做的迁移现有服务器端数据管道的计划是简单的部分。只需逐步查看 Rails 正在呈现 React 组件的任何位置,并确保数据输入符合 API 形状。你还可以在客户端使用作为 React PropTypes 使用的 API V2 形状进一步验证合规性。

对于我们来说棘手的部分是与所有与客人预订流互动的团队合作:我们的商务旅行、增长和度假租赁团队;我们的中国和印度市场特定团队、灾难恢复……列表还很长,我们需要重新教育所有这些人,即使可以直接将数据传递给正在呈现的组件(“是的,我知道这只是一个实验,但是……”),_所有数据 _都需要通过 API。

步骤 2:非 API 数据:配置、实验、短语、L10n、I18n……

有一类数据与我们认为的 API 数据不同,它包括应用程序配置、用户特定实验分配、国际化、本地化和类似的问题。多年来,Airbnb 构建了一些令人难以置信的工具来支持所有这些功能,但是提供它们到前端的机制有些不成熟(或者可能在构建时已经完全成熟,然后在脚下的地面开始变化之前!)。

我们使用 Hypernova 服务器渲染 React,但在我们深入进行这个重构之前,不确定在 React 组件中进行的实验交付是否会在服务器端呈现期间崩溃,或者在客户端可用的字符串翻译是否都可靠地在服务器上可用。关键是,如果服务器和客户端输出不匹配到位,页面不仅会闪烁差异,而且在加载后还会重新呈现整个页面,这对性能来说是可怕的。更糟糕的是,我们曾经编写了一些神奇的Rails函数,例如 add_bootstrap_data(key, value),可以在Rails的任何地方调用,通过 BootstrapData.get(key) 使数据在客户端全局可用(尽管不一定适用于Hypernova)。这开始作为一个小团队的有用工具,变成了大型应用和团队无法追踪的巫术来源。随着每个团队拥有不同的页面或功能,因此每个团队培养了不同的加载配置机制,每个机制都适合其独特的需求,因此“数据清洗”犯罪变得越来越棘手。

很明显,这已经崩溃了,所以我们统一了非API数据的引导机制,并开始将所有应用程序/页面迁移到Rails和React/Hypernova之间的这个标准机制。

一个用于引导非API数据的标准高阶组件

这个高阶组件有两个非常重要的功能:

  • 它接收一个标准形状的引导数据作为普通的JavaScript对象,并正确初始化所有支持工具,使服务器端呈现和客户端呈现完全相同。
  • 它除了bootstrapData之外吞噬所有内容,另一个简单的对象,我们期望<App>将其加载到Redux中,供子级根据需要使用(而不是BootstrapData.get)。

一次性地,我们消除了add_bootstrap_data,并防止工程师将任意键传递到顶层React组件中。秩序恢复到了夏尔,不久之后我们就能在客户端动态导航到路由并呈现具有物质复杂性的内容,而无需依赖Rails(这是双关语)。

前端超级加速

现在我们把目光转向客户端,重新设计了服务器端,我们现在转向客户端。

惰性加载单页应用程序

朋友们,单页应用程序(SPA)的巨大加载旋转器已经过去了。当我们使用React Router提出客户端路由的想法时,很多人反对的就是这个可怕的加载旋转器。

在Chrome时间轴中按路线分配懒惰加载的束

但是,如果你看上面的图片,你会看到通过路由拆分代码和按需加载束的影响。本质上,我们服务器呈现页面并仅提供在浏览器中进行交互所需的最少JavaScript,然后我们开始在浏览器空闲时主动下载其余部分。

在Rails方面,我们有一个控制器用于通过SPA提供的所有路由。每个操作只需要(1)进行客户端导航时客户端将进行的任何API请求,然后(2)将该数据与配置一起引导到Hypernova。我们将每个操作之间的Ruby代码从控制器、辅助函数和Presenter的几千行减少到约20-30行。成功!

但不仅仅是代码有明显的区别...

按路线提取东京的Homes:旧版页面加载与客户端路由(4-5倍差异)的对比

...现在路由之间的转换非常平滑,并且速度快了一个台阶(约5倍),我们可以开始着手动画,这也是本文开头所提到的。

AsyncComponent

在React之前,我们一次性呈现整个页面,这种做法延续到了我们早期的React时代。但是,我们使用了一个类似于这个的AsyncComponent,以在挂载后加载组件层次结构的部分。

这对于不初始可见的重型元素非常有用,例如模态和面板。我们明确的目标是,只交付最初呈现页面所需的JavaScript,并使其交互,而不是多余的一行代码。这也意味着,例如,如果团队想在没有使用D3的页面上使用D3来制作模态中的图表,他们可以将下载该库的“成本”作为其模态代码的一部分,与页面的其余部分隔离开来。

最重要的是,这种方法可以在需要时轻松使用:

在这里,我们可以简单地将同步版本的地图替换为异步版本,这在小断点上非常有用,在这里地图通过按钮的用户交互显示。由于大多数这些用户都在使用手机,所以在担心Google Maps之前让他们进行交互,可以在页面加载时间上获得可观的提升。

此外,请注意scheduleAsyncLoad()实用程序,它在用户交互之前请求包。由于该地图如此经常使用,因此我们不需要等待用户交互才能请求它。相反,我们可以在到达Homes Search路由时将其排队。如果用户在下载之前请求它,则在组件可用之前,他们会看到一个合理的<Loader />。不用担心。

此方法的最终好处是HomesSearch_Map成为浏览器可以缓存的命名包。当我们解聚较大的基于路由的束时,应用程序的缓慢更改部分保持不变,从而进一步节省JavaScript下载时间。

将无障碍性构建到我们的设计语言中

毫无疑问,这需要一个专门的帖子,但我们已经开始构建我们的内部组件库,并强制执行无障碍性作为硬约束。在未来的几个月中,我们将替换所有与屏幕阅读器兼容的客人流程中的UI。

通过我们的设计语言系统将无障碍性构建到我们的产品中的示例

该UI足够丰富,我们希望将CheckBox与标题以及aria-describedby的子标题相关联。为了实现这一点,需要在DOM中具有唯一标识符,这意味着强制要求将所需的ID作为任何调用父级需要提供的prop。这些是UI可以施加的硬约束,以确保如果在产品中使用组件,则已经构建了无障碍性。

上面的代码还演示了我们的响应式实用程序HideAt和ShowAt,它们允许我们在不使用CSS隐藏和显示的情况下,在不同的屏幕大小下大大改变用户体验。这导致页面更加简洁。

关于状态的手术和哲学

没有处理应用程序状态的前端文章是不完整的。

我们为所有API数据和“全局”状态(例如身份验证状态和实验配置)使用Redux。个人而言,我喜欢redux-pack用于异步操作。你的结果可能有所不同。然而,由于页面的复杂性,特别是在搜索方面,使用Redux处理表单元素等低级用户交互并不起作用。我们发现,无论我们如何优化,Redux循环都会使输入框的输入感觉不够灵敏。

我们的房间类型筛选器(如上图所示)

因此,我们使用组件本地状态来处理用户的所有操作,直到触发路由更改或网络交互,我们没有遇到任何问题。

同时,我喜欢Redux容器组件的感觉,我们发现即使使用本地状态,我们也可以构建可以共享的高阶组件。一个很好的例子是我们的过滤器。搜索底特律的住宅,你会在页面上找到几个不同的面板,每个面板都可以独立操作,可以修改你的搜索。在各种断点上,实际上有数十个组件需要知道当前应用的搜索过滤器以及如何更新它们,既可以在用户交互期间临时更新,也可以在用户正式接受后更新。

这里我们有一个不错的技巧。需要与过滤器交互的每个组件都可以用这个HOC包装,然后就完成了。它甚至附带了属性类型。每个组件都会将响应过滤器(与当前显示的结果相关联的过滤器)与Redux连接起来,但保留了一个本地的stagedFilters对象可供修改。

通过这种方式处理状态,与我们的价格滑块交互不会影响页面的其他部分,因此性能很好。但是所有过滤器面板都使用相同的函数签名实现,因此开发很简单。

下一步是什么?

现在,将前端带入现在的艰苦工作基本上已经完成,我们可以将注意力转向未来。

  • 核心预订流程中所有页面的AMP版本将导致移动Web上从Google搜索开始的次秒(在某些情况下)交互时间,并且为了实现这一点所需的许多更改将在移动Web和桌面Web上驱动P50 / P90 / P95冷加载时间的显着改进。
  • PWA功能将导致返回访问者次秒(在某些情况下)的交互时间,并打开了首次离线功能对于连接不稳定的用户非常重要。
  • 放弃遗留技术/框架的最后一锤将使捆绑大小减半。这不是华丽的工作,但最终摆脱jQuery、Alt、Bootstrap、Underscore和所有外部CSS请求(它们会阻止渲染,而且97%的规则未使用!)将简化我们发送的代码以及新雇员需要学习的印记。
  • 最后,是手动追踪渲染瓶颈、异步加载不可见于初始渲染的代码、避免不必要的重新渲染以及减少重新渲染的成本的辛勤工作。这些改进是一个笨重的应用程序和一个良好的机器之间的区别。

译自:https://medium.com/airbnb-engineering/rearchitecting-airbnbs-frontend-5e213efc24d2

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
一个人玩
先找到想要的,然后出发

评论(0)

添加评论