用React Native和Uber Engineering为UberEATS提供动力

0
用React Native和Uber Engineering为UberEATS提供动力

UberEATS在美国,我们的目标是让你从最喜欢的餐厅点餐,就像叫uberX或uberPOOL一样无缝。就像推出任何新产品一样,构建一个食品配送网络也会带来工程上的成功和惊喜。尽管美味,但这种新的美味乘客(食物!)也带来了不少挑战。例如,它不能指定自己的首选路线或与司机聊天,而且在接送时确实需要更多的步骤。在这篇文章中,我们特别关注一个挑战:Uber Engineering如何将第三方引入到之前的双边市场中。

幸运的是,我们能够利用Uber现有的技术栈,让UberEATS迅速上线并运行起来。旅行变成了送货。司机伙伴变成了送餐伙伴,骑手变成了食客。但并没有类似的团队,因为在过去的五年中,人们一直认为一次旅行只有两个人参与;而不是三个人一份芝士披萨,一份泰式炒河粉或鸡肉法希塔。

建立餐厅仪表板

图1:UberEATS市场包括三方:餐厅、外卖合作伙伴和食客。这种新动态彻底颠覆了优步传统的双面模式。

餐馆需要一种与送餐伙伴和食客沟通的方式。至少,双方需要传达以下信息:

  1. 发布新订单
  2. 接受订单
  3. 交付伙伴的到来
  4. 完成订单

这四个基本需求催生了“餐馆仪表盘”反应/通量通过平板设备访问的单页web应用程序。

图2:显示一个活动订单的餐厅仪表盘。

为未来50个城市修改餐厅仪表盘

自从独立的应用程序初始启动在2015年12月的多伦多会议上,我们继续致力于为餐厅创建一个轻松、可靠的接口,用于协调配送。几个月后,我们清楚地意识到,为了继续改进Restaurant Dashboard,有必要进行一次彻底的修改。

我们的网页应用只提供对设备的有限访问,这被证明是一个严重的问题,因为它限制了我们与餐厅交流重要信息的能力。这方面的一个例子是,在基于声音的通知提示出现之前,用户必须与网页进行交互。餐馆里熙熙攘攘,所以声音是通知餐厅员工新订单的重要方式,或者是发货的时间合伙人已经来取一个了。为了解决这个问题,我们在每次加载页面时都显示一个模式,以便强制用户交互。虽然这允许我们播放声音,但这是以用户体验为代价的。

图3:Restaurant Dashboard显示了强制用户交互的模式,从而启用声音。

我们还需要构建一些在网页浏览器上完全不可行的功能,或者只能在高度受限的格式中使用。例如,打印纸质收据是许多餐厅的必备功能,但网络浏览器只允许使用该功能的餐厅使用该功能AirPrint兼容的打印机。这种限制给餐厅和工程师带来了很大的困惑和挫折。我们意识到,为了克服这个障碍,我们需要访问硬件,这将允许我们使用打印机供应商提供的原生sdk直接与打印机通信。

评估本地反应

虽然现在下结论还为时过早反应本地作为移动应用开发的灵丹妙药,它似乎非常适合UberEATS的用例。因为《Restaurant Dashboard》最初是面向网页而创造,所以我们的团队拥有大量使用React的经验,但却缺少iOS/Android曝光率。我们从UberEATS成立之初就开始为它工作,积累了大量关于该服务的餐厅部分如何运作的知识。这些考虑使React Native成为一个引人注目的选择,它为网络语言的移动开发提供了一个平台。它为我们提供了“烹饪”我们想要达到近乎完美的应用程序所需的工具。

多平台支持也是我们关心的一大问题。目前,Uber与餐厅密切合作,寻找平板设备并安装餐厅仪表盘应用程序,但随着UberEATS的继续扩大,这种做法可能变得难以持续。当我们转向自带设备(BYOD)模式时,Uber的司机-合作伙伴方面也经历了类似的转变。通过以一种平台无关的方式构建UberEATS应用程序,我们可以选择以后扩展到Android,并支持两个平台向前发展。

React Native对于我们来说是一个可行的选择,它在我们现有的移动基础设施中工作,并支持最初促使我们转向本地应用的各种功能也是很重要的。为了做到这一点,我们构建了一个“演示”应用程序,专门用于验证关键功能。这包括我们从Uber的其他团队引进本地依赖来测试功能,包括崩溃报告、用户认证和分析。由于这些特性跨越了原生的Objective-C层和解释的JavaScript层,这也是对我们交付需要在这两个非常不同的环境之间集成的特性的能力的一个有用的测试。

