Earlier this year, we introduced React Native for iOS. React Native brings what developers are used to from React on the web — declarative self-contained UI components and fast development cycles — to the mobile platform, while retaining the speed, fidelity, and feel of native applications. Today, we're happy to release React Native for Android.

At Facebook we've been using React Native in production for over a year now. Almost exactly a year ago, our team set out to develop the Ads Manager app. Our goal was to create a new app to let the millions of people who advertise on Facebook manage their accounts and create new ads on the go. It ended up being not only Facebook's first fully React Native app but also the first cross-platform one. In this post, we'd like to share with you how we built this app, how React Native enabled us to move faster, and the lessons we learned.

今年早些时候,我们发布了 React Native for iOS。React Native 将开发者在 web 上所使用的 React — 拥有声明式的自包含组件以及快速的开发周期 — 带到了移动平台, 同时保留了原生应用程序的运行速度、保真度及外观。今天,我们很高兴地发布了 React Native for Android

现在我们已经在 Facebook 的生产环境中使用 React Native 超过一年了。几乎就是一年之前,我们的团队着手开发广告管理应用。当时我们的目标是创建一个新的应用,它能让数以百万计在 Facebook 上做广告的人们能随时随地的管理他们的账户,并创建新的广告。最后的成果不仅仅是 Facebook 第一完全使用 React Native 开发的应用, 而且是首次实现跨平台的一个应用。本文将跟您分享我们是如何构建这个应用的,React Native 是如何是的我们行动更快的,以及我们所积累的教训。

Choosing React Native

Not long ago, React Native was still a new technology that had not been proven in production. While developing a new app based on this technology carried some risk, it was outweighed by the potential upsides.

First, our initial team of three product engineers was already familiar with React. Second, the app needed to contain a lot of complex business logic to accurately handle differences in ad formats, time zones, date formats, currencies, currency conventions, and so on. Much of this was already written in JavaScript. The prospect of writing all that code in Objective-C only to later write it in Java for the Android version of the app wasn't appealing — nor would it be efficient. Third, in React Native it would be easy to implement most of the UI surfaces we wanted to build — displaying lots of data in the form of lists, tables, or graphs. Product engineers could be immediately productive implementing these views, as long as they knew React.

选择 React Native

不久之前,React Native 仍然还是一中没有在生产环境中得到验证的新技术。所以利用该技术开发一个新的应用要承担一些风险,不过这些风险已经被其潜在能带来的好处盖过去了。

首先,我们最初由三个产品工程师组成的团队已经对 React 很熟悉。其次,这个应用需要包含许多复杂的业务逻辑来精细的处理各种不同广告格式、时间轴、日期格式、货币、货币约定等等东西。这些许多都已经用 JavaScript 写好了。全部用 Objective-C 写一遍之后为开发 Android 版本再用 Java 写一遍,这种方案的前景对我们并没有什么吸引力——当然也并不高效。第三,要实现大多数我们想要构建的 UI 界面 — 以列表、表格或者图像的方式展示许多的数据——在 React Native 中做会比较容易。了解了 React,产品工程师应该可以快速高效的实现这些视图。

Of course, some features presented a challenge for this new platform — for example, the image editor, which lets advertisers zoom and crop a photo, and the map view, which lets advertisers target people within a certain radius of a location. Another example is the breadcrumb navigation, which helps advertisers visualize the hierarchy of ads in their accounts. These provided opportunities for us to push the platform further.

Building Ads Manager for iOS first

Our team decided to develop an iOS version of the app first, which aligned very well with React Native also being developed first for iOS. We grew the team from three to eight engineers over the following months. The new recruits weren't familiar with React — and some of them weren't familiar with JavaScript — but they were eager to build a great mobile experience for our advertisers, and they ramped up quickly.

当然,有些特性对这个新平台而言却是是一种挑战 — 例如图像编辑器,它可以用来给广告主缩放和裁剪一张照片,还有地图视图,它可以用来给广告主在以一个位置为中心的特定半径内定位人员。另外一个例子就是面包屑导航,它能帮助广告主看清楚他们账户中的广告层级。这些都向我们提供了将这个平台推向更远的机会。

首先为 iOS 构建广告管理器

我们的团队决定首先实现这个应用的一个 iOS 版本,这样的安排还不多,React Native 一开始也是针对 iOS 而开发的。接下来的几个月我们的团队成员从 3 名工程师发展到了 8 名。新聘人员对 React 不熟悉 — 而他们其中一些人对 JavaScript 也不熟悉 — 但他们都渴望为我们的广告主构建一种优秀的移动体验,所以他们成长得很快。

