当前位置:首页阅读

Airbnb的ReactNative历程(二) 技术篇上

Airbnb的ReactNative历程(二) 技术篇上

Android

Airbnb的ReactNative历程(二) 技术篇上

以下系列文章转载自F君的挚友TigaLiang,一位Airbnb的高级工程师。主要讲述了Airbnb Android端的发展,欢迎各位读者订阅收看。

原文链接:

在 Android、iOS、web 和跨平台框架交叉领域中,React Native 是一个相对较新且迅速发展的平台。在两年的实践后,我们可以大胆地说,React Native 在很多方面是革命性的。对于移动端开发来说,这是一种范式的转变,我们能从它所主张的的很多方面获益。然而,在获益的过程中,也伴随着很多显著的痛点。

React Native itself is a relatively new and fast-moving platform in the cross-section of Android, iOS, web, and cross-platform frameworks. After two years, we can safely say that React Native is revolutionary in many ways. It is a paradigm shift for mobile and we were able to reap the benefits of many of its goals. However, its benefits didn’t come without significant pain points.

哪些方面奏效了

What Worked Well

跨平台

Cross-Platform

React Native 首要的好处是你写得代码能够同时以 Native 的形式在 Android 和 iOS 上运行。使用 React Native 实现的大部分的功能都能够复用 95% 到 100% 的代码,只有 0.2% 的文件是平台特有的(.android.js 或者 .ios.js)。

The primary benefit of React Native is the fact that code you write runs natively on Android and iOS. Most features that used React Native were able to achieve 95–100% shared code and 0.2% of files were platform-specific (.ios.js).

统一的设计语言系统(DLS)

Unified Design Language System (DLS)

我们开发了一门跨平台的设计语言,叫做 DLS。对于每一个 UI 组件,我们有 Android、iOS、React Native 和 web 的版本。有一门统一的设计语言和适合编写跨平台的功能,因为这意味着设计、组件名称和屏幕上的展现在不同的平台上都是一致的。同时,我们也能够在适当的情况下做一些适应不同平台的决策。比如,在 Android 上我们使用原生的 Toolbar,在 iOS 上则使用 UINavigationBar,另外我们在 Android 上隐藏 disclosure indicators(”右箭头”指示器),因为它并不遵循 Android 平台的设计规范。

We developed a cross-platform design language called DLS. We have Android, iOS, React Native, and web versions of every component. Having a unified design language was amenable to writing cross-platform features because it meant that designs, component names, and screens were consistent across platforms. However, we were still able to make platform-appropriate decisions where applicable. For example, we use the native Toolbar on Android and UINavigationBar on iOS and we chose to hide disclosure indicators on Android because they don’t adhere to the Android platform design guidelines.

我们选择了在 React Native 上重写各个组件而不是封装原生组件,因为给各个平台单独提供适合平台的 API 会更加可靠,同时因为 Android 和 iOS 工程师可能不知道怎样正确地测试 React Native 的更改,因此这么做也降低了维护成本。然而,这也造成了平台间的碎片化,同一个组件的原生版本和 React Native 版本可能会不同步。

We opted to rewrite components instead of wrapping native ones because it was more reliable to make platform-appropriate APIs inp>React

React

React 之所以是最受喜爱的 web 框架,这是有原因的。它简单并且强大,能很好地扩展到大型的代码库。其中几个我们尤其喜欢的点是:

组件化: React 通过良好定义的属性和状态强制要求分离关注点(UI 和业务逻辑)。这是 React 具有良好的可扩展性的主要原因。

简单的生命周期: 众所周知,Android 和 iOS(稍微好一点)的生命周期都非常复杂。React 组件从根本上解决了这个问题,并且让学习 React Native 比学习 Android 或 iOS 简单得多。

声明式: React 的声明式特性使得 UI 和底层的状态保持同步。

There is a reason that React is the most-loved web framework. It is simple yet powerful and scales well to large codebases. Some of the things we particularly like are:

