非死book 是如何构建首个跨平台 React Native 应用的?

jopen 8年前

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

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

选择 React Native

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

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

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

非死book 是如何构建首个跨平台 React Native 应用的?

非死book 是如何构建首个跨平台 React Native 应用的?

首先为 iOS 构建广告管理器

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

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

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

我们所面对过的比较大一个挑战之一就是导航流。为了对广告主现有的广告和宣传活动进行导航,我们想要有一个导航条。针对广告的创作流程,我们需要一 个向导式的导航条。最重要的是,让应用拥有正确的渐变动画以触摸手势也很关键,否则应用感觉上会更像是一个漂亮的移动网站,而不是一个原生应用。

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

油Tube 视频地址:https://youtu.be/a6yJ7M8FoEo 

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

部署到 Android

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

油Tube 视频地址:https://youtu.be/MNNR01NF290

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

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

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

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

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

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


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

为了解决这个问题,我们还开发了每次构建都会执行到的集成测试。然而已有的持续集成系统只能在 iOS 平台和 Android 平台分别发现 iOS 和 Android 问题,不能做到在 iOS 的构建上运行 Android 测试,反过来也不行。这需要工程师去解决,不过还是有很多问题,可能会导致app出错。

当这些都搞定之后,我们可以在两个平台上一起发布 非死book 第一个完全构建在 React Native 上的 app,有着 native 的外观和体验,由同一个 JavaScript 工程师团队完成。当这些工程师加入团队的时候,不是每一个人都熟悉 React Native,然而他们在仅仅5个月的时间就开发出了有着 native 外观和体验的 iOS 版本的 app。在3个月之后,他们发布 app 的 Android 版本。