阅读 89

Airbnb 的 React Native 历程(五):Airbnb 移动端的下一步

原文链接:medium.com/airbnb-engi…

我们在这个系列的 5 篇文章里,讲述了 Airbnb 使用 React Native 进行移动端开发的历程,以及在放弃 React Native 之后的计划。这是这个系列文章的第 5 篇。

This is the fifth in a series of blog posts in which we outline our experience with React Native and what is next for mobile at Airbnb.

激动人心的时代

Exciting Times Ahead

我们一边使用 React Native 进行实验时,一边也在加快我们原生端的发展。如今,我们有很多激动人心的项目在产品上或者正在筹备中。其中一些项目是受到 React Native 最好的部分和我们使用它的经验的启发。

Even while experimenting with React Native, we continued to accelerate our efforts on native as well. Today, we have a number of exciting projects in production or in the pipeline. Some of these projects were inspired by the best parts and learnings from our experience with React Native.

服务端驱动的渲染

Server-Driven Rendering

尽管我们不再使用 React Native,我们还是体会到了只写一次代码的价值。我们依然非常依赖我们的统一设计语言系统(DLS),很多界面在 Android 和 iOS 上看起来几乎是一样的。

Even though we’re not using React Native, we still see the value in writing product code once. We still heavily rely on our universal design language system (DLS) and many screens look nearly identical on Android and iOS.

一些团队已经开始实验并统一强大的服务端驱动渲染框架。通过这些框架,服务端往客户端设备发送数据,描述需要渲染的组件、界面配置和可进行的操作。然后每个移动端平台对这些数据进行解析,然后使用 DLS 组件渲染原生界面,甚至整个完整的产品流程。

Several teams have experimented with and started to unify around powerful server-driven rendering frameworks. With these frameworks, the server sends data to the device describing the components to render, the screen configuration, and the actions that can occur. Each mobile platform then interprets this data and renders native screens or even entire flows using DLS components.

大规模的服务端驱动渲染也具有一些自身的挑战。这里是我们正在解决的一部分:

  • 安全地更新组件的定义,同时维护向后兼容性。
  • 跨平台共享组件的定义。
  • 响应运行时的事件,比如按钮点击或用户输入。
  • 在几个 JSON 驱动的界面间切换,同时保持内部状态。
  • 渲染在构建时没有实现的整个自定义组件。我们正在用 Lona 格式对这个进行试验。

Server-driven rendering at scale comes with its own set of challenges. Here is a handful we’re solving:

  • Safely updating our component definitions while maintaining backward compatibility.
  • Sharing type definitions for our components across platforms.
  • Responding to events at runtime like button taps or user input.
  • Transitioning between multiple JSON-driven screens while preserving internal state.
  • Rendering entirely custom components that don’t have existing implementations at build-time. We’re experimenting with the Lona format for this.

服务度驱动渲染的框架发挥了巨大的价值,它允许我们无需手动下载(over-the-air,中文有点难描述~)就能实时地对功能进行实验和更新。

Server-driven rendering frameworks have already provided huge value by allowing us to experiment with and update functionality instantly over-the-air.

Epoxy 组件

Epoxy Components

在 2016 年,我们开源了 Android 上的 Epoxy 项目。Epoxy 是一个能够轻松实现异构 RecyclerView, UICollectionViewUITableView 的框架。如今,我们大多数新界面都使用了 Epoxy。通过这样,使得我们可以把一个界面拆分成独立的组件,并且实现懒渲染。如今,我们在 Android 和 iOS 上都有 Epoxy。

In 2016, we open sourced Epoxy for Android. Epoxy is a framework that enables easy heterogeneous RecyclerViews, UICollectionViews, and UITableViews. Today, most new screens use Epoxy. Doing so allows us to break up each screen into isolated components and achieve lazy-rendering. Today, we have Epoxy on Android and iOS.

Epoxy 在 iOS 上的代码是这样的:

This is what it looks like on iOS:

BasicRow.epoxyModel(
  content: BasicRow.Content(
    titleText: "Settings",
    subtitleText: "Optional subtitle"),
  style: .standard,
  dataID: "settings",
  selectionHandler: { [weak self] _, _, _ in
    self?.navigate(to: .settings)
  })
复制代码

在 Android 上,我们利用 DSLs in Kotlin 的能力,使得组件的代码既易编写又类型安全:

On Android, we have leveraged the ability to write DSLs in Kotlin to make implementing components easy to write and type-safe:

basicRow {
 id("settings")
 title(R.string.settings)
 subtitleText(R.string.settings_subtitle)
 onClickListener { navigateTo(SETTINGS) }
}
复制代码