Components: React Components enforce separation of concerns with well-defined props and state. This is a major contributor to React’s scalability.

Simplified Lifecycles: Android and, to a slightly lesser extent, iOS lifecycles are notoriously complex. Functional reactive React components fundamentally solve this problem and made learning React Native dramatically simpler than learning Android or iOS.

Declarative: The declarative nature of React helped keep our UI in sync with the underlying state.

迭代速度

Iteration Speed

使用 React Native 进行开发的时候,通过热加载(hot reloading),我们只需要一两秒钟就能在 Android 和 iOS 上看到代码的修改。对我们的原生 APP 来说,构建时的性能一直是头等的优先级,但从来都没有接近过我们使用 React Native 时的速度。最好的时候,原生 APP 的编译时间是 15 秒,但一次完整的打包时间可能高达 20 分钟。

While developing in React Native, we were able to reliably use hot reloading to test our changes on Android and iOS in just a second or two. Even though build performance is a top priority for our native apps, it has never come close to the iteration speed we achieved with React Native. At best, native compilation times are 15 seconds but can be as high as 20 minutes for full builds.

基础架构的投入

Investing in Infrastructure

在原生 APP 的基础架构层,我们集成了庞大的能力。所有的核心模块,例如网络、国际化(多语言)、A/B Test、共享元素转场动画、设备信息、账号信息等等,这些能力全都被封装到一个 React Native API 里。这些桥梁(bridges)是一些更复杂的部分,因为我们想把现有 Android 和 iOS 的 API 封装成对 React 来说是一致且规范的接口。随着原生基础架构的快速迭代和发展,让这些桥梁始终保持最新的状态,是一个不断追赶的过程,在这个过程中,基础架构团队的投入使得产品团队的工作更加容易。

We developed extensive integrations into our native infrastructure. All core pieces such as networking, i18n, experimentation, shared element transitions, device info, account info, and many others were wrapped in a single React Native API. These bridges were some of the more complex pieces because we wanted to wrap the existing Android and iOS APIs into something that was consistent and canonical for React. While keeping these bridges up to date with the rapid iteration and development of new infrastructure was a constant game of catch up, the investment by the infrastructure team made product work much easier.

没有这些基础架构上的投入,React Native 的开发体验和用户体验就会欠佳。因此,我们认为如果没有基础架构上大量持续的投入,React Native 就没法简单地被应用于现有 APP 的开发。

Without this heavy investment in infrastructure, React Native would have led to a subpar developer and user experiences. As a result, we don’t believe React Native can be simply tacked on to an existing app without a significant and continuous investment.

性能

Performance

人们对 React Native 最大的担忧之一是它的性能。然而,实际上这并不是一个问题。我们大部分 React Native 的界面和原生的界面一样流畅。人们通常认为性能只是一个单一的维度。经常有移动端工程师看到 JS 时就想着 “比 Java 慢”。然而,React Native 把业务逻辑和布局的过程移出主线程,实际上能够在很多情况下提升界面渲染的性能。

One of the largest concerns around React Native was its performance. However, in practice, this was rarely a problem. Most of our React Native screens feel as fluid as our native ones. Performance is often thought of in a single dimension. We frequently saw mobile engineers look at JS and think “slower than Java”. However, moving business logic and layout off of the main thread actually improves render performance in many cases.

有时我们确实会遇到性能的问题,这些问题通常是由过度渲染造成的,并且可以通过有效地使用 shouldComponentUpdate、removeClippedSubviews 及更好地利用 Redux 来缓解。

When we did see performance issues, they were usually caused by excessive rendering and were mitigated by effectively using shouldComponentUpdate, removeClippedSubviews, and better use of Redux.

然而,初始化和首次渲染时间使得 React Native 在启动界面(如下所述)、deeplinks 等方面表现较差,并提高了界面之间切换的 TTI(Time To Interactive)。另外,由于 Yoga 把 React Native 组件转成了原生的 View,界面绘制时的掉帧很难调试。

