背景:上周,Airbnb 的工程团队发布了一个五部分系列文章,解释了他们为什么放弃 React Native。文章写得很好,如果你想了解本文讨论的背景,值得一读。本文中出现的引号都来自 Airbnb 的文章。
正如你可能想象的那样,Airbnb 最近的公告引起了我们 NativeScript 团队的兴趣。Airbnb 对 React Native 的反馈非常技术性,有趣的是,它深入探讨了 React Native 和 NativeScript 在应用开发中采用不同方法的领域。因此,在阅读这个系列文章时,我们不禁想知道——Airbnb 是否会因使用 NativeScript 而获得更好的结果?
在本文中,我们将详细介绍 Airbnb 的抱怨,并讨论在 NativeScript 中如何解决其中一些相同的问题。我们将从 NativeScript 擅长的事情开始(毕竟这是 NativeScript 博客),然后转向 NativeScript 不太擅长的方面。
让我们从 NativeScript 和 React Native 之间可能最大的区别开始——每个框架如何访问原生 API。
“React Native 有一个桥接 API 用于在原生和 React Native 之间通信。虽然它按预期工作,但编写起来极其繁琐。”
React Native 和 NativeScript 都提供了一系列跨平台 JavaScript API 用于常见的移动任务。例如,在React Native 中,你使用名为 Animated
的 API 进行动画,而在NativeScript 中,你使用名为 Animation
的 API。但尽管名称略有不同,但这些 API 提供了非常相似的功能(动画化用户界面组件)。在大多数情况下,这些框架的跨平台 API 都是相似的,在某些情况下,它们甚至是一样的——例如,这两个框架都提供了对 Web 的 fetch()
和 XMLHttpRequest
API 的支持。
当您需要执行不是常见移动任务的事情时,这些框架之间的架构差异就会显现出来——也就是,当您需要框架的跨平台 JavaScript API未提供的原生功能时。
在 React Native 中,要访问原生 iOS 和 Android API,您需要使用一系列通常称为桥接 API 的 API。(以下是React Native 的 iOS 桥接文档;以及Android 的相同文档)。
桥接 API 的完整分解超出了本文的范围。对于我们的讨论,需要知道的是,这些桥接 API 需要您编写原生代码和 JavaScript 代码。作为具体的例子,下图显示了 React Native 的 iOS 桥接文档中的第一个示例——注意顶部的 iOS 代码和下面的 JavaScript 代码。
React Native 需要采用这种方法,因为 React 本身运行在 JavaScript 虚拟机中,因此无法直接访问原生 iOS 和 Android API。像 React Native 的 RCT_EXPORT_METHOD
这样的钩子使原生代码能够连接到应用的 JavaScript 上下文,允许两者共存。正如 Airbnb 指出的那样,这些 API 确实按预期工作,并且它们成功地驱动了 App Store 和 Google Play 中的大量应用。但是,这种架构并非没有问题。
“这些桥接是比较复杂的部分,因为我们希望将现有的 Android 和 iOS API 包装成对 React 来说一致且规范的东西。虽然让这些桥接随着新基础设施的快速迭代和开发保持最新是一个持续的追赶游戏,但基础设施团队的投入使产品工作变得容易得多。”
要在现实世界的应用中利用桥接,您必须在三个地方编写代码——iOS 一次,Android 一次,以及在 JavaScript 中使用该代码一次——这会创建许多需要保持同步的活动部件,尤其是在 iOS、Android、React 和 React Native 都继续发布新版本的情况下。
这种架构的另一个结果是组织上的,因为您的代码不仅在三个不同的地方,而且这些代码可能由三个专门负责每个平台的不同团队编写。
“在 React Native 中,我们从一个空白的画布开始,不得不编写或创建所有现有基础设施的桥接。这意味着有时产品工程师需要一些尚不存在的功能。在那种情况下,他们要么必须在一个他们不熟悉的平台上工作,并且超出了他们项目的范围来构建它,要么就被阻塞,直到它被创建。”
“此外,需要大量的桥接基础设施才能使产品工程师有效地工作。”
在 NativeScript 中,我们采用了一种完全不同的方法来访问原生 API。NativeScript 不是使用桥接,而是将所有 iOS 和 Android API 直接注入到我们使用的 JavaScript 虚拟机中。(这种工作原理的完整技术细节超出了本文的范围,但如果你对这些框架的架构有任何兴趣,值得一读。)
这意味着如果您需要在 NativeScript 中访问原生 API……您只需执行即可。例如,以下代码是有效的 NativeScript 代码,它在 Android 上返回 JavaScript 整数 3
。
java.lang.Math.min(3, 4)
如果您使用任何跨平台框架开发过 iOS 或 Android 应用,您就会知道您需要访问原生 API 来调整应用行为的频率有多高。您可能不需要 java.lang.Math.min
,但您可能需要调整应用的状态栏,配置应用如何使用原生键盘,或为您的应用构建一个完整的插件,以便为其他人抽象这些原生 API。
用一种语言编写代码也有一些辅助好处,例如在开发期间减少上下文切换,更好地重用工具(prettier、linting 等),以及能够停留在一个 IDE 或编辑器中。这些优势非常吸引人,以至于 Airbnb 在 2017 年底试图在 React Native 中重现类似 NativeScript 的方法。
“我们在 2017 年底开始研究从 TypeScript 定义自动生成桥接代码,但这为时已晚。”
尽管如此,值得注意的是,NativeScript 的桥接架构并不是解决所有移动问题的灵丹妙药。虽然您可以使用 NativeScript 以 JavaScript 编写原生代码,但在超出简单的单行示例后,需要学习NativeScript 的桥接约定。(如果您想了解其规模,可以查看NativeScript 核心模块的源代码。)此外,即使 NativeScript 允许您用一种语言编写代码,如果您想为您的应用编写重要的原生代码,您仍然需要 iOS 和 Android 开发专业知识。
NativeScript 最适合需要能够轻松利用原生代码的应用,但仍然希望其大部分功能由跨平台友好的 JavaScript 或 TypeScript 代码驱动。
说到 TypeScript,让我们继续讨论在使用这些框架时另一个常见的痛点——类型安全。
“我们还遇到过许多来自 JavaScript 的类型与预期不符的问题。例如,整数通常被字符串包装,这个问题直到它通过桥接传递后才会被意识到。更糟糕的是,有时 iOS 会静默失败,而 Android 会崩溃。”
NativeScript 中一些更“有趣”的代码是将 Java <--> JavaScript 和 Objective-C <--> JavaScript 之间的值进行转换的代码。我对 React Native 的内部细节不太熟悉,无法在这里评论它们的具体实现,但我相信他们也有一些有趣的故事可以讲。
无论您使用什么平台,在多种语言之间进行类型转换都不是一件有趣的事情,但我们认为我们在 NativeScript 中处理事物的方式具有一定的优势——而这种优势就是TypeScript。
我对在 Web 应用中使用 TypeScript 有着复杂的情绪,我甚至就此发表过演讲,但对于移动应用,我现在认为 TypeScript 是绝对必要的,因为它能够帮助您使用不熟悉的 API(这在移动应用中很难避免)。
在 NativeScript 中,我们从第一天起就使用 TypeScript 构建了我们的跨平台模块,这意味着您可以在编辑器中轻松浏览我们提供的 API。
更重要的是,我们还为注入到 JavaScript 虚拟机中的原生 iOS 和 Android API 提供了 TypeScript 类型定义。这意味着您可以使用 TypeScript 编写原生代码——获得与在 Xcode 或 Android Studio 中编写代码相同的许多好处。
此优势帮助 JavaScript 开发人员编写无错误的原生代码,并帮助原生开发人员在 JavaScript 代码库中感觉更自在——这是 Airbnb 难以做到的事情。
“JavaScript 是一种无类型的语言。缺乏类型安全既难以扩展,也成为习惯使用类型化语言的移动工程师的一个争论点,这些工程师原本可能对学习 React Native 感兴趣。”
“JavaScript 是无类型的副作用是重构极其困难且容易出错。重命名 props,尤其是像 onClick 这样名称通用的 props 或通过多个组件传递的 props,都是一场准确重构的噩梦。更糟糕的是,重构在生产环境中而不是在编译时中断,并且难以为其添加正确的静态分析。”
如果 TypeScript 有任何缺点,那就是它只是另一个需要您维护、更新和集成的工具。我个人在我的 NativeScript 应用中没有遇到这些问题,但 Airbnb 在尝试评估 TypeScript 时提到了这些问题。
“我们也探索了 TypeScript,但将其集成到我们现有的基础设施(如 babel 和 metro bundler)中被证明是有问题的。但是,我们正在继续积极研究 Web 上的 TypeScript。”
然而,总的来说,我们发现 TypeScript 帮助我们更快地开发 NativeScript,并帮助我们的用户避免 JavaScript 驱动的原生框架固有的类型转换的一些更糟糕的方面。
让我们将讨论从使用原生 API 转移到移动应用中极其常见的用户界面组件——列表。
很多移动应用只不过是美化的列表——想想 Facebook、Twitter 等。列表是移动应用开发框架的基石,从一开始就给 React Native 带来了一些问题。
“[长列表] React Native 在这个领域取得了一些进展,使用了像 FlatList 这样的库。但是,它们远没有达到 Android 上的 RecyclerView 或 iOS 上的 UICollectionView 的成熟度和灵活性。许多限制很难克服,因为线程的问题。适配器数据无法同步访问,因此在快速滚动时可能会看到视图异步渲染时闪烁。文本也不能同步测量,因此 iOS 无法使用预先计算的单元格高度进行某些优化。”
Airbnb 在这里提到的关键点是“线程”。众所周知,React Native 使用多线程架构,其中 JavaScript 和用户界面在不同的线程上运行。虽然这有一些明显的优势,主要是因为 JavaScript 处理不会干扰 UI 渲染,但也有一些权衡。
以 Airbnb 提到的 RecyclerView
和 UICollectionView
API 为例。这些是构建内存高效列表的极其强大的内置原生 API,它们可以在用户滚动时异步渲染行。但是,React Native 无法使用这些 API,因为原生组件 (RecyclerView
或 UICollectionView
) 无法异步访问存储在 JavaScript 中的数据——它们位于不同的线程上。正如 Airbnb 指出的那样,React Native 投入了大量的工程力量来解决这个问题,并且他们的像 FlatList
这样的较新 API 非常好,但很难构建一个可以与内置原生控件相媲美的自定义解决方案。
在 NativeScript 中,我们使用单线程模型,因为它使我们能够快速访问前面讨论的原生 API。这意味着我们不受 React Native 的相同限制,并且可以绝对利用 RecyclerView
和 UICollectionView
等原生 API。为了让您了解其工作原理,下图显示了如果我随意将 50,000 个项目放入 NativeScript 列表中会发生什么。(值得注意的是,我在这里展示的不是 NativeScript 的强大功能;而是内置原生控件的强大功能。)
注意:您可以在NativeScript Playground中查看上面应用程序的源代码并自行运行。
在 NativeScript 中,您需要注意的一件事是,占用处理器资源的 JavaScript 确实有可能导致 UI 卡顿。
但有两点需要注意。首先,当您遇到这些 CPU 密集型任务时,NativeScript 确实提供了用于卸载这些任务的 API。其次,NativeScript 的架构与使用 Swift 或 Objective-C 在 iOS 上或使用 Java 在 Android 上编写的“原始”原生应用程序一致。默认情况下,原生代码在与原生 UI 相同的线程上运行,因此原生代码中的 CPU 密集型操作也会降低应用程序性能并导致丢帧。
此外,React Native 必须看到能够从 JavaScript 同步调用原生的某些好处,因为 React Native 团队最近宣布他们正在重新设计他们的线程模型以允许 NativeScript 默认情况下使用的这种确切用例。一旦实施,React Native 可能能够在列表性能方面缩小差距。
在谈到性能时,让我们继续讨论您可能没有意识到的一件事,它对 JavaScript 驱动的原生应用程序的性能有巨大影响——JavaScript 虚拟机。
NativeScript 和 React Native 都使用 JavaScript 虚拟机来驱动您的原生应用程序。React Native 在 iOS 和 Android 上都使用 Apple 的 JavaScriptCore,而 NativeScript 在 iOS 上使用 JavaScriptCore,在 Android 上使用 Google 的 V8。
而且,事实证明,您使用的这些 VM 的版本会对应用程序的性能产生巨大影响。
“Android 没有自带 JavaScriptCore,因此 React Native 自带了一个。但是,您默认获得的那个非常旧。因此,我们不得不费尽心思地打包了一个较新的版本。”
在 NativeScript 和 React Native 等框架中更新 JavaScript VM 可能会非常困难,因为 VM 代码与这些框架中一些最复杂的代码交织在一起。
在 NativeScript 中,我们也曾因为这个原因而让我们的 VM 版本停滞不前,但是,我们最近更新到了 V8 的现代版本,并且在 Android 上的启动性能得到了巨大(约 50%)的提升。例如,下图显示了 V8 更新前(左)和 V8 更新后(右)的 NativeScript 应用程序的启动时间差异。
更新 JavaScript VM 也为新的 JavaScript 语言特性打开了大门。例如,在 NativeScript 中,您现在可以在 NativeScript 中使用几乎所有 ECMAScript 6 特性,而无需预编译器。(我们在 ES6 模块上有一些工作要做,届时我们的 ES6 支持将完整。)
因此,在新的 JavaScript VM、TypeScript 更好的类型安全性、对内置列表控件的访问以及对原生 API 的轻松使用之间,希望您对像 Airbnb 这样的公司可能对尝试 NativeScript 感兴趣的原因有所了解。
但是,为了公平起见,我认为值得一提的是我认为 Airbnb 在我们得出任何结论之前可能不喜欢 NativeScript 的一些事情。
“我们的网站主要使用 React 构建。”
Airbnb 广泛讨论了他们在 Web 上使用 React 如何促使他们将 React 用于其原生应用程序。他们还谈到了如何在他们使用 React Native 的后期开始调查 Web 和原生应用程序之间代码共享的问题。
“在 React Native 探索的后期,我们开始同时为 Web、iOS 和 Android 构建。鉴于 Web 也使用 Redux,我们发现可以跨 Web 和原生平台共享大量代码而无需任何修改。”
在 NativeScript 中,我们已经看到了框架整合对于一家公司有多么重要。NativeScript 的初始版本没有提供任何框架支持,虽然我们确实有用户,但当我们在2016 年 4 月推出集成 Angular 支持时,我们看到了 NativeScript 使用量的大幅增长。
当 NativeScript 社区发布NativeScript 的 Vue.js 支持时,我们也看到了类似的使用量增长。当您已经知道导航、数据绑定等概念如何开箱即用时,开发人员的生产力可以提高多少真是令人惊叹——尤其是在您能够直接在 Web 和原生应用程序之间共享代码时。
话虽如此,NativeScript 并没有提供任何对 React 的支持,这对像 Airbnb 这样大量投资于 React 的公司来说并不理想。支持框架需要大量工作,尤其是在考虑框架支持需要的所有工具、模板、文档等时。在 NativeScript 中,我们对当前的 Angular 集成非常满意,并且正在与我们的社区合作支持 Vue.js。此外,我们认为我们无法比 Facebook 做得更好 🙂
使用 JavaScript 构建原生应用程序的最大好处之一就是开发周期有多快。因为您的源代码是用 JavaScript 编写的,所以您不必在每次更改后编译代码,这可以带来巨大的生产力提升。
“从 React Native 切换回原生后,立即显而易见的一件事是迭代速度。从一个可以可靠地在 1 或 2 秒内测试更改的世界到一个可能需要等待长达 15 分钟的世界是不可接受的。”
NativeScript 和 React Native 都能够利用 JavaScript 本身的这些生产力提升。例如,我最近使用下面的 gif 展示了您如何在多个设备上同时测试 NativeScript 的简单性。(更改是为了展示NativeScript 对渐变背景的支持,如果您好奇的话。)
尽管 NativeScript 和 React Native 都利用了 JavaScript 的优势,但 React Native 通过一项名为热重载的功能更进一步。热重载允许 React Native 在更改后重新加载应用程序,而不会丢失应用程序的状态。如果您好奇,可以阅读React Native 团队关于此功能的文章,但作为开发人员,此功能基本上意味着您可以非常快速地测试 JavaScript 更改。
话虽如此,NativeScript 的 LiveSync(如上图 gif 中所示)超级快,并且支持在您可以连接的尽可能多的设备上进行开发。使用 NativeScript,您可以在一秒钟内看到大多数更改,而不是原生应用程序中常见的 15 到 20 秒。
NativeScript 尚未提供热重载,但这是我们正在积极开发的功能,并且我们很快就能以毫秒为单位测量更新周期。请继续关注NativeScript 博客,了解未来计划和版本的详细信息。
如果您是一家拥有大型现有原生 iOS 和 Android 应用程序的公司,那么重写整个应用程序以使用新的技术栈的想法通常是行不通的。
“我们将 React Native 集成到大型现有应用程序中,这些应用程序继续以非常快的速度发展。”
尽管我们在 NativeScript 中确实提供了在现有应用程序中运行的功能(这是我们的 iOS 文档;这是我们的 Android 文档),但我们的支持目前处于实验性质,而不是正式支持的使用场景。
同时,React Native 团队在这个领域投入了大量精力,并且有许多经过实战检验的应用程序在生产环境中使用这种方法(包括 Airbnb)。此外,Facebook表示将更好地集成原生应用程序作为其即将进行的重新架构的重点。
但是,React Native 的架构并非没有问题。将 React Native 或 NativeScript 等框架集成到现有应用程序中可能会很复杂(只需浏览 React Native 文档以了解一下),并且固有的复杂性会导致问题,尤其是在 iOS、Android、React Native、NativeScript 等继续发展时。我们认为 Airbnb 的一些问题来自尝试将 React Native 集成到复杂的现有应用程序中,而不是从头开始构建新的 React Native 或 NativeScript 应用程序。
在 NativeScript 中,我们倾听用户的意见,以帮助优先考虑此类功能。如果您有兴趣让我们在此工作流程中投入更多时间和资源,请在此 GitHub 问题上告知我们。
那么,考虑到所有这些,Airbnb 使用 NativeScript 会否更成功呢?
显然我们永远不会知道,但我们确实相信 NativeScript 会帮助解决 Airbnb 遇到的几个问题,例如使用原生 API、处理类型和构建快速列表。话虽如此,NativeScript 不支持 React,并且没有提供与大型现有移动应用程序相同级别的集成。
像任何重大的技术决策一样,在开始全面开发项目之前,使用所有选项构建概念验证应用程序通常是一个好主意。考虑到这一点,如果您想尝试 NativeScript,请从NativeScript Playground 中的教程开始,我们的基于浏览器的体验将帮助您快速入门。如果您对本文中 NativeScript 有任何疑问,请随时在评论中提出。