当前位置: 首页 > Web前端 > HTML5

Airbnb上的ReactNative(译)

时间:2023-04-05 11:09:21 HTML5

在Android、iOS、Web和跨平台框??架的横向比较中,ReactNative本身就是一个相对较新且发展迅速的移动平台。两年后,我们可以有把握地说ReactNative在很多方面都是革命性的。这是移动设备的范式转变,我们可以从中获益良多。然而,有明显的痛点,而不仅仅是那些优势。跨平台ReactNative的主要好处是您编写的代码可以在Android和iOS上本地运行。使用ReactNative的大部分功能可以实现95-100%的共享代码,0.2%的文件是平台特定的(android.js/ios.js)。统一设计语言系统(DLS)我们开发了一种称为DLS的跨平台设计语言。我们有每个组件的Android、iOS、ReactNative和Web版本。拥有统一的设计语言使得编写跨平台功能成为可能,因为这意味着设计、组件名称和屏幕在跨平台时保持一致。但是,我们仍然可以在适用的情况下做出适合平台的决定。例如,我们在Android上使用原生工具栏,在iOS上使用UINavigationBar,我们选择隐藏Android上的披露指示器,因为它们不符合Android平台设计指南。我们选择重写组件而不是包装原生组件,因为为每个平台单独制作适合平台的API更可靠,并减少可能不知道如何正确测试ReactNative中的更改的Android和iOS工程师的维护开销。但是,它确实会导致同一组件的本机版本和ReactNative版本不同步的平台之间的碎片化。ReactReact是最流行的Web框架。它简单而强大,并且可以很好地扩展到大型代码库。我们特别喜欢它的特性:组件:React组件通过定义明确的props和状态强制分离关注点。这是React可扩展性的主要贡献者。简化的生命周期:Android和较小程度上的iOS生命周期非常复杂。FunctionalReactComponents从根本上解决了这个问题,让学习ReactNative比学习Android或iOS简单得多。声明式:React有助于使我们的UI与底层状态的声明性特性保持同步。迭代速度在ReactNative中开发时,我们能够在一两秒内可靠地使用热重载来测试Android和iOS上的更改。尽管构建性能是我们本机应用程序的首要任务,但它永远无法接近我们使用ReactNative实现的迭代速度。本机编译时间最多为15秒,但完整构建时间可能高达20分钟。搭建基础环境的成本我们的开发是广泛集成原生基础设施的。所有核心部分,如网络、国际化、测试、共享组件转换、设备信息、帐户信息等,都包含在一个ReactNativeAPI中。这些桥是一些比较复杂的桥,因为我们想把现有的Android和iOSAPI包装成React的一致性和规范。虽然通过快速迭代和开发新基础设施来保持这些桥梁是最新的是一场不断的追赶游戏,但基础设施团队的投资使产品开发工作变得更加容易。如果不对底层环境进行大量投资,ReactNative将导致开发人员和用户体验下降。因此,我们不认为ReactNative可以简单地添加到现有应用程序中而无需大量持续投资。性能ReactNative最大的问题之一是它的性能。然而,在实践中,出现的次数相对较少。我们的大多数ReactNative屏幕都和我们的原生屏幕一样流畅。性能通常被认为是一个单一的维度。我们经常看到移动工程师看着JS,认为“比Java还慢”。然而,在很多情况下,将业务逻辑和布局从主线程中移出实际上可以提高渲染性能。当我们确实看到性能问题时,它们通常是由过度渲染引起的。有效的使用是缓解shouldComponentUpdate、removeClippedSubviews,更好的使用redux。然而,初始化和首次渲染时间(如下所述)使ReactNative在启动画面、深度链接方面表现不佳,并增加了在屏幕之间导航时的TTI时间。此外,丢帧的屏幕很难调试,因为Yoga在ReactNative组件和本机视图之间转换。Redux我们使用Redux进行状态管理,我们发现它可以工作并阻止UI与状态不同步,并且可以轻松地跨屏幕共享数据。但是,学习曲线相对困难。我们有一些通用模板的生成器,但它仍然是最具挑战性的部分之一,也是使用ReactNative时的混淆来源。值得注意的是,这些挑战并非特定于ReactNative。由Native支持因为ReactNative中的所有内容都可以通过本地代码桥接,所以我们最终能够构建很多我们在开始时不确定的东西,例如:共享元素转换:我们构建了一个组件组件由Android和iOS上的原生共享元素代码支持。这甚至适用于Native和ReactNative屏幕。Lottie:通过包装Android和iOS上的现有库,我们能够让Lottie在ReactNative中工作。本机网络堆栈:ReactNative在两个平台上使用我们现有的本机网络堆栈和缓存。其他核心基础知识:就像网络一样,我们包装了现有原生基础设施的其余部分,如国际化、实验等,以便它在ReactNative中无缝工作。动画感谢ReactNativeAnimated库,我们能够实现无抖动的动画,甚至是交互驱动的动画,例如滚动视差。JS/React开源因为ReactNative真正运行React和javascript,所以我们能够利用伟大的javascript项目,如redux、reselect、jest等。ReactNativeforFlexbox使用Yoga处理布局,Yoga是一个跨平台的C库,处理通过flexboxAPI进行布局计算。早期,我们遇到了Yoga的局限性,例如缺乏纵横比,但在后续更新中添加了这些局限性。此外,像flexboxfroggy这样有趣的教程让入门变得更加有趣。在ReactNative探索的后期与Web协作,我们立即开始为Web、iOS和Android构建。鉴于web也使用Redux,我们发现很多代码可以在web和原生平台之间共享而无需更改。缺点ReactNativeReactNative不如Android或iOS成熟。它更新、更雄心勃勃,而且发展速度极快。虽然ReactNative在大多数情况下都运行良好,但在某些情况下它的不成熟表现出来并且某些错误难以在本地调试。不幸的是,这些情况很难预测,可能需要数小时到数天才能解决。维护ReactNative的一个分支由于ReactNative的不成熟,我们有时需要对ReactNative源码进行修补。除了回馈ReactNative之外,我们还必须维护一个分支,我们可以在其中快速合并更改并突破我们的版本。在这两年中,我们不得不在ReactNative之上添加大约50次提交。这使得升级ReactNative的过程非常痛苦。JavaScript工具JavaScript是一种无类型语言。缺乏类型安全性导致难以扩展也成为移动工程师争论的焦点,他们习惯于使用一种可能对学习ReactNative感兴趣的语言来输入。我们探索了采用过程,但神秘的错误消息导致了令人沮丧的开发体验。我们还研究了TypeScript,但将其集成到我们现有的基础设施(例如babel和metrobundler)中被证明是有问题的。但是,我们将继续在网络上积极研究TypeScript。重构JavaScript无法解决的一个副作用是重构非常困难且容易出错。重命名道具,尤其是具有通用名称的道具,如onClick或通过多个组件传递的道具,是精确重构的噩梦。更糟糕的是,重构会中断生产而不是编译时间,并且很难添加适当的静态分析。JavaScriptCore不一致ReactNative的一个微妙和棘手的方面是由于它在JavaScriptCore环境中执行。这是我们遇到的后果:iOS提供了自己开箱即用的JavaScriptCore。这意味着iOS基本上是一致的,对我们来说没问题。Android不提供自己的JavaScriptCore,因此ReactNative捆绑了自己的。但是,默认情况下您会得到前一个。因此,我们不得不在调试时不遗余力地捆绑Chrome开发者工具的最新ReactNative实例。这很棒,因为它是一个功能强大的调试器。然而,一旦附加了调试器,所有JavaScript都会在Chrome的V8引擎中运行。99.9%的时间都很好。但是,在一个实例中,我们得到了toLocaleString在iOS上工作的时间,但仅在调试时在Android上工作。事实证明,AndroidJSC不包含它,除非您正在调试,否则它会静默失败,在这种情况下,它使用的是V8。如果不了解这些技术细节,产品工程师可能会经历数天的痛苦调试。ReactNative开源库学习平台难度大,耗时长。大多数人只知道一两个平台。ReactNative库与原生桥(例如地图、视频等)需要对所有三个平台有相同的了解才能成功。我们发现大多数ReactNative开源项目都是由只有一两次经验的人编写的。这会导致Android或iOS上出现不一致或意外错误。在Android上,许多ReactNative库还要求您使用node_modules的相对路径,而不是发布与社区期望不一致的maven工件。并行基础架构和功能工作我们在Android和iOS上积累了多年的原生基础架构。然而,在ReactNative中,我们从一张白纸开始,必须编写或创建连接所有现有基础设施的桥梁。这意味着有时产品工程师需要尚不存在的功能。到那时,他们要么不得不在他们不熟悉的平台上工作,在他们的项目范围之外构建,要么被阻止直到他们可以创建它。APP崩溃监控我们使用Bugsnag在Android和iOS上进行崩溃报告。虽然我们能够让Bugsnag在两个平台上运行,但它不如其他平台可靠并且需要更多的工作。因为ReactNative在业界相对较新且很少见,所以我们必须构建大量基础设施,例如在内部上传源映射,并且必须与Bugsnag合作才能完成过滤器崩溃等操作。反应本机。由于围绕ReactNative的自定义基础设施的数量,我们偶尔会遇到严重的问题,如未报告的崩溃或源映射未正确上传。最后,如果问题跨越ReactNative和本机代码,调试ReactNative崩溃通常更具挑战性,因为堆栈跟踪不会在ReactNative和本机之间跳转。BridgeReactNative有一个桥接API,用于在本机和ReactNative之间进行通信。虽然它按预期工作,但编写起来非常麻烦。首先,它需要正确设置所有三个开发环境。我们还遇到了很多来自JavaScript的类型出乎意料的问题。例如,整数通常被包裹在字符串中,这个问题在桥接之前是行不通的。更糟糕的是,有时iOS会在Android崩溃时悄无声息地失败。我们从2017年底开始研究自动生成TypeScript定义的桥接代码,但为时已晚。初始化时间在ReactNative第一次渲染之前,它的运行时必须被初始化。不幸的是,这对我们的应用程序来说需要几秒钟,即使在高端设备上也是如此。这使得几乎不可能将ReactNative用于启动画面。我们通过在应用程序启动时对其进行初始化来最小化ReactNative的首次渲染时间。初始渲染时间与原生屏幕不同,渲染ReactNative至少需要一个完整的主线程->js->yoga布局线程->主线程往返才能有足够的信息来首次渲染屏幕。我们看到iOS上的平均初始p90渲染时间为280毫秒,Android上为440毫秒。在Android上,我们使用通常用于共享元素转换的postponeEnterTransitionAPI来延迟显示屏幕,直到它被渲染。在iOS上,我们遇到了来自ReactNative的快速设置导航栏配置的问题。因此,我们为所有ReactNative屏幕转换添加了50ms的人为延迟,以防止导航栏在加载配置后闪烁。应用大小ReactNative对应用大小的影响也不容忽视。在Android上,每个ABI的ReactNative(Java+JS+本机库如Yoga+JavascriptRuntime)的总大小为8mb。在一个APK中使用x86和arm(仅限32位)它将接近12mb。64位由于此问题,我们仍然无法在Android上发送64位APK。手势我们避免将ReactNative用于涉及复杂手势的屏幕,因为Android和iOS具有不同的触摸子系统,因此提出统一的API对整个ReactNative社区来说是一个挑战。然而,工作仍在继续,react-native-gesture-handler刚刚达到1.0。长列表ReactNative在这个领域取得了一些进展,包括像FlatList这样的库。然而,它们远不及Android上的RecyclerView或iOS上的UICollectionView的成熟度和灵活性。由于线程,许多限制很难克服。Adapter数据无法同步访问,所以可以在快速滚动的同时异步渲染查看视图。文本也不会同步测量,因此iOS无法使用预先计算的单元格高度进行某些优化。升级到ReactNative虽然大多数ReactNative升级都是微不足道的,但有些升级是痛苦的。特别是,几乎不可能使用ReactNative0.43(2017年4月)到0.49(2017年10月),因为它使用React16alpha和beta。这是一个非常大的问题,因为大多数为Web使用而设计的React库不支持预发布的React版本。2017年年中,在此次升级中纠结正确依赖项的过程对其他ReactNative基础设施工作造成了重大损害。麻烦的崩溃我们不得不处理一些非常奇怪且难以解决的崩溃。例如,我们目前正在经历带有@ReactProp注释的崩溃,并且无法在任何设备上重现,即使那些具有相同硬件和软件的设备也会在野外崩溃。在Android上保存在进程内的实例状态Android经常清理后台进程,但让它们有机会同步保存自己的状态。但是,在ReactNative上,所有状态只能在js线程中访问,因此无法同步完成。即使不是这种情况,作为状态存储的redux也不兼容这种方法,因为它包含可序列化和不可序列化数据的混合,并且可能包含比savedInstanceState包中更多的数据,这可能导致崩溃.生产。