4c7735ae4f78663f9571ead16964666e
[译] Airbnb 在移动端的下一步计划 - 把最好的带回原生

Medium 原文 What’s Next for Mobile at Airbnb

这是我们一系列文章中的最后一篇,该系列将重点介绍我们使用 React Native 的体验以及 Airbnb 在移动端的下一步计划。

激动人心的时刻

即使在试点 React Native 的时候,我们还在加倍努力地进行着原生开发。目前,我们已经产出或规划了许多令人兴奋的项目。其中一些项目的灵感来自于我们使用 React Native 的最佳实践。

服务端驱动渲染

即使在我们不使用 React Native 的时候,我们仍然看中产品代码只写一次的价值。我们依旧非常依赖我们的通用设计语言系统(DLS),并且在 Android 和 iOS 上许多界面看起来是几乎相同的。

已经有好几个团队开始尝试在强大的服务端驱动渲染上做统一框架。使用这些框架,服务端将数据发送到设备,描述要渲染的组件、界面配置以及可能发生的操作。然后每个移动平台解析这些数据,就能使用这些 DLS 组件渲染原生界面,甚至控制整个流程。

大规模的服务端驱动渲染带来了一系列挑战。以下是我们正在解决的问题:

  • 安全地更新我们的组件定义来保持向后兼容。
  • 共享我们跨平台组件的类型定义。
  • 运行时响应按钮点击或用户输入等事件。
  • 在保留内部状态的同时支持在多个 JSON 驱动的界面之间做转换。
  • 支持渲染没有在编译期实现的自定义组件。我们正在尝试使用 Lona 格式来实现这个功能。

服务端驱动渲染框架已经通过让我们尝试动态地更新功能来证明了它的巨大价值。

Epoxy 组件

2016年,我们在 Android 端开源了 Epoxy。 Epoxy 是一个能方便启用异构 RecyclerViews,UICollectionViews 和 UITableViews 的框架。现在,大多数新界面都在使用 Epoxy。这样做可以让我们将每个界面分解成独立的组件并实现延迟渲染。如今,我们可以在 Android 和 iOS 上使用 Epoxy 了。

它在 iOS 上是这样的:

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

在 Android 端,我们利用了 Kotlin 编写 DSL 的能力,使得编写组件更加方便,而且类型安全:

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

Epoxy 中的 Diffing 算法

在 React 中,你会从 render 方法中返回一系列组件。 React 性能的关键在于这些组件只是你要呈现的实际视图或 HTML 的数据模型表示。组件树会进行差异比较,只有改动过的才会被分发执行。我们为 Epoxy 建立了一个类似的概念。在 Epoxy 中,你可以在 buildModels 中为整个界面声明模型。与优雅的 Kotlin DSL 搭配使其在概念上与 React 非常相似,看起来像这样:

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 的分发调用来重新渲染你的界面。

在 iOS 上,它看起来像这样:

```
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)
}

top Created with Sketch.