However, the initialization and first-render time (outlined below) made React Native perform poorly for launch screens, deeplinks, and increased the TTI time while navigating between screens. In addition, screens that dropped frames were difficult to debug because Yoga translates between React Native components and native views.

Redux

Redux

我们使用 Redux 实现状态管理,我们发现 Redux 很高效,并且能够防止 UI 和 状态不同步,以及很容易实现不同界面间的数据共享。然而,Redux 因为它的 boilerplate 脚手架而臭名昭著,并且它的学习曲线相对较难。我们为一些通用的模板提供了生成器,但这依然是使用 React Native 时的一大难题以及困惑来源。但这些难题并不是 React Native 特有的,所以并不值得强调。

We used Redux for state management which we found effective and prevented the UI from ever getting out of sync with state and enabled easy data sharing across screens. However, Redux is notorious for its boilerplate and has a relatively difficult learning curve. We provided generators for some common templates but it was still one of the most challenging pieces and source of confusion while working with React Native. It is worth noting that these challenges were not React Native specific.

原生的支持

Backed by Native

因为 React Native 的所有东西都能通过 bridge 调用原生代码,我们最终实现了很多我们一开始不确定是否可行的东西,比如:

我们开发了一个

通过封装 Android 和 iOS 上原有的库,我们让 Lottie 能够在 React Native 上正常运作。

React Native 使用我们原有的原生网络框架,并且在原生和 React Native 上都能使用缓存。

就像网络框架一样,我们封装了其余的原生基础框架,比如国际化(多语言)、A/B Test 框架等等,使得他们能在 React Native 上无缝地运作。

Because everything in React Native can be bridged by native code, we were ultimately able to build many things we weren’t sure were possible at the beginning such as:

We built a

We were able to get Lottie working in React Native by wrapping the existing libraries on Android and iOS.

React Native uses our existing native networking stack and cache on both platforms.

Just like networking, we wrapped the rest of our existing native infrastructure such as i18n, experimentation, etc. so that it worked seamlessly in React Native.

静态分析

Static Analysis

在 Web 端,我们长期深度地使用 ESLint。但在 Airbnb,我们是第一个使用 Prettier 的平台(译者注:ESLint 和 Prettier 都是静态代码分析工具)。我们发现,在 PR(Pull Request)的时候,Prettier 对于减少 nits 和 bikeshedding(译者注:nits 和 bikeshedding 指不严重的、不影响代码正确运行的问题,如代码格式等)很有效。我们 Web 端的基础架构团队正在深入调研 Prettier。

We have a strong history of using eslint on web which we were able to leverage. However, we were the first platform at Airbnb to pioneer prettier. We found it to be effective at reducing nits and bikeshedding on PRs. Prettier is now being actively investigated by our web infrastructure team.

同时,我们也分析测量渲染时间和性能,进而找出哪些界面应该优先深入研究,去找出其中的性能问题。

We also used analytics to measure render times and performance to figure out which screens were the top priority to investigate for performance issues.

相比我们的 Web 端的基础架构,React Native 更小,也更新,因此它已被证明是一个测试新 idea 的良好平台。很多我们为 React Native 打造的工具和 idea 都已经被 Web 端采用。

Because React Native was smaller and newer than our web infrastructure, it proved to be a good testbed for new ideas. Many of the tools and ideas we created for React Native are being adopted by web now.

动画

Animations

多亏 React Native Animated 库,我们得以实现顺滑的动画,甚至是交互驱动的动画,比如滚动时的视差效果。

Thanks to the React Native Animated library, we were able to achieve jank-free animations and even interaction-driven animations such as scrolling parallax.

JS/React 开源

JS/React Open Source

因为 React Native 实际运行的是 React 和 JavaScript,我们得以使用 Javascript 海量的开源项目,比如 Redux、Reselect、Jest 等等。

