React Native 在 Airbnb(译文)

956 阅读13分钟

在Android,iOS,Web和跨平台框架的横向对比中,React Native本身是一个相对较新且快速开发移动的平台。两年后,我们可以肯定地说React Native在很多方面都是革命性的。这是移动设备的范例转变,我们能够从中受益很多。然而也有明显的痛点,它的优点不仅仅是这些

优点

跨平台 React Native 的主要好处是,您编写的代码可以在Android和iOS上本机运行。使用React Native的大多数功能都能够实现95-100%的共享代码,0.2%的文件是特定于平台的(android.js / ios.js)。

统一设计语言系统(DLS)
我们开发了一种名为DLS的跨平台设计语言。我们有Android,iOS,React Native和每个组件的Web版本。拥有统一的设计语言可以编写跨平台功能,因为它意味着设计,组件名称和屏幕在不同平台上是一致的。但是,我们仍然可以在适用的情况下做出适合平台的决策。例如,我们使用Android 上的原生工具栏和iOS 上的UINavigationBar,我们选择隐藏Android上的披露指标,因为它们不符合Android平台设计指南。

我们选择重写组件而不是包装本机组件,因为为每个平台单独制作适合平台的API更加可靠,并减少了可能不知道如何正确测试React Native中的更改的Android和iOS工程师的维护开销。但是,它确实会导致同一组件的本机版本和React Native版本不同步的平台之间出现碎片。

react
React是最受欢迎的 Web框架。它简单而强大,可以很好地扩展到大型代码库。我们特别喜欢它的特点:

  • 组件: React组件通过明确定义的道具和状态强制分离关注点。这是React可扩展性的主要贡献者。
  • 简化的生命周期: Android以及在较小程度上,iOS生命周期非常复杂。功能性反应性React组件从根本上解决了这个问题,使学习React Native比学习Android或iOS简单得多。
  • 声明:react有助于保持我们的UI同步与底层状态的声明性质。

迭代速度
在React Native中进行开发时,我们能够在一两秒内可靠地使用热重新加载来测试Android和iOS上的更改。尽管构建性能是我们原生应用程序的首要任务,但它从未接近我们使用React Native实现的迭代速度。充其量,本机编译时间为15秒,但完整版本可能高达20分钟。

基础环境的搭建成本
我们开发与本机基础架构是广泛集成。所有核心部分(如网络,国际化,测试,共享组件转换,设备信息,帐户信息等)都包含在一个React Native API中。这些桥接是一些比较复杂的桥接,因为我们希望将现有的Android和iOS API包装成React的一致和规范。虽然通过快速迭代和新基础设施的开发使这些桥接保持最新状态是一个不断追赶的游戏,但基础设施团队的投资使产品开发工作变得更加容易。

如果不对基础环境进行大量投入,ReactNative将导致降低开发人员和用户体验。因此,我们不相信React Native可以简单地添加到现有应用程序而无需大量持续投入。

性能
React Native最大的担忧之一就是它的性能。但是,在实践中,出现的次数相对来说还是比较少的。我们的大多数React Native屏幕都像我们的原生屏幕一样流畅。性能通常被认为是单一维度。我们经常看到移动工程师看JS并认为“比Java慢”。但是,在许多情况下,从主线程移动业务逻辑和布局实际上改善了渲染性能。

当我们确实看到性能问题,他们通常由过多的渲染引起的有效利用是缓解shouldComponentUpdate,removeClippedSubviews,并更好地利用终极版。

但是,初始化和首次渲染时间(如下所述)使得React Native在启动屏幕,深层链接方面表现不佳,并且在屏幕之间导航时增加了TTI时间。此外,丢帧的屏幕很难调试,因为Yoga在React Native组件和本机视图之间进行转换。

Redux
我们使用Redux进行状态管理,我们发现它有效并阻止UI与状态不同步,并在屏幕上轻松实现数据共享。然而,学习曲线相对困难。我们为一些常见模板提供了生成器,但在使用React Native时,它仍然是最具挑战性的部分之一和混淆源。值得注意的是,这些挑战并非特定于React Native。