总的来说,演示能够提供我们想要的结果。像崩溃报告这样的库可以独立于应用程序的业务逻辑运行,可以开箱即用。对于触发分析事件等功能,桥接到JavaScript层也被证明是非常简单的。事后看来,这种技术障碍的缺乏可能导致我们过于依赖原生库,而原生库和JavaScript功能之间的这种紧张关系将继续影响我们后来的许多架构决策。

构建迁移路径

最初的目标是构建最小数量的脚手架,以使Restaurant Dashboard本机运行。为了实现这一点,我们创建了一个本地导航和认证系统,以及一个指向我们现有web应用的WebView。

图4:上面的图表展示了本地和web Restaurant Dashboard Flux商店之间的交互。

来自WebView的网络请求使用NSURLProtocol以便有必要的身份验证头。额外的钩子被添加到窗口中,这允许我们通过向WebView注入JavaScript来更新基于web的Restaurant Dashboard的通量存储。这在逐渐迁移功能方面给了我们很大的灵活性。

有了这个功能平价的最小可行产品(MVP),我们就可以迅速开始在真正的餐厅中进行测试。它还解锁了一些本地功能方面的“快速胜利”。我们集成了几个本地打印机sdk,以扩大兼容打印机的范围,超出AirPrint支持的范围。我们还禁用了睡眠模式,这只需要一行本地代码,但从网络上是不可能做到的。

然后,应用程序的其余部分可以逐个迁移到React Native。在可能的情况下,我们的目标是让这些迁移成为更广泛的特性工作的一部分,而不是为了重写而重写。

定义架构

如前所述,React Native融合了网页和移动开发,允许我们在本地或JavaScript中编写功能。这种功能还带来了移动和web社区的模式和概念。这种思想的大熔炉给了我们更多的选择,但也在选择正确的抽象概念方面提出了新的挑战。

我们最终设计UberEATS的方式和我们设计普通快餐的方式差不多反应/回家的web应用,尽可能避免iOS模式和模块。幸运的是,对于我们的需求和偏好来说,web概念和技术总体上可以很好地转化为本地开发。

这款应用的路由功能就是一个很好的例子。在web上,Restaurant Dashboard使用了流行的react-router库,该库允许以声明的方式定义路由,这与视图的方式非常相似。然而,该系统假定存在url,而这些url往往在浏览器之外是缺乏的。React Native提供了一个命令式导航库,它类似于UINavigationController

为了提高速度,我们最初保留了反应路由器库,目的是在MVP启动并运行后替换路由框架。方法很容易解决不存在URL的问题HTML5的历史JavaScript中的API,它只是一个堆栈。

当需要从React -路由器迁移到其中一个React原生库,如Navigator或NavigationExperimental,新的实现似乎并没有提供比我们当前解决方案更有吸引力的优势。事实证明,无论您是在浏览器中还是在本机中,普通的反应路由器都是一种非常棒的路由方式。

从移植过程中得到的另一个关键教训是,最小化iOS和JavaScript之间的交互并将逻辑集中在JavaScript层是非常有利的。这样做有许多显著的好处,例如:

  • 减少JavaScript和Objective-C之间的上下文切换
  • 增加可移植性(通过减少特定于平台的代码)
  • 减少bug的范围

当我们开始这个项目时,我们开发了一个简单的API用于与原生层通信。虽然我们认识到保持这个层很薄的优点,但我们低估了React Native层中可以保留多少代码。分析和登录等功能基本上只是网络调用,可以相对轻松地用JavaScript实现,而最初用Objective-C编写的代码需要移植到Java才能支持Android。然而,更有可能的是,我们将利用这个机会用JavaScript重写这些库,以便它们可以跨平台共享。

自动将更新

React Native应用程序通过少量的Objective-C/Java代码引导,然后加载JavaScript包。与任何其他资产一样,bundle随应用程序一起发布。正如我们所建议的,如果业务逻辑仍然集中在bundle中,则可以通过在启动时加载不同的JavaScript文件来更新应用程序,这是一个简单的过程。在本机层,应用程序可以更改React native桥接使用的文件,并请求重新加载它。

为了保持更新逻辑与平台无关,我们选择更进一步,在桥的周围创建一个本机包装器,允许JavaScript包本身确定加载的是哪个包。