Experienced iOS engineers on the React Native team helped us bridge features that weren't yet available in React Native, such as providing access to the phone's camera roll. They also helped us bundle the app with some of Facebook's existing iOS libraries that were already being used in other Facebook apps to perform authentication, analytics, crash reporting, networking, and push notifications. That let our team focus on building just the product.

As mentioned above, we were able to reuse a lot of our pre-existing JavaScript libraries. One such library is Relay, Facebook's framework for delivering data to React applications via GraphQL. Another set of libraries dealt with internationalization and localization, which can be tricky when time zones and currencies are involved. Normally these libraries load the right configuration from a JSON endpoint on the website. We wrote scripts to export the JSON files for all supported locales, included the files with the app using iOS's localized bundles, and then exposed the JSON data to JavaScript with a few lines of native code. This allowed our libraries to work nearly unchanged.

React Native 团队中有经验的 iOS 工程师帮助我们在 React Native 中桥接了一些终端设备上暂时还用不着的功能特性,比如提供对手机摄像头的访问。他们还帮助我们将已经被应用在其它 Facebook 应用中的,执行用户认证、分析、奔溃报告、网络以及推送通知这些操作的一些 iOS 库,捆绑到了应用。这样就让我们的团队只用去关注构建这个产品。

如上所述,我们能够重用许多之前已经有了的 JavaScript 库。一个这样的库就是 Relay,Facebook 的一个通过 GraphQL 将数据传递到 React 的框架。另外还有一些处理国际化和本地化(这些逻辑在涉及到时间域和货币时会变得有些棘手)的库。通常这些库会从网站的一个 JSON 端点加载正确的配置。我们已经编写了脚本将所有支持的语言导出为 JSON 文件,使用 iOS 的本地化包来引入这些文件,然后拥几行 native 代码就能将 JSON 数据暴露给 JavaScript。这让我们的库几乎不做什么修改就能拿来用。

One of the bigger challenges we faced was the navigation flows. For navigating an advertiser's existing ads and campaigns, we wanted a breadcrumb navigation bar. For the ad creation flow, we needed a wizard-style navigation bar. On top of that, it was also crucial to get the transition animations and touch gestures right, otherwise the app would have felt more like a glorified mobile website than a native app.

Our solution was the Navigator component, which was made available along with React Native under the CustomComponents directory. In essence, it's a React component that keeps track of a set of other React components in a stack. It can display one of these components and animate between them based on button presses or touch gestures. It also has a pluggable navigation bar component, which let us implement an iOS-like navigation bar for most regular views, breadcrumbs for navigating ads and campaigns, and a wizard-like stepper for the creation flow. The navigation bar component is notified of animation progress and can perform the necessary animation increment to match. This means all animations, both for the views and for the navigation bars, are computed in JavaScript, but tests showed that we were still able to perform them at 60 fps.

YouTube 视频地址:https://youtu.be/a6yJ7M8FoEo 


我们的方案就是 Navigator 组件, React Native 的 CustomComponents 目录下附带就有这个东西能拿来用。实质上,它就是一个由其他的 React 组件堆积起来的 React 组件集合. 它可以显示这些组件中的一个组件,并且这些组件会在按下按钮或者发生触摸时动画轮换。它还有一个可插入式的导航条组件, 让我们可以实现一个 iOS 风格的,用于大多数一般视图的导航条,用于导航广告和宣传活动的面包屑导航,以及一个用于创建流程的向导式导航。导航条组件的动画进度条可以接收通知,并且匹配性的显示进度增加的动画。这意味着所有的动画,针对视图的以及针对导航条的,都是用 JavaScript 来进行计算的, 而测试显示我们仍然可以以 60 fps 的帧率执行它们。

YouTube 视频地址:https://youtu.be/a6yJ7M8FoEo 

There's only one way that navigation animations could stutter, and that's when the JavaScript thread was blocked during a big operation. When we encountered this scenario, it was almost exclusively due to processing large amounts of newly fetched data. Of course, it makes sense that when you navigate to a new view, more data has to be loaded and processed. On a sufficiently fast network, that process could easily interfere with a navigation animation still in progress. Our solution here was to explicitly delay the data processing until animations were complete, using the InteractionManager component, which also ships as part of React Native. We would first animate to a view that contained placeholders and then let Relay do the data processing, which automatically caused the necessary React components to re-render.

Shipping an Android version

When Ads Manager for iOS was close to shipping, we started looking at building an Android version of the same app. A React Native port to Android seemed like the best way to make that work. Fortunately, the React Native team was already hard at work creating just that. Naturally, we wanted to reuse as much app code as possible. Not just the business logic but also the UI code, because most of the views were largely the same, save for some styling. Of course, there were places where the Android version needed to look and feel different from the iOS version, for instance, in terms of navigation or using native UI elements for date pickers, switches, etc.