Because React Native truly runs React and javascript, we were able to leverage the extremely vast array of javascript projects such as redux, reselect, jest, etc.

Flexbox

Flexbox

React Native 使用 Yoga 来处理布局,这是一个跨平台的 C 语言库,它通过 Flexbox 的 API 处理布局的计算。早期,我们收到 Yoga 的一些限制,比如它不支持长宽比,但这个已经在后续的更新里添加了支持。另外,一些有趣的教程,比如 Flexbox froggy,使得入门更加有趣。

React Native handles layout with Yoga, a cross-platform C library that handles layout calculations via the flexbox API. Early on, we were hit with Yoga limitations such as the lack of aspect ratios but they have been added in subsequent updates. Plus, fun tutorials such as flexbox froggy made onboarding more enjoyable.

和 Web 端的合作

Collaboration with Web

在探索 React Native 的后期,我们开始针对 Web、iOS 和 Android 进行构建。因为 Web 也使用 Redux,我们发现大量的代码无需修改就可以在 Web 和 原生 APP 间共享。

Late in the React Native exploration, we began building for web, iOS, and Android at once. Given that web also uses Redux, we found large swaths of code that could be shared across web and native platforms with no alterations.

哪些方面不够理想

What didn’t work well

React Native 的不成熟

React Native Immaturity

相比 Android 和 iOS,React Native 还不够成熟。它狠心、野心很大,并且发展迅猛。虽然在大多数情况下,React Native 表现都很好,但是在某些情况下,React Native 的不成熟还是会表现出来,并导致一些在原生开发里很容易实现的东西变得很困难。不幸的是,这些情况很难预测,并且可能会花费几个小时到几天的时间去解决。

React Native is less mature than Android or iOS. It is newer, highly ambitious, and moving extremely quickly. While React Native works well in most situations, there are instances in which its immaturity shows through and makes something that would be trivial in native very difficult. Unfortunately, these instances are hard to predict and can take anywhere from hours to many days to work around.