Epoxy Diffing

Epoxy Diffing

在 React 中,你会从渲染器返回一个组件的列表。React 性能的关键点在于,那些组件都是一些数据模型,那些数据模型描述了真正想要渲染的 views 或者 HTML。然后将组件树进行比较,只有发生改变的部分才会被继续分发。我们在 Epoxy 里建立了类似的机制。在 Epoxy 里,我们在 buildModels函数里声明整个界面的模型。这一点,加上优雅的 Kotlin DSL,使得它在概念上和 React 非常相似,代码看起来是这样的:

In React, you return a list of components from render. The key to React’s performance is that those components are just a data model representation of the actual views/HTML you want to render. The component tree is then diffed and only the changes are dispatched. We built a similar concept for Epoxy. In Epoxy, you declare the models for your entire screen in buildModels. That, paired with the elegant Kotlin DSL makes it conceptually very similar to React and looks like this:

override fun EpoxyController.buildModels() {
  header {
    id("marquee")
    title(R.string.edit_profile)
  }
  inputRow {
    id("first name")
    title(R.string.first_name)
    text(firstName)
    onChange { 
      firstName = it 
      requestModelBuild()
    }
  }
  // Put the rest of your models here...
}
复制代码

每次数据发生变化的时候,你只要调用 requestModelBuild(),它就会使用最佳的 RecyclerView 分发调用重新渲染你的界面。

Any time your data changes, you call requestModelBuild() and it will re-render your screen with the optimal RecyclerView calls dispatched.

在 iOS 上,代码看起来是这样的:

On iOS, it would look like this:

override func itemModel(forDataID dataID: DemoDataID) -> EpoxyableModel? {
  switch dataID {
  case .header:
    return DocumentMarquee.epoxyModel(
      content: DocumentMarquee.Content(titleText: "Edit Profile"),
      style: .standard,
      dataID: DemoDataID.header)
  case .inputRow:
    return InputRow.epoxyModel(
      content: InputRow.Content(
        titleText: "First name",
        inputText: firstName)
      style: .standard,
      dataID: DemoDataID.inputRow,
      behaviorSetter: { [weak self] view, content, dataID in
        view.textDidChangeBlock = { _, inputText in
          self?.firstName = inputText
          self?.rebuildItemModel(forDataID: .inputRow)
        }
      })
  }
}
复制代码

一个新的 Android 产品框架(MvRx)

A New Android Product Framework (MvRx)

最近其中一个最激动人心的发展是我们正在开发的一个新框架,我们内部称它为 MvRx。MvRx 融合了 Epoxy、Jetpack、RxJava 和 Kotlin 的各方优点,还有很多来自 React 的原理,使得我们在构建新界面的时候比以往更容易、更无缝衔接。虽然有点自以为是,但它确实是个很灵活的框架,我们采用了很多常见的开发模式和 React 的优点来开发它。它还是线程安全的,它基本所有的东西都运行在子线程里,这就使得屏幕的滚动和动画非常流畅。

One of the most exciting recent developments is a new Framework we’re developing that we internally call MvRx. MvRx combines the best of Epoxy, Jetpack, RxJava, and Kotlin with many principles from React to make building new screens easier and more seamless than ever before. It is an opinionated yet flexible framework that was developed by taking common development patterns that we observed as well as the best parts of React. It is also thread-safe and nearly everything runs off of the main thread which makes scrolling and animations feel fluid and smooth.

目前它已经在很多界面上正常工作,并且基本免除了对生命周期的处理。目前我们正在一部分 Android 产品上试用它,如果它继续成功的话,我们会打算开源它。如果要构建一个有网络请求的可用界面,这里就是所需的完整代码:

So far, it has worked on a variety of screens and nearly eliminated the need to deal with lifecycles. We are currently trialing it across a range of Android products and are planning on open sourcing it if it continues to be successful. This is the complete code required to create a functional screen that makes a network request:

data class SimpleDemoState(val listing: Async<Listing> = Uninitialized)

class SimpleDemoViewModel(override val initialState: SimpleDemoState) : MvRxViewModel<SimpleDemoState>() {
    init {
        fetchListing()
    }

    private fun fetchListing() {
        // This automatically fires off a request and maps its response to Async<Listing>
        // which is a sealed class and can be: Unitialized, Loading, Success, and Fail.
        // No need for separate success and failure handlers!
        // This request is also lifecycle-aware. It will survive configuration changes and
        // will never be delivered after onStop.
        ListingRequest.forListingId(12345L).execute { copy(listing = it) }
    }
}