图5:Restaurant Dashboard在任何时候都可以存储多达三个JavaScript包。

Restaurant Dashboard定期检查新的捆绑包并自动下载。本机代码和包代码都遵循语义版本控制,为每个新部署分配唯一标识,如果更改了本机- JavaScript通信接口,则会被认为是破坏的。例如,重命名分析模块AnalyticsV2会被认为是破坏性的更改,因为从JavaScript包到Analytics的现有调用会触发异常。

当然,即使非常注意语义版本控制,仍然有可能出现错误的更新。在UberEATS上下文中,糟糕的更新指的是一个包更新导致Restaurant Dashboard在包处理逻辑有机会运行之前崩溃。崩溃的时机将使它不可能通过推出一个新的捆绑包来解决问题。导致这类不稳定的更新最终会发生,因此有一个能够检测不稳定构建并从中恢复的弹性系统是很重要的。

避免部署糟糕更新的一种方法是将每次发布都视为试验,这样可以逐步推出,如果必要的话,还可以回滚更新。

图6:Restaurant Dashboard的回滚过程确定要加载哪个bundle。

为了让回滚过程正常工作,Restaurant Dashboard需要识别出它有一个坏的包,然后重新加载一个“安全”的包(意思是,一个我们知道是没有错误的包,比如应用最初附带的包),否则它将无法找到回滚到哪个版本的软件。我们通过自动重新加载与应用程序一起打包的原始JavaScript包来实现这一点,然后加载两个推送包中的一个:最新的安全包或最新的包。如果最近的bundle可以被加载,它就会成为安全的bundle。在不存在安全包的情况下,原始包将继续使用,不进行更新。

这种更新Restaurant Dashboard的方法比常规手机应用更新的阻力要小得多,因为可以根据需要发布新版本,将新功能的发布时间从几周缩短到几天。更新在后台下载,完成后加载,避免用户交互。这种缺少即时用户交互的情况使得更新能够更快地传播,并且大多数设备可以保持在最新的版本上。同样的机制还允许我们快速回滚错误的构建,最大限度地减少对餐厅合作伙伴的破坏。

虽然以这种方式推送更新并没有完全取代正常的应用发布(游戏邦注:iOS或Android原生代码的更改仍然偶尔需要更新),但它降低了更新的频率。随着本机层随着项目的发展而逐渐成熟,我们希望这种趋势能够继续下去。

测试和类型检查

在Uber Engineering中,团队行动迅速,web项目倾向于在变更被推送到存储库时发布,而不是等待构建火车。这与通常与移动应用相关的多周发布过程形成鲜明对比。当我们在开发Restaurant Dashboard时考虑转移到本地应用程序时,我们担心应用程序的稳定性可能会因为这种紧凑的周转而受到影响;毕竟,如果你在React Native解释器中崩溃,你就会在现实生活中崩溃。即使捆绑推送提供了一种降低这种风险的方法,崩溃也远非理想。

单元测试和浅呈现已经出现了很长一段时间,但是最近在JavaScript社区中出现了一股将静态类型检查纳入其中的趋势或打印稿。

在这一次更新应用程序时,我们决定使用Flow进行类型检查,这个决定让我们对业务逻辑的正确性有了额外的信心。事实上,它已经被证明是一种非常有用的工具,可以在代码到达生产环境之前进行测试和捕获错误。

Flow强大的一个简单例子是类型检查减速器功能。如下所述,减速机以当前状态和动作作为输入,反过来,它期望返回一个新的状态作为输出:

处理副作用

使用Flow进行类型检查允许我们验证我们的状态在这个过程之后保持了正确的形状,这是Flow社区的一个荣誉,新发布的版本继续在我们的应用程序中发现可能的错误来源。此外,与可选类型相关的最小开销意味着它不会阻碍快速迭代和开发。

餐厅仪表板使用回来的用于管理数据流。Redux为我们提供了一种简单、可预测的方法,通过以下几个关键原则来建模应用程序状态:

  1. 所有的状态都在存储中,这是一个单一的不可变对象
  2. 视图将存储作为输入,并呈现React Native组件
  3. 视图可以分派操作,这些操作是修改存储的请求
  4. 简化程序将操作和当前状态作为输入,返回一个新存储