维护 React Native 代码库的一个 Fork`

Maintaining a Fork of React Native

由于 React Native 的不成熟,有时候我们需要对 React Native 的资源打补丁。除了给 React Native 做贡献之外,我们不得不维护一个 fork,这样我们才能快速合入我们的修改并升级版本。过去的两年来,我们不得不在 React Native 的官方项目之上加了大概 50 个 commit。这个情况导致我们升级 React Native 的过程异常痛苦。

Due to React Native’s immaturity, there were times in which we needed to patch the React Native source. In addition to contributing back to React Native, we had to maintain a fork in which we could quickly merge changes and bump our version. Over the two years, we had to add roughly 50 commits on top of React Native. This makes the process of upgrading React Native extremely painful.

JavaScript 工具

JavaScript Tooling

JavaScript 是一门无类型的语言。缺乏类型安全既导致它难以扩展,也成为一些习惯于有类型语言的移动端工程师的争论点,不然这些工程师对学习 React Native 挺感兴趣的。我们探索过它的 Adopting Flow,但是它隐蔽的错误信息导致了令人沮丧的开发体验。我们也探索过 TypeScript,但是把它集成到我们现有的基础架构(比如 bable 和 metro bundler)已被证明是很有问题的。但是,我们还在继续积极地在 Web 端探索 TypeScript。

JavaScript is an untyped language. The lack of type safety was both difficult to scale and became a point of contention for mobile engineers used to typed languages who may have otherwise been interested in learning React Native. We explored adopting flow but cryptic error messages led to a frustrating developer experience. We also explored TypeScript but integrating it into our existing infrastructure such as babel and metro bundler proved to be problematic. However, we are continuing to actively investigate TypeScript on web.

重构

Refactoring

JavaScript 作为无类型语言的一个副作用是,重构会非常困难且又容易出错。重命名属性,尤其是对于那些名字很通用的(比如 onClick)的属性,这些属性又在多个组件间传递的时候,想要准确地完成这种重构简直就是噩梦。更糟糕地是,这种错误在线上版本出错,而无法在编译时就发现这种错误,而且很难增加合适地静态分析。(译者注:所谓“动态类型一时爽,代码重构火葬场”。)

A side-effect of JavaScript being untyped is that refactoring was extremely difficult and error-prone. Renaming props, especially props with a common name like onClick or props that are passed through multiple components were a nightmare to refactor accurately. To make matters worse, the refactors broke in production instead of at compile time and were hard to add proper static analysis for.

JavaScriptCore 不一致

JavaScriptCore inconsistencies

React Native 一个微妙而棘手地地方在于,它是在 JavaScriptCore 地环境下运行的。以下是我们由此遇到的一下问题:

iOS 系统自带了 JavaScriptCore,这意味着 iOS 上的 JavaScriptCore 通常是一致的,而且对我们来说一般不会出现问题。

由于 Android 系统并不自带 JavaScriptCore,因此 ReactNative 需要打包带上。因为默认打包的是一个古老的版本,因此我们需要花精力去打包一个更新的版本。

调试的时候,React Native 连接到一个 Chrome Developer Tools 实例。这非常好,因为那是一个非常强大的调试器。然而,当连接了这个调试器之后,所有的 JavaScript 就在 Chrome 的 V8 引擎下运行,在 99.9% 的情况下,这是没问题的。但是这里举一个有问题的例子,toLocaleString 在 iOS 上运行没有问题,但是在 Android 上只有在调试的时候才能正常运行。这证明 Android 的 JSC 并不支持这个函数并且自动失败,除非是在 V8 的环境下调试的时候才能正常运行。对产品开发的工程师来说,如果不了解这种技术细节,可能得花上几天的时间进行痛苦的调试。

One subtle and tricky aspect of React Native is due to the fact that it is executed on a JavaScriptCore environment. The following are consequences we encountered as a result:

iOS ships with its own JavaScriptCore out of the box. This meant that iOS was mostly consistent and not problematic for us.

Android doesn’t ship its own JavaScriptCore so React Native bundles its own. However, the one you get by default is ancient. As a result, we had to go out of our way to bundle a newer one.

While debugging, React Native attaches to a Chrome Developer Tools instance. This is great because it is a powerful debugger. However, once the debugger is attached, all JavaScript runs within Chrome’s V8 engine. This is fine 99.9% of the time. However, in one instance, we got bit when toLocaleString worked on iOS but only worked on Android while debugging. It turns out that the Android JSC doesn’t include it and it was silently failing unless you were debugging in which case it was using V8 which does. Without knowing technical details like this, it can lead to days of painful debugging for product engineers.

React Native 开源库

React Native Open Source Libraries

学习一个平台是既困难又花时间的。很多人只熟悉一到两个平台。一些提供原生桥梁(native bridges,比如地图,视频等)的 React Native 库,要求同时同等地熟悉 3 个平台才能够成功运用好这些库。我们发现,很多 React Native 的开源项目,都是由一些只在一到两个平台上有过经验的人编写的。这导致了这些库在 Android 和 iOS 上的不一致性及一些不符合预期的 bug。

Learning a platform is difficult and time-consuming. Most people only know one or two platforms well. React Native libraries that have native bridges such as maps, video, etc. requires equal knowledge of all three platforms to be successful. We found that most React Native Open source projects were written by people who had experience with only one or two. This led to inconsistencies or unexpected bugs on Android or iOS.

在 Android 上,很多 React Native 库也要求使用一个到 node_modules 的相对路径进行依赖,而不是把这些库发不到 Maven 仓库上,这个是不符合 Android 社区规范的。

On Android, many React Native libraries also require you to use a relative path to node_modules rather than publishing maven artifacts which are inconsistent with what is expected by the community.

Airbnb的ReactNative历程(二) 技术篇上)宝,都看到这里了你确定不收藏一下??