计算卡路里:我们如何提高UberEats.com的性能和开发者体验

0
计算卡路里:我们如何提高UberEats.com的性能和开发者体验

在Uber Eats,我们希望无论在桌面还是移动设备上,只要按一下按钮,就能尽可能轻松地点餐。这就是为什么我们的工程团队花费大量时间为餐厅和顾客考虑、构建和维护web应用程序。Uber Eats在很大程度上依赖于基于网络的应用程序餐馆的应用(它的目标是通过网络React Native for Web)广泛的分析平台而且菜单工具

UberEats.com它可以让食客通过网页界面点餐,填补了网页应用提供更好用户体验的利基,对我们的移动应用形成了补充。例如,额外的屏幕空间使大订单更容易下。

它还使我们能够灵活地将Uber Eats订餐体验与其他Uber应用程序集成,提供更无缝的体验,并释放用户手机上的本地存储空间。例如,在新版的Uber Rider应用程序中,用户可以直接在同一个界面上点餐,而无需安装UberEats应用程序。UberEats.com的WebView版本使这一功能成为可能。

UberEats.com团队花了一年时间从头重写了这款网页应用,使其性能更好,更易于使用。在完善UberEats.com的同时,我们优先考虑在不影响质量或稳定性的情况下提高开发人员的生产力。

然而,正如许多工程师所知,重写并不是万能的;它们可能是昂贵、耗时的努力。当计划重写时,很容易低估手头任务的范围,为项目的失败做好准备。尽管存在风险,从头构建一个新系统往往比修改现有系统更有吸引力,因为它提供了一个从头重新思考现有体系结构并解决任何结构性痛点的机会。尽管有这些好处,但使用现有的系统更安全:工程师通常了解当前的实现,并且可以立即准确地预测任何迁移项目的复杂性。

考虑到这一点,我们在决定是否重写UberEats.com时考虑了许多因素。最终,我们认为重写可以节省时间和资源,并实现新的直观功能。这一决定导致了一个更高性能和可扩展的平台,为web应用程序的用户提供了更好的用户体验。

传说中的传奇

UberEats.com是用JavaScript编写的,于2016年初作为一个React单页web应用程序推出,利用Redux/Redux- saga进行状态管理,后端由Express(一个流行的Node.JS HTTP服务器)提供支持。自定义代码允许React组件在服务器上呈现。然而,Saga模式的使用导致了额外的复杂性,阻止了web应用程序扩展以满足用户需求并跟上开发速度。

使用UberEats.comRedux-Saga对于数据获取等异步操作。这种情况下的Saga与1987年白皮书中介绍的Saga模式非常不同,传奇,由赫克托·加西亚-莫利纳和肯尼斯·塞勒姆编写,旨在提高长生命周期事务的性能。相反,redux - saga的行为类似于微服务,使用“效果”进行通信,“效果”具有类型和有效负载。

效果可以从Redux存储中读取状态,等待承诺,并监听特定的Redux操作。从后端获取用户信息的效果可能是这样的:


电话:{
fn: fetchUserInformation, //返回一个promise的函数
args:[1] //用户ID

saga对于编排长时间运行的异步任务特别有用。例如,一个Saga可以定期调用HTTP端点以获取正在进行的订单信息,而另一个Saga可以侦听更新并显示通知,如下所示:

函数fetchOrdersSaga() {
While (true) {
//调用HTTP端点
尝试{
const orders = yield调用(fetchOrders);
收益率put (fetchOrdersSuccess(订单);
} catch (err) {
收益率put (fetchOrdersFailure (err));

//等待5s
Yield call(delay, 5000);

函数notifyUserSaga() {
//等待,直到我们有必要的权限
const enabled = yield call(getNotificationsPermission());
If (!enabled) {
返回;

While (true) {
//等待订单存储的更新
const orders = yield take(ORDERS_UPDATE);

//如果发生了有趣的事情,通知用户
if (isFoodArriving(orders)) {
收益率(foodArrivingNotification(订单)


随着时间的推移,工程师们开始严重依赖于Sagas进行日常功能开发。这种对Sagas的依赖产生了“UI Sagas”模式,即仅用于加载呈现特定组件所需的数据的Sagas。例如,我们的Restaurant Categories页面有一个Saga,它断言存在一个有效的类别,并为该类别获取相关的商店。我们的数据层由超过100个saga组成,分布在68个不同的reducer上,其中超过一半是“UI saga”。

随着saga数量的增加以及saga之间相互依赖关系的增加,代码库的维护和特性开发变得越来越慢。重构代码很容易出错,因为很难确定某个特定页面或特性需要哪些saga才能正常工作。Sagas可能隐式地依赖于已经运行的其他Sagas,但是抽象本身并没有提供静态验证这些依赖关系的方法。工程师在应用程序的某个部分重构Saga时,可能会无意中破坏应用程序的另一部分。例如,9个Saga正在侦听和响应USER_ADDRESS_LOADED事件。更改地址输入流需要工程师跟踪每个Saga如何使用事件,并确保它将继续正常工作。在送餐业务中,地址是一个非常重要的信息,并且,不可避免地会引入错误,导致用户饥饿。

利用现有的状态也很困难。添加新特性可能需要跟踪几十个文件,以确定某个特定字段被填充的位置。用户的状态(附近的餐馆、订单等)被提取并归一化为不同的Redux商店。从Redux存储中读取和调整状态的Memoized函数虽然有助于提高性能,但也增加了额外的复杂性。

除了降低维护和特性开发的速度外,无法静态推理Saga的相互依赖关系也使得安全的代码分割应用程序变得困难。代码分割的目的是通过将应用程序代码分成不同的包来提高性能,从而尽量减少发送到浏览器的代码量。

我们使用Webpack动态导入语法用于代码分割。动态导入的工作原理与常规的JavaScript导入类似,不同的是,您将得到一个解析为导入代码的承诺,而不是返回对导入代码的引用。这让我们能够表达那些东西可能是基于代码中最终遵循的分支的依赖项。Webpack使用静态分析来识别可能的执行路径,并将代码库分组到不同的包中。为了最大化代码分割的好处,工程师需要一个非常模块化的代码库;单一的常规导入链将阻止Webpack生成合理大小的包。

UberEats.com JavaScript文件的依赖关系图
图1。在这个JavaScript文件和组件的依赖关系图中,细线表示动态导入,粗线表示常规导入。这些颜色说明了将这些文件分成单独的包的一种方式。蓝色组件被标识为所有入口点的公共组件,而黄色(左)和绿色(右)可以放在单独的包中。

在UberEats.com的第一个版本中,我们尝试将每个顶级页面的代码分成不同的包,以加快加载时间。我们也使用了单一的整体node_modules为我们的项目依赖项创建Bundle,并为每个顶级页面创建单独的Bundle。单片的一个优点node_modulesBundle的特点是,项目依赖关系不会像代码库的其他部分那样经常更改,这使得浏览器更容易缓存。

不幸的是,代码分割并没有在第一版UberEats.com上带来显著的性能提升。数据层(Sagas、reducer、selector等)在我们整个应用程序的大小中占了很大的比例,不能动态地拆分和加载,因为我们不能静态地确定给定页面需要数据层的哪些部分。即使顶级页面的包大小很小,它们仍然依赖于大约300kb的数据层代码,这就限制了我们可以使用更小的包来保持较低的加载时间并提供良好的用户体验。

很明显,需要对代码库进行重大更改,以解决开发速度缓慢和性能问题。

UberEats.com的新架构

最初的UberEats.com是在使用React进行服务器端渲染时编写的,这就需要为服务器端渲染和数据获取等核心功能提供自定义解决方案。由于这些技术现在处于更加成熟的状态,我们希望转向更加标准化的解决方案,从而减轻我们团队的一些维护负担。

新的UberEats.com使用Fusion.js这是一个由优步网络平台团队构建的现代开源框架。Fusion.js通过强大的插件架构、服务器端渲染支持和易于使用的代码分割增强了典型的React/React- router /Redux设置。

在底层,Fusion.js使用React Router版本4,一个流行的开源路由库,这使得基于路由的代码拆分变得容易。React Router使用一种声明式的方法,允许将新特性作为路由添加到适当的页面上,并在导航到该路由时惰性加载特定于特性的代码。例如,我们通过向餐厅页面添加路由,添加了在启动后下组订单的功能,当导航到餐厅页面时,将加载组订单组件、状态管理和数据获取代码。然而,为了完全拆分重写的代码,我们需要以不同于原始UberEats.com的方式构建数据层。

数据层

最初版本的UberEats.com有太多的Sagas和reducer,最终导致了一个臃肿的全球状态。我们的性能和开发人员生产力目标要求我们保持全局状态精益。在重新构建数据层时,我们对如何防止历史重演有一些不同的想法。

保持全局状态大小较小的一种方法是惰性加载减速器,并限制何时saga可以消耗和分派动作。但是,一旦我们为每个页面绘制出数据需求,一个更简单的解决方案就显而易见了。我们的大多数应用程序状态只与它所访问的页面相关,这意味着我们可以依赖React的本地状态而不是Redux的全局状态。我们没有尝试扩展Redux,而是计划大幅减少对它的使用,并将状态管理从全局Redux存储转移到使用它的组件。这也与hook的介绍,这种模式简化并鼓励React组件中的本地状态管理。

在我们的新架构中,我们仍然使用Redux,但只用于两个用途:缓存我们不想在页面导航之间重新加载的数据(例如,餐厅提要,餐厅菜单),以及在服务器呈现后为客户端补充水。本地状态用于所有用户交互(例如,选择项目自定义)以及我们不想在页面导航之间缓存的数据。

通过使用局部状态,我们能够将状态管理逻辑与使用它的组件放在一起。遵循这种模式使得代码拆分变得很简单——当我们从它的主包中拆分路由时,系统将它的相关数据管理逻辑也随之拆分,如下面的图3所示:

UberEats.com后端示意图
图2。UberEats.com后端发送不同的包给用户基于用户代理头以及页面正在被访问。我们使用分离入口点,移动设备和桌面设备接收完全不同的顶级组件。这些组件中的包拆分让我们能够隔离不同的特性或页面,以及它们各自的数据获取和表示需求。

数据层处理向组件获取和提供数据,但组件需要一种方法来处理可能尚未加载或加载失败的数据。为了处理数据可能处于的不同状态,我们使用一个相当简单的抽象,称为Maybe。

type MaybeType = {|
数据:? T,
错误:?错误,
hasLoaded:布尔,
isLoading:布尔,
|};

我们可以将这个Maybe与其他Maybe结合起来创建一个新的Maybe,或者我们可以将它传递给我们的mayloaded组件来有条件地呈现数据,如下所示:

< MaybeLoaded
源={秩序}
loading={() => < loading />}
error={err => }
loaded={order => }
/>

我们的数据层使用fusiont .js提供rpc风格的接口.这个接口符合我们最常见的数据获取需求,即从单个端点请求实体(例如,餐馆列表或餐馆菜单)。许多Uber的新网络应用程序使用GraphQL和Apollo从多个服务请求和组合数据。将来,我们可能会迁移到GraphQL和Apollo,以简化我们的数据获取代码。我们也很高兴有可能交换maybelloaded一次悬念因为数据获取是稳定的,因为这将与我们依赖现有库而不是自定义代码的架构选择保持一致。

通过使数据的获取和管理尽可能靠近消费组件,我们能够允许我们的数据层轻松地在块之间分割。由此产生的模块化数据层尤其重要,因为我们需要在特定于平台的组件之间重用数据层。

两个入口

通常,我们可以通过使用媒体查询来显示、隐藏或移动内容,从而构建一个在移动和桌面浏览器上都可以运行的网站。然而,当每个平台上的内容差异太大,难以维护时,媒体查询就会出现问题。在最初的代码库中,我们在组件中使用媒体查询和平台代码路径来解决不同平台上的内容差异。这导致组件树具有许多条件分支,使得工程师很难跟踪UI元素被呈现在哪里。

在重写开始时,我们决定基于平台有两个单独的入口点。我们这样做是为了避免一个入口点在原始代码库中创建的维护开销,并确保每个平台只加载特定于它的代码。

现在,移动和桌面用户只需要与他们平台样式所需的代码交互,简化了开发人员的工作流程,因为他们现在只需要处理具有更少分支的更小的组件。分割代码还可以防止在没有在两个平台上测试更改时导致的样式回归。虽然这两个平台确实共享了许多相同的代码,包括按钮、输入和列表项等元素,但当涉及到安排这些共享组件的布局代码时,它们通常会出现分歧。

UberEats.com结帐页面截图
图3。在移动设备(左)和桌面(右)的结帐屏幕截图中,我们用绿色突出显示了平台间共享的组件。

例如,实现单独的入口点显著简化了UberEats.com的地址输入流程。在网站的桌面版本中,这显示为位于标题中的下拉类型。而在移动设备上,流程是自己的页面。能够将这两种体验分开来极大地降低了表示组件的复杂性,但仍然允许我们共享两个平台使用的业务逻辑代码。

UberEats.com为移动和桌面平台保留了两个入口点,这使得UberEats.com可以创建专门为单一平台定制的组件,这意味着它们需要包含更少的代码。除了布局组件之外,特定于平台组件的一个例子是餐厅页面头,它在桌面和移动端上的设计不同,以利用更大的屏幕。这两个入口点还允许我们生成只包含请求平台所需代码的块。除了基于平台的代码分块之外,我们实际上还根据请求设备的功能提供不同的代码。

不同的浏览器有不同的捆绑包

就像我们必须针对不同的设备重写UberEats.com一样,我们也需要针对不同的浏览器进行优化,这就带来了另一组挑战和机遇。JavaScript尤其会带来问题,因为英语是一种迅速发展的语言,有八种新语言建议预计将于2020年出版。这些特性可以帮助简化代码,减少bug的表面积,或者解锁新功能。之前讨论过的动态导入语法是2020年提议的功能之一。

要真正使用这些新功能,我们需要浏览器来支持它们,并让人们使用受支持的浏览器。不幸的是,浏览器可能没有完整的实现,或者根本不支持某个特性。在重写后的UberEats.com上,我们使用转换和填充来利用这些新功能,而不必等待所有用户升级到兼容的浏览器。由于转换和腻子程序会增加包的大小,所以我们只希望将它们包含在不支持该特性的浏览器中,为了做到这一点,我们提供了特定于浏览器的包。

转换接受一个新特性,并使用我们知道浏览器将支持的语法来表示它。例如,我们可以表示更新的为. .的迭代语法是常规的循环,老浏览器可以理解:

代码转换
图4。在UberEats.com重写中,我们的转换简化了“for ..”的“to a”for,以便所有浏览器都可以使用此功能。此转换由a提供巴别塔的插件

polyfill还用于添加对新功能的支持。例如,包括方法于2016年添加到JavaScript Array对象。我们可以在旧的浏览器中通过包含一个扩展Array对象原型的填充来支持这一点。这个功能的一个非常简单、不完整的实现是:

Array.prototype.includes =函数(i) {
返回this.indexOf(i) != -1;
};

虽然这些示例看起来很简单,但转换和腻子的一个不幸的副作用是它们可能非常冗长。正确地捕获特定特性的语义可能需要大量代码。填充物发生器功能例如,有700多行代码。转换所需的额外代码可能一开始很小,但随着重复使用,很快就变得很重要。

代码转换使用Babel插件
图5。当我们使用Babel插件将可选链接转换为ES5时,代码会变得非常冗长。在本例中,一行代码变成了七行,这增加了包的大小。

当一项特性被现代浏览器广泛支持时,为了少数传统浏览器而增加捆绑包大小是很难的,因为这最终会降低大多数用户的体验。另一方面,不引入转换意味着一些用户可能根本无法使用该站点。

对象解构是这个问题的一个很好的例子,因为除了Internet Explorer之外的浏览器都很支持该特性。在最初的UberEats.com上,我们不得不在一个功能一个功能的基础上做出这种权衡,以确保我们不会让捆绑包的大小膨胀太多,同时继续支持访问网站的所有常用浏览器。

对于新的UberEats.com,我们能够避开这种困境。在构建时,Fusion使用babel-preset-env为我们的应用程序创建“遗留”和“现代”bundle集。现代捆绑包是为相对现代的浏览器设计的,因此可以使用更少的巴别塔转换,而遗留捆绑包允许我们通过为旧浏览器包含更多的转换和填充程序来继续为尽可能广泛的受众服务。这种方法使现代浏览器的包大小减少了15%,并确保使用传统浏览器的用户仍然可以访问站点。

服务器端呈现

除了提高浏览器的可访问性之外,我们还想确保重写后的UberEats.com能够以最小的延迟呈现其UI。为了实现这一点,我们选择在服务器上呈现站点页面,确保所有必要的HTML和CSS都可以加载,而不需要执行任何JavaScript。通过Fusion.js强大的服务器端渲染支持,这变得很容易。当在服务器上运行时,Fusion的默认行为是在呈现时等待RPC响应,并将所有所需的CSS直接写入页面,确保内容的初始呈现和JavaScript完成加载后的干净的水合过程。

服务器端呈现的决定也涉及SEO。一些网络爬虫不会正确地在页面上执行JavaScript,这可能会导致重要内容没有被索引。通过这种方式,服务器端渲染可以让UberEats.com在搜索引擎上变得更加可见。

服务器端呈现的性能影响是复杂的。在服务器端呈现期间,我们通常必须等待所有获取数据的API请求完成,然后才能将响应(web页面)发送给用户。另一方面,在客户端呈现期间,用户必须等待所有JavaScript被下载和解析,然后才能发出任何API请求。如果用户的浏览器需要很长时间来解析JavaScript,或者他们的网络连接不稳定,那么即使花费更多的时间在服务器上等待,服务器端呈现也会带来更好的体验。

在UberEats.com上,我们仔细选择我们想要在服务器端呈现的内容,以及我们想要延迟在客户端加载的内容。作为一般规则,当我们预计请求需要很长时间(超过500毫秒)时,我们会推迟向服务器发出API请求,以便站点加载得更快。在这种情况下,我们需要发送显示加载状态的HTML,等待JavaScript加载,发出API请求,然后呈现结果。

截图来自UberEats.com推荐页面
图6。我们采用了一种混合方法在Restaurants页面上加载数据。

在其他情况下,服务器端和客户端混合呈现方法是合适的。例如,根据用户的位置和偏好加载餐馆列表是一个缓慢的API请求,因此我们不能使用服务器端渲染。但是,我们希望向用户显示一些餐厅,而不必等待JavaScript加载。为了实现这一点,我们有两个API请求,一个快速请求用于获取服务器端呈现中包含的受欢迎的餐厅,另一个较慢的请求用于获取客户端请求的其余餐厅。

我们认为服务器端渲染没有一刀切的方法。我们希望将所有API的响应时间减少到我们认为可以接受的阈值,但与此同时,我们将继续根据具体情况评估服务器端呈现的决定。

实现连续部署

一旦我们重写了UberEats.com,我们很快就发现我们部署构建的方法也需要改造。我们最初在重写之前部署UberEats.com的方法是标记新版本并运行我们的“健全测试”,这是一系列旨在测试关键流程(如下订单)的全面步骤。我们进行了详尽的测试,以将错误构建的风险降至最低。这意味着在移动和桌面浏览器上测试各种不同类型的订单。

如果构建通过了测试,工程师将开始部署,并在部署构建时仔细监视常见错误,例如未捕获的异常。如果工程师注意到一个错误,他们将回滚构建。虽然这通常可以防止严重的错误(如破坏核心功能,如订餐)进入生产,但它是时间密集型的,减慢了开发速度,阻碍了产品工程师致力于新功能。作为重写的一部分,我们决定自动化完整测试和部署过程。

为了使测试更容易,我们还将健全性测试转换为傀儡师集成测试。Puppeteer提供了一个高级API,用于控制无头Chrome浏览器,让我们模拟访问UberEats.com的测试构建并与之交互。完整检查列表的详尽特性使得这个过程非常简单,因为每个步骤(例如访问哪个页面或单击哪个按钮)都详细记录了。对于每个集成测试运行,我们在一个隔离的环境中创建测试送货员、用餐者和餐厅帐户。我们可以将它们用于集成测试,而不会影响其他实际用户,甚至可能同时运行的其他测试。

为了简化部署过程,我们转向了持续部署系统。持续部署,顾名思义,是一个在变更登陆并通过我们的测试套件(在本例中是Puppeteer)后自动部署变更的过程。这种方法的主要好处是,更改可以快速进入生产环境,将工程师从耗时的完整测试、启动和监视部署过程中解放出来。这样做的代价是,团队必须确保我们的测试和警报包含所有可能的故障场景。这就在团队内部灌输了一种严格的监视、测试和警报方法,因为不一定有工程师在现场监视部署。

UberEats.com的部署流程示意图
图7。UberEats.com构建的部署流程从左侧的“代码登陆”开始,并遵循箭头,直到应用程序完全部署或回滚(基于业务指标)。

当变更登陆时,我们的持续部署系统将其部署到一个登台环境,该环境的设计非常类似于生产环境。该系统在核心流程和其他关键站点功能上运行桌面和移动集成测试。如果测试通过了,我们可以合理地相信构建中没有什么可怕的错误。

下一步是金丝雀环境,它包含我们生产主机的一个小子集。我们的持续部署系统在足够长的时间内观察构建在这个环境中的行为,以帮助确保新的构建不会影响我们的应用程序健康状况(例如通过增加未捕获异常的数量)或产品指标(例如,通过减少订购食品的人数)。

一旦在金丝雀环境中构建成功,我们的持续部署系统将在几个小时内逐步将其推广到其他服务器。我们故意将部署错开几个小时,这样我们就有足够的机会发现和解决任何剩余的问题。

如果任何部署步骤失败,持续部署系统将回滚构建,并通知进行更改的工程师。当这种情况发生时,相关工程师会手动暂停部署并恢复更改,因为我们试图避免阻塞其他工程师的构建管道,他们可能试图发布与UberEats.com无关的功能。

虽然它需要在强大的监控和集成测试方面进行前期投资,但持续部署为我们节省了大量的工程时间,这些时间可以重新投资于功能开发和进一步的架构改进。

UberEats.com的重写让我们可以从头开始,并做出架构选择,以最大限度地提高开发人员的生产力和性能。通过自动化我们的构建和发布周期,创建特定于平台的表示组件,生成特定于浏览器的包,以及在数据层中使用更多的本地状态而不是全局状态,我们极大地提高了开发人员快速有效工作的能力。

我们学到了什么

在重写UberEats.com的过程中,我们开发了一些最佳实践,可以应用于构建一个持久的系统,同时也便于开发人员进行工作:

注意抽象概念

从根本上说,代码应该易于理解。如果工程师需要调试一个特性,他们应该能够遵循代码路径。这意味着他们应该知道先决条件数据的来源,以及给定的代码路径如何与系统的其余部分交互。

我们从原地迁移的尝试中得到的一个重要教训是,抽象可能会损害可读性,并且一旦在代码库中根深蒂固就很难撤销。抽象不应该为了抽象而存在。它们应该捕获代码库中重复出现的模式或概念。我们注意到,为了完全理解我们想要抽象的基本概念,我们有时需要研究大量的用例。过早地创建抽象可能会导致它错过重要的用例,如果我们在第一时间延迟引入任何抽象,可能需要更大的重构。我们在创建独立的移动和桌面入口点时应用了这种方法。我们最初准备将两者合并,如果一个优雅的抽象出现,但最终觉得重复的代码更容易推理。

抽象也应该易于调试。如果使用抽象来执行某个功能,例如加载数据或等待事件,则应该清楚该操作是成功还是失败。我们发现,在跟踪可疑的代码路径时,内省也很有用。

通常很难预见抽象将如何影响代码库未来的可读性,因此我们建议对抽象持怀疑态度,直到有强烈的需求,例如重复的非琐碎代码。

避免代码基碎片化

随着时间的推移,随着工程师引入不同的库、开始迁移或试验模式,代码库有变得碎片化的趋势。我们当然觉得这是我们预重写代码库的情况。频繁的贡献者将变得熟悉并知道如何克服这些障碍,但对于新贡献者或不频繁的贡献者来说,这可能是一个巨大的问题。

特性开发应该像复制和修改现有文件一样简单,而不必担心半途而废的迁移模式。在实践中,这限制了我们愿意尝试的新模式或迁移,因为任何迁移计划都需要一个可行的计划来迁移整个代码库和一个现实的时间表。自动化在这里也有帮助,因为它允许我们以编程方式迁移大部分代码库。

自动化所有的事情

我们重写UberEats.com的目标之一是提高开发人员的工作效率。工程时间是稀缺的,应该合理利用。一般来说,可以通过代码模块、静态类型、测试和lint规则自动化的任务应该是自动化的。

代码评审

为了减少花在代码审查上的时间,我们确保代码库中的任何“风格”约定都得到了执行ESLint, JavaScript linter,和更漂亮,一个固执己见的代码格式化器。我们还编写了新的规则来覆盖我们自己的惯例和模式,这些规则太特殊了,无法被现有的lint规则所支持,包括自定义规则,要求颜色和边缘符合Uber风格指南。

迁移

除了代码审查之外,还可以自动进行迁移,从而获得积极的结果。迁移到新模式可能具有挑战性,因为它们可能会在代码库中造成混乱或碎片化。我们试图通过使用棉绒规则来部分缓解这一问题,以确保新模式被持续使用。例如,我们添加了lint规则,以防止在切换到钩子时引入新的高阶组件。

在其他情况下,我们能够完全自动化迁移,因为在代码中通过杠杆来描述对抽象语法树的必要转换相对容易JSCodeshift编写并运行这些转换。这方面的一个例子是切换到可选链接,这是一个实验性的JavaScript特性,用于对可能未定义或为空的引用进行属性访问。在可选链接出现之前,项目通常会使用助手库,例如Lodashidx.我们能够自动地将所有idx调用转换为可选的链式调用,使我们能够立即利用这个新特性。

测试

与产品经理和设计师的沟通可能很耗时,测试用户界面的特定部分也是如此。我们利用了故事书,一个用于在沙箱环境中呈现UI组件的开源工具,以隔离和记录单个功能。在所谓的“故事”中,每个功能都以其所有可能的状态呈现。我们开发了自动化工具来捕获API响应,并将它们保存为故事的固定装置。然后,我们可以将这些故事发送给不同的涉众,他们可以验证它们的功能,而不必手动导航到某个特性,例如跟踪正在进行的订单。

图片来自UberEats.com在Storybook中的风格指南
图8。我们使用Storybook创建了UberEats.com的内部风格指南,这样开发者就可以参考按钮等视觉元素。

自动化测试是检测应用程序退化的关键支柱,无需在部署之前依赖耗时的完整测试。

除了生产效率的好处之外,通过自动化过程迫使我们更深入地思考我们试图完成什么,特别是在lint规则的情况下,我们必须在代码中描述不希望看到的模式或预期的行为。

通过重写UberEats.com,我们了解到,如果我们专注于创建经过良好测试且易于扩展的可读代码库,就可以节省工程师的时间。通过使用自动化来推动工程师朝着正确的方向前进,使这个架构能够自我支持,可以进一步减轻工程师的负担,并在不影响正确性的情况下节省更多的时间。

前进

UberEats.com的重新架构是出于提高性能的愿望,同时使我们更容易迭代新的产品创意。重写不仅满足了我们的性能和生产力目标,而且还超过了它们。例如,bundle大小的减小超过了我们最初的性能目标,为我们提供了可以花在其他地方的时间和资源。

简化的代码库还释放了工程资源,可以专门为用户提供更多的功能。我们最近特别兴奋的一个功能是群体订购,多人可以共享一个购物车。我们希望这能减轻多人点菜的压力。一个稳定、高效的订餐平台只是一个开始,我们会不断尝试新的功能,让订餐过程更方便。

如果您有兴趣为全球用户构建web应用程序或设计Uber平台的其他部分,请访问我们的网站职业页面!

评论