由Native支持
因为React Native中的所有内容都可以通过本机代码进行桥接,所以我们最终能够构建许多我们在开始时不确定的内容,例如:

  • 共享元素转换:我们构建了一个组件,该组件由Android和iOS上的本机共享元素代码支持。这甚至可以在Native和React Native屏幕之间使用。
  • Lottie:通过在Android和iOS上包装现有库,我们能够让Lottie在React Native中工作。
  • 本机网络堆栈: React Native在两个平台上使用我们现有的本机网络堆栈和缓存。
  • 其他核心基础知识:就像网络一样,我们包装了其余的现有本地基础设施,如i18n,实验等,以便它在React Native中无缝运行。

动画
感谢React Native Animated库,我们能够实现无抖动的动画甚至是交互驱动的动画,例如滚动视差。

JS / React开源 因为React Native真正运行React和javascript,所以我们能够利用极大的javascript项目,如redux,reselect,jest等。

Flexbox的
React Native使用Yoga处理布局,这是一个跨平台的C库,可通过flexbox API 处理布局计算。在早期,我们受到瑜伽限制的影响,例如缺乏宽高比,但它们已在后续更新中添加。此外,有趣的教程,如flexbox froggy使入门更加好玩。

与Web协作
在React Native探索的后期,我们立即开始为web,iOS和Android构建。鉴于Web也使用Redux,我们发现大量代码可以在Web和本机平台之间共享而无需更改。

缺点

React Native
React Native不如Android或iOS成熟。它更新,更雄心勃勃,移动速度极快。虽然React Native在大多数情况下都能很好地运行,但有些情况下,它的不成熟表现出来并且在本地调试某一些bug很难解决。不幸的是,这些实例很难预测,可能需要几个小时到几天才能解决。

维护React Native的分支
由于React Native的不成熟,我们有时需要修补React Native源。除了回馈React Native之外,我们还必须维护一个fork,我们可以快速合并更改并突破我们的版本。在这两年中,我们不得不在React Native之上添加大约50个提交。这使得升级React Native的过程非常痛苦。

JavaScript工具
JavaScript是一种无类型语言。缺乏类型安全性难以扩展,也成为移动工程师争论的焦点,因为他们习惯于输入可能对学习React Native感兴趣的语言。我们探索了采用流程,但隐秘的错误消息导致令人沮丧的开发人员体验。我们还研究了TypeScript,但是将它集成到我们现有的基础设施中,例如babel和metro bundler,这被证明是有问题的。但是,我们正在继续积极研究网络上的TypeScript。

重构
JavaScript无法解决的副作用是重构非常困难并且容易出错。重命名道具,尤其是具有通用名称的道具,如onClick或通过多个组件传递的道具,这些都是精确重构的噩梦。更糟糕的是,重构在生产中而不是在编译时中断,并且很难添加适当的静态分析。

JavaScriptCore不一致
React Native的一个微妙而棘手的方面是由于它是在JavaScriptCore环境中执行的。以下是我们遇到的后果:

  • iOS提供了自己的JavaScriptCore开箱即用。这意味着iOS大部分都是一致的,对我们来说没有问题。
  • Android不提供自己的JavaScriptCore,因此React Native捆绑了它自己的。但是,默认情况下你得到的之前的。结果,我们不得不竭尽全力捆绑一个最新的
  • 在调试时,React Native会附加到Chrome Developer Tools实例。这很棒,因为它是一个功能强大的调试器。但是,一旦附加调试器,所有JavaScript都在Chrome的V8引擎中运行。99.9%的时间都很好。但是,在一个实例中,我们得到了什么时候toLocaleString在iOS上工作,但只在调试时才适用于Android。事实证明,Android JSC 不包含它,它默默地失败,除非你在调试,在这种情况下,它使用的是V8。在不知道这样的技术细节的情况下,它可能会导致产品工程师经历数天的痛苦调试。

React Native开源库
学习平台既困难又耗时。大多数人只知道一两个平台。具有本地桥(例如地图,视频等)的React Native库需要所有三个平台的相同知识才能成功。我们发现大多数React Native Open源项目都是由只有一两个经验的人编写的。这导致Android或iOS上出现不一致或意外错误。

在Android上,许多React Native库还要求您使用node_modules的相对路径,而不是发布与社区所期望的不一致的maven工件。

并行基础设施和功能工作
我们在Android和iOS上积累了多年的原生基础设施。但是,在React Native中,我们从一个空白的平板开始,不得不编写或创建所有现有基础架构的桥梁。这意味着有时候产品工程师需要一些尚不存在的功能。在那时,他们要么必须在他们不熟悉的平台上工作,要么在他们的项目范围之外进行构建,要么被阻止直到可以创建它。