经常需要修改存储以响应异步操作,例如网络请求。Redux没有规定一种方法来做到这一点,但常用的方法是使用一个ddlew一个reforReduxth一个t允许操作是返回承诺并在此过程中分派其他操作的函数。

图7:在Restaurant Dashboard中,数据流经Redux应用程序。

我们最初的方法是使用thinkks,但我们很快就遇到了问题,因为我们的应用程序逻辑(和副作用)变得更加复杂。具体来说,我们遇到了两种不适合坦克模型的副作用模式:

  1. 定期更新应用程序状态
  2. 副作用之间的协调

传奇一个n替代年代deeffectodelforRedux一个pp年代,利用ES6 (ECMAScript 6)生成器函数提供了一个不那么复杂的选项。它们不是扩展操作的概念,而是被建模为一个独立的线程,该线程可以访问存储、监听Redux操作并分派新的操作。为了避免与坦克相关的问题,UberEATS.com最近完全迁移到Sagas,这让我们相信它们可以扩展,并且足够成熟,可以满足我们的需求。(这里没有无尽的传奇!)

Sagas真正发挥作用的一个领域是管理应用程序状态的周期性变化,比如检索活动订单的新列表。使用“坦克”可以实现这一点,但远非优雅。(谁能想到呢?不是我们!)例如,组件可以定期分派操作来获取订单;或者,坦克可以递归调用自己。然而,除了实现问题之外,无论是具有计时器逻辑的组件——还是不断触发自己的独立坦克——都不能很好地适应Redux模型。

saga为解决这个问题提供了一种干净的方法,因为它们使我们能够创建一个长期存在的任务,定期获取新订单并分派一个操作来更新存储。

与长时间运行的任务相关的一个问题是保持它们之间的通信,如下所示:

在上面的获取订单示例的基础上,应该只在存在有效的用户会话时检索订单并更新存储。执行此规则的失败可能会导致不明显的错误,例如在注销餐厅和更新其订单之间存在竞争条件。这反过来可能会揭示触发崩溃或来自UI的奇怪提示的边缘情况,因为输入订单的代码可以很合理地假设不存在一家餐厅。

防止这类问题相对简单,但是识别潜在的竞争条件和添加必要的检查是费时且容易出错的。更重要的是,我们的订单代码不应该关注用户会话的状态,因为它们是两个独立的关注点。

saga提供了一种简单的方法来监听与会话相关的操作,并启动或停止获取命令的后台任务。例如,当我们看到登录事件时,我们应该分离一个任务来定期获取订单,如果看到注销,则取消该任务。这可以简单地用Saga来表达,如下:

分叉任务是另一个生成器,它将继续运行,直到它或其父任务被终止。

事实上,事实证明这种针对特定操作的限制任务的模式是相当常见的。就像组件装饰器一样,我们可以把这个逻辑拉到一个更高阶的生成器函数中,如下所示:

Sagas的特性还简化了测试过程。使用Saga,单元测试给定的功能就像调用相关的Saga并对结果进行深入比较一样简单。

这种让许多小服务通过消息传递相互通信的方法对于许多后端工程师来说是很熟悉的,但是我们生成和使用Redux操作而不是卡夫卡事件。从开发人员的角度来看,观察这些模式应用于客户端代码非常有趣。

反思UberEATS之旅

几乎不可能在一篇文章中总结部署一个应用程序的整个体验,尤其是一个如此显著地影响了餐厅与UberEATS应用程序交互的方式的应用程序。如果有什么不同的话,我们希望这篇文章能够为我们团队为UberEATS选择React Native的思考过程提供一些额外的见解,以及我们为确保我们的餐厅合作伙伴的稳定和强大的用户体验所采取的一些步骤。

虽然React Native仍然只构成了UberEATS工程生态系统的一小部分,但我们使用它来重建Restaurant Dashboard的经验是非常积极的。自去年实施以来,改进后的“餐厅仪表盘”已经成为UberEATS上几乎所有餐厅的标准工具。按照这个速度,随着我们扩大用户市场,我们对框架继续满足我们需求的能力持乐观态度。

有兴趣在UberEATS上用React Native做一些美味的东西吗?一定要看看UberEATS工作委员会我们UberEATS开发团队的空缺职位。

克里斯·刘易斯(Chris Lewis)是Uber的软件工程师,负责UberEATS的餐厅仪表盘。为了推动他的编程,克里斯使用UberEATS从他最喜欢的旧金山餐厅订购寿司。

评论