class SimpleDemoFragment : MvRxFragment() {
    // This will automatically subscribe to the ViewModel state and rebuild the epoxy models
    // any time anything changes. Similar to how React's render method runs for every change of
    // props or state.
    private val viewModel by fragmentViewModel(SimpleDemoViewModel::class)

    override fun EpoxyController.buildModels() {
        val (state) = withState(viewModel)
        if (state.listing is Loading) {
            loader()
            return
        }
        // These Epoxy models are not the views themself so calling buildModels is cheap. RecyclerView
        // diffing will be automaticaly done and only the models that changed will re-render.
        documentMarquee {
            title(state.listing().name)
        }
        // Put the rest of your Epoxy models here...
    }

    override fun EpoxyController.buildFooter() = fixedActionFooter {
        val (state) = withState(viewModel)
        buttonLoading(state is Loading)
        buttonText(state.listing().price)
        buttonOnClickListener { _ -> }
    }
}
复制代码

对于处理 Fragment 参数,进程重启时的 savedInstanceState 持久化,TTI 监测和其他很多特性,MvRx 都具有对应的封装,调用很简单。

MvRx has simple constructs for handling Fragment args, savedInstanceState persistence across process restarts, TTI tracking, and a number of other features.

我们也正在 iOS 上开发一个类似的框架,它目前正在早起的测试中。

We’re also working on a similar framework for iOS that is in early testing.

我们期望今早听到更多关于此的信息,但是我们对于目前所取得的进展非常激动。

Expect to hear more about this soon but we’re excited about the progress we’ve made so far.

迭代速度

Iteration Speed

从 React Native 切换回原生的时候,一个很明显的点是迭代速度。原本你只需要等一两秒钟就能够可靠地测试你对代码的修改,现在变成了最多要等 15 分钟,这是不可接受的。所幸的是,我们对此也会提供一些急需的改进措施。

One thing that was immediately obvious when switching from React Native back to native was the iteration speed. Going from a world where you can reliably test your changes in a second or two to one where may have to wait up to 15 minutes was unacceptable. Luckily, we were able to provide some much-needed relief there as well.

我们在 Android 和 iOS 上搭建了一个基础架构,能够让你只编译 App 的其中一部分,这部分包含一个启动器,也可以依赖特定的功能模块。

We built infrastructure on Android and iOS to enable you to compile only part of the app that includes a launcher and can depend on specific feature modules.

在 Android 上,我们使用了 gradle 的 product flavors 特性。我们的 gradle 模块长这个样子:

On Android, this uses gradle product flavors. Our gradle modules look like this:

这个新的间接引用使得工程师们可以构建和开发整个 App 的其中一小部分。再加上 IntelliJ 的 module uploading,在 MacBook Pro 上的构建和 IDE 性能得以显著提升。

This new level of indirection enables engineers to build and develop on a thin slice of the app. That paired with IntelliJ module unloading dramatically improves build and IDE performance on a MacBook Pro.

我们建了一些脚本用来创建新的测试 flavor,在短短的几个月里,我们已经创建了超过 20 个这种 flavor。使用这些新的 flavor 的开发模式下的构建,比平均的构建速度快了 2.5 倍,并且费时超过 5 分钟的构建的百分比降低了 15 倍。

We have built scripts to create a new testing flavor and in the span of just a few months, we have already created over 20. Development builds that use these new flavors are 2.5x faster on average and the percentage of builds that take longer than five minutes is down 15x.

作为参考,这个代码片段就是用来动态生成具有根部依赖模块的产品 flavor 的。

For reference, this is the gradle snippet used to dynamically generate product flavors that have a root dependency module.

类似地,在 iOS 上,我们的模块看起来是这样的:

Similarly, on iOS, our modules look like this:

同样的系统,带来比之前快 3 到 8 倍的编译速度。

The same system results in builds that are 3–8x faster.

结论

Conclusion

在一个不怕尝试新技术,同时致力于维持质量、速度和开发体验上的高标准的公司工作,是令人兴奋的。在结束之时,React Native 是我们用以实现功能的一个重要工具,并且带给我们很多思考移动开发的新思路。如果这听起来是一次你想参与的旅程,请告诉我们

It is exciting to be at a company that isn’t afraid to try new technologies yet strives to maintain an incredibly high bar for quality, speed, and developer experience. At the end of the day, React Native was an essential tool in shipping features and giving us new ways of thinking about mobile development. If this sounds like a journey you would like to be a part of, let us know!


这是这个系列文章的第五部分,这个系列重点讲述了 React Native 在 Airbnb 的历程,以及 Airbnb 在此后的计划。

This is part five in a series of blog posts highlighting our experiences with React Native and what’s next for mobile at Airbnb.