APP崩溃监控
我们使用Bugsnag在Android和iOS上进行崩溃报告。虽然我们能够让Bugsnag在两个平台上都能正常工作,但它的可靠性较低,需要的工作量比其他平台要多。因为React Native在业界相对较新且很少见,所以我们必须构建大量的基础设施,例如在内部上传源映射,并且必须与Bugsnag一起工作,以便能够执行过滤崩溃等事情。反应原生。

由于React Native周围的自定义基础架构数量很多,我们偶尔会遇到严重的问题,即未报告崩溃或源地图未正确上传。

最后,如果问题跨越React Native和本机代码,调试React Native崩溃通常更具挑战性,因为堆栈跟踪不会在React Native和native之间跳转。

桥接
React Native有一个桥接API,用于在本机和React Native之间进行通信。虽然它按预期工作,但编写起来非常麻烦。首先,它需要正确设置所有三个开发环境。我们还遇到了很多问题,其中来自JavaScript的类型是出乎意料的。例如,整数通常用字符串包裹,这个问题直到通过桥接才会实现。更糟糕的是,有时iOS会在Android崩溃时无声地失败。我们开始研究从2017年底开始自动生成TypeScript定义的桥接代码,但实在太晚了。

初始化时间
在React Native第一次呈现之前,必须初始化其运行时。不幸的是,即使在高端设备上,这对于我们的应用程序来说也需要几秒钟。这使得使用React Native用于启动屏幕几乎是不可能的。我们通过在app-launch处初始化它来最小化React Native的首次渲染时间。

初始渲染时间
与原生屏幕不同,渲染React Native需要至少一个完整的主线程 - > js - >瑜伽布局线程 - >主线程往返才有足够的信息来首次渲染屏幕。我们在iOS上看到平均初始p90渲染时间为280毫秒,而在Android上则为440毫秒。在Android上,我们使用了postponeEnterTransition API,它通常用于共享元素转换,以延迟显示屏幕,直到它呈现为止。在iOS上,我们遇到了从React Native快速设置导航栏配置的问题。因此,我们在所有React Native屏幕转换中添加了50ms的人为延迟,以防止导航栏在加载配置后闪烁。

应用大小
React Native对应用程序大小的影响也不容忽视。在Android上,React Native(Java + JS +本地库,如Yoga + Javascript Runtime)的总大小为每个ABI 8mb。在一个APK中使用x86和arm(仅32位),它将更接近12mb。

64位
由于问题,我们仍然无法在Android上发送64位APK 。

手势 我们避免将React Native用于涉及复杂手势的屏幕,因为Android和iOS的触摸子系统不同,因此提出统一的API对整个React Native社区来说都是一个挑战。但是,工作正在继续进行,而react-native-gesture-handler只是达到1.0。

很长的list React Native在这个领域取得了一些进展,包括像FlatList这样的库。然而,它们远不及Android 上的RecyclerView或iOS 上的UICollectionView的成熟度和灵活性。由于线程化,许多限制很难克服。无法同步访问适配器数据,因此可以在快速滚动时异步呈现视图时查看视图。文本也无法同步测量,因此iOS无法使用预先计算的单元格高度进行某些优化。

React Native的升级 虽然大多数React Native升级都是微不足道的,但也有一些令人痛苦。特别是,几乎不可能使用React Native 0.43(2017年4月)到0。49(2017年10月),因为它使用了React 16 alpha和beta。这是一个非常大的问题,因为大多数专为Web使用而设计的React库不支持预发布的React版本。在此次升级中纠缠正确的依赖关系的过程对2017年中期其他React Native基础架构的工作造成了重大损害。

麻烦的崩溃
我们不得不处理一些难以解决的非常奇怪的崩溃事件。例如,我们目前正在经历@ReactProp注释的崩溃,并且无法在任何设备上重现它,即使那些具有相同硬件和软件的设备也会在野外崩溃。

Android上的进程中保存的实例状态 Android经常清理后台进程,但让他们有机会同步保存自己的状态。但是,在React Native上,只能在js线程中访问所有状态,因此无法同步完成。即使不是这种情况,redux作为状态存储也不兼容这种方法,因为它包含可序列化和非可序列化数据的混合,并且可能包含的数据超过了savedInstanceState包中可能导致崩溃的数据。生产。