YouTube 视频地址:https://youtu.be/MNNR01NF290 

只有一种情况下导航动画才会有问题,那就是当 Javascript 线程在一个长操作过程中被阻塞时。这种情况基本都是由处理大量新获取的数据引起的。当然当你切换到新的页面,读取和处理新的数据是不可避免的。在一个网速快的环境下, 这个问题可以用正在加载的导航动画来解决。在这里我们采取另一种方案:在动画完成前,是用 InteractionManager 组件显式延迟数据处理(这个是 Reactive Native 内置的组件)。首先我们先切换到包含模板的页面,然后使用 Relay 来做数据处理,接着他会自动调用必要的 React 组件来重新渲染界面。

部署到 Android

既然 iOS 的广告管理器快要部署完成了,我们现在给这个 app 部署一个 Android 版本。使用 Reactive Native 的 Android 移植版应该是最好的选择。幸运的是,Reactive Native 团队已经将 Android 移植版做的够好了。通常我们想尽可能重用代码,不仅是业务逻辑也有 UI,因为大多数页面是基本相同的,为调整样式节省了时间。当然也有一些地方 Android 版的外观和 iOS 版是需要不同的,比如导航,日期选择器和开关按钮等等。

YouTube 视频地址:https://youtu.be/MNNR01NF290

Fortunately, the React Native packager's blacklist feature and React's abstraction mechanism helped us a lot with maximizing code reuse across the two platforms and minimizing the need for explicit platform checks. On iOS, we told the packager to ignore all files ending in .android.js. For Android development, it ignored all files ending in .ios.js. Now we could implement the same component once for Android and once for iOS, while the consuming code would be oblivious to the platform. So instead of introducing explicit if/else checks for the platform, we tried to refactor platform-specific parts of the UI into separate components that would have an Android and iOS implementation. At the time of shipping Ads Manager for Android, that approach yielded around 85 percent reuse of app code.

A bigger challenge that we faced was how to manage the source code. Android and iOS codebases were managed in two different repositories at Facebook. The source code for Ads Manager for iOS lived in the iOS repository, of course, while the code for the Android version would have to live in the Android repository for various reasons. For example, much like with the iOS version, we wanted to make use of a few of Facebook's Android libraries, which lived in the Android repository. In addition, all the build tools, automation, and continuous integration for Android apps were hooked up to the Android repository. Given that the Android port of the app required refactoring existing iOS code to abstract platform-specific components into their own files, we would've essentially been constantly forking and merging two versions of the same codebase. That seemed like an unacceptable situation to us.

幸运的是,React Native 打包器的黑名单功能和 React 的抽象机制帮助我们尽可能的在两个平台之间复用代码,尽可能减少对平台的检查。对于 iOS,可以告诉打包器忽略以 .android.js 结尾的文件。而开发Android 的时候,则是忽略 .ios.js 结尾的文件。这样,我们就可以对同一个组件用 Android 和 iOS 分别实现一次,而使用组件的代码可以是平台无关的。我们不是显示的用 if/else 的方式来检测平台,而是重构平台相关的UI部分,分割成不同的需要 Android 和 iOS 分别实现的组件。在发布 Android 版广告管理器的时候,代码的复用率达到了大约85%。

我们面临的一个大的挑战是如何管理代码。在 Facebook,Android 和 iOS 的代码在不同的代码仓库里。广告管理器的 iOS 版本的代码在 iOS 代码库里,而由于各种原因,Android 版本的代码只能在 Android 代码库里。就像 iOS 版那样,我们需要使用一些 Android 代码库里的 Facebook 的 Android 库。另外,所有的构建、自动化和持续集成工具都绑定了 Android 代码库。如果把已有的 iOS 版代码重构,抽象出平台无关的组件用于 Android 版的移植,则意味着我们要经常 fork 和 merge 相同的代码的两个不同版本。对我们来说,这种情形不能接受。

In the end, we decided to designate the iOS repository as the source of truth, mostly because it was already there and the iOS version of the app was the most mature. We set up a cronjob that synced all JavaScript code from the iOS to the Android repository many times a day. Committing JavaScript to the Android repository was discouraged and was permitted only if it was followed up with an accompanying commit to the iOS repository. If the sync script detected a discrepancy, it filed a task for further investigation.

We also made it possible for the JavaScript packager server to run Android code from the iOS repository. That way, our product developers, who touched mostly JavaScript and no native code, could develop and test their changes on both iOS and Android directly from the iOS repository. But that still required them to have built the native parts of the Android app from the Android repository, and the same for the iOS app — a huge tax when testing changes on two platforms. To speed up the flow for JavaScript-only developers, we also built a script that downloaded the appropriate native binary from our continuous integration servers. This made it unnecessary to even keep a clone of the Android repository for most developers — they could do all their JavaScript development from the source of truth in the iOS repository and iterate as fast as or faster than on Facebook's web stack.

最后我们决定把 iOS 代码库当做可信的代码源,主要是因为代码已经存在并且 iOS 版的 App 也已经成熟了。我们通过 cron 的任务每天把所有 JavaScript 代码从 iOS 同步几次到 Android 代码库。直接提交 JavaScript 代码到 Android 仓库是不被允许的,除非同时提交到 iOS 仓库。如果同步脚本检测到有不一致的地方,会生成一个任务,后续去进一步检查。

我们同时还做到了让 JavaScript 打包服务器从 iOS 仓库运行 Android 代码。这样的话,那些主要开发 JavaScript,不涉及 native 代码的开发人员就可以基于 iOS 代码库开发和测试自己的代码改动。不过在测试在两个平台上的改动时,还是需要从 Android 代码库构建 Android 应用,从 iOS 代码库构建 iOS 应用,这是一件很麻烦的事情。为了优化 JavaScript 程序员的开发流程,我们开发了一些脚本,用来从持续集成服务器上下载合适的 native 二进制代码文件。这使得大多数的开发者不需要保留 Android 代码库的副本,他们可以直接在可信的 iOS 源代码上开发,这样就可以像在 Facebook 的 web stack 上一样快速迭代。

What we learned

The React Native team developed the platform alongside our app, and exposed the native components and APIs that we needed to make it happen. Those components will benefit everyone building an app in the future. Even if we'd had to build out a few components ourselves, using React Native over pure native still would've been worth it. We would've had to write those components anyway, and they probably wouldn't have been reusable by other teams down the road.

One lesson we learned was that working across separate iOS and Android code repositories is difficult, even with lots of tools and automation. When we were building the app, Facebook used this model, and all of our build automation and developer processes were set up around it. However, it doesn't work well for a product that, for the most part, has a single shared JavaScript codebase. Fortunately, Facebook is moving to a unified repository for both platforms — only one copy of common JavaScript code will be necessary, and syncs will be a thing of the past.

React Native 团队在这个 App 开发的过程中开发出了 React Native 平台,提供我们需要的组件和接口,使得我们的 App 成为可能。将来这些组件也会给所有的 App 开发者带来便利。即使我们不得不自己实现一些组件,在纯 native 之上使用 React Native技术 也是值得一试的。这些组件是我们不得不实现的,并且可能将来也不会被其他团队重用。

对于我们的教训是,即使有大量的工具和自动化脚本,要跨两个分离的 iOS 和 Android 代码库进行工作是很困难的。在我们开发这个 App 的时候,Facebook 使用的是这种模式,所有的构建自动化工具和开发流程都做了相应的配置。不过,如果一个产品有一个共享的 JavaScript 代码库,这种模式并不合适。幸运的是,Facebook 正在往一个统一的代码库迁移 ,所有的平台合在一起,只需要一份公共的 JavaScript 代码拷贝,代码同步也不再需要了。

Another lesson we learned concerned testing. When making changes, every engineer must be careful to test on both platforms, and the process is prone to human error. But that's just an inevitable consequence of developing a cross-platform app from the same codebase. That said, the cost of an occasional mishap due to insufficient testing is far outweighed by the development efficiency gained by using React Native and being able to reuse code across both platforms in the first place. Keep in mind, this lesson does not apply only to product engineers; it also applies to the React Native platform engineers working in Objective-C and Java. Much of the work these engineers do is not purely limited to the respective native languages. It can also affect JavaScript — for example, component APIs or partially shared implementations. Native iOS engineers are typically not used to having to test changes on Android, and the reverse is true of Android engineers. This is mainly a cultural gap that took time and effort to close, and as a result, over time, our stability has increased.

我们遇到的另一个问题跟测试有关。有改动时,所有的工程师都需要小心的在所有的平台上测一遍,整个流程很容易产生人为的错误。然而,使用同一个代码库开发跨平台的应用,这种问题是必然存在的。即便如此,React Native 带来的开发效率以及一开始就在跨平台开发中重用代码带来的优势,远远超出测试不充分导致的偶发问题造成的代价。要知道的是,这个问题不光是产品开发工程师会遇到,React Native 平台开发工程师在开发 Objective-C 和 Java 的时候也会遇到。这些工程师大部分时间都不会只面对一门编程语言。同样,这个问题也会影响JavaScript,例如,开发组件 API 或者部分共享(partially shared)的实现。通常,纯 iOS 开发工程师是不需要测试 Android 上的改动的,对 Android 工程师也是如此。这主要还是一种文化上的差异,需要时间和努力去消除,在这个过程中我们产品的稳定性也在增强。