[譯] Airbnb 在 React Native 上下的賭注(五 — 完結篇):Airbnb 移動端路在何方?

ALVIN君發表於2018-07-12

Airbnb 移動端路在何方?

發揮原生最大的潛力

[譯] Airbnb 在 React Native 上下的賭注(五 — 完結篇):Airbnb 移動端路在何方?

這是系列部落格文章中的第五篇,本文將會概述使用 React Native 的經驗,以及 Airbnb 移動端接下來要做的事情。

激動人心的時刻即將來臨

即使當初在嘗試使用 React Native 時,我們也同時加快了原生的開發。今天,我們在生產環境或正在進行中的專案方面,有許多令人激動的計劃。其中一些專案的靈感,來自我們使用 React Native 的最佳部分和經驗。

伺服器驅動渲染

即使我們已不再使用 React Native,但也看到了只編寫一次產品程式碼的價值。我們仍然非常依賴通用設計語言系統(DLS),因為許多頁面在 Android 和 iOS 上幾乎一模一樣。

幾個團隊已經嘗試開始在強大的伺服器驅動的渲染框架上達成一致。使用這些框架,伺服器將資料傳送到裝置,描述需要渲染的元件,頁面配置以及可能發生的操作。然後,每個移動平臺都會對這些資料進行解析,並使用 DLS 元件渲染原生頁面,甚至是整個流程。

伺服器驅動的大規模渲染還有很多難題。下面是我們正在解決的幾個問題:

  • 在保持向下相容性的同時,需要安全地更新元件定義。
  • 跨平臺共享元件的型別定義。
  • 在執行時響應事件,如按鈕點選或使用者輸入。
  • 在保留內部狀態的同時,在多個 JSON 驅動的螢幕之間進行過渡。
  • 在構建時渲染完全沒有現有實現的自定義元件。我們正在試驗 Lona 格式。

伺服器驅動的渲染框架已經提供了巨大的價值,我們可以即時實驗和更新功能。

Epoxy 元件

2016 年,我們開源了 Android 的 Epoxy。Epoxy 是一個框架,可以實現簡單的異構 RecyclerView、UICollectionView 和 UITableView。今天,大多數新頁面都採用了 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 中,你可以在 buildModel 中為整個頁面宣告模型。與優雅的 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()
    }
  }
  // 其餘模組程式碼放在這裡...
}
複製程式碼

每當資料發生變化時,你都要呼叫 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)
        }
      })
  }
}
複製程式碼

一個新的 Android 產品架構(MvRx)

最近令人非常激動的進展之一是,我們正在開發新架構,內部稱之為 MvRx。 MvRx 結合了 Epoxy、JetpackRxJava 的優點,以及 Kotlin 與 React 的許多原理,構建出的新頁面比以往任何時候都更容易、更流暢。它是一個固執己見而又靈活的框架,通過採用我們觀察到的共同開發模式以及 React 的最佳部分而開發出來的。同時它也是執行緒安全的,幾乎所有事情都從主執行緒執行,這使得滾動和動畫都能變得非常流暢。

到目前為止,它已經在各種頁面上正常工作了,並且幾乎不用去處理生命週期。我們目前正在針對一系列 Android 產品進行試用,如果它能繼續取得成功,我們會計劃開源。這是建立發出網路請求的功能頁面所需的完整程式碼:

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

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

    private fun fetchListing() {
        // 這會自動觸發請求並將其響應對映到 Async <Listing>
        // 這是一個密封類,可以是:Unitialized、Loading、Success 和 Fail。
        // 無需單獨處理成功和失敗的回撥!
        // 此請求也是有生命週期的。它將在配置更改後繼續存在
        // 在 onStop 之後不會再傳遞。
        ListingRequest.forListingId(12345L).execute { copy(listing = it) }
    }
}

class SimpleDemoFragment : MvRxFragment() {
    // 這將自動同步 ViewModel 狀態並重建 Epoxy 模型
    // 任何時候都會發生變化。類似於 React 的渲染方法:如何為每次更改而執行
    // 引數或狀態。
    private val viewModel by fragmentViewModel(SimpleDemoViewModel::class)

    override fun EpoxyController.buildModels() {
        val (state) = withState(viewModel)
        if (state.listing is Loading) {
            loader()
            return
        }
        // 這些 Epoxy 模型不是檢視本身,所以呼叫 buildModels 花銷很小。 
        // RecyclerView diffing 將自動完成,只有模型的改變才會重新渲染。
        documentMarquee {
            title(state.listing().name)
        }
        // 其餘模組程式碼放在這裡...
    }

    override fun EpoxyController.buildFooter() = fixedActionFooter {
        val (state) = withState(viewModel)
        buttonLoading(state is Loading)
        buttonText(state.listing().price)
        buttonOnClickListener { _ -> }
    }
}
複製程式碼

MvRx 的架構比較簡單,主要用於處理 Fragment 引數,跨程式重啟的 savedInstanceState 永續性,TTI 跟蹤以及其他一些功能。

我們還在開發一個類似的 iOS 框架,該框架正在進行早期測試。

預計很快會聽到更多這方面的訊息,我們對迄今取得的進展感到興奮。

迭代速度

當從 React Native 切換回原生時,馬上顯現出來的問題就是迭代速度。從一個在一或兩秒就能可靠地測試更改部分的平臺,到一個可能需要等待 15 分鐘的平臺,根本無法接受。幸好,我們也找到了一些補救措施。

我們在 Android 和 iOS 上構建了基礎架構,可以只編譯包含啟動器的應用中的一部分,並且可以依賴於特定的功能模組。

在 Android 上,這裡使用了 gradle product flavors。我們的 gradle 模組看起來像這樣:

[譯] Airbnb 在 React Native 上下的賭注(五 — 完結篇):Airbnb 移動端路在何方?

這種新的間接層,使得工程師們能夠在應用的一小部分上進行構建和開發。與 IntelliJ 的解除安裝模組配合使用,大大提高了 MacBook Pro 上的構建時間和 IDE 效能。

我們編寫了指令碼來建立新的測試 flavor,在短短几個月內,我們已經建立了 20 多個。使用這些新的 flavor 開發版本平均要快 2.5 倍,花費 5 分鐘以上的構建時間百分比下降了 15 倍。

作為參考,這是 gradle 程式碼段,可用於動態生成具有根依賴性模組的 product flavor。

同樣,在 iOS 上,我們的模組如下所示:

[譯] Airbnb 在 React Native 上下的賭注(五 — 完結篇):Airbnb 移動端路在何方?

相同系統的構建速度可提高 3-8 倍

結論

很高興能夠成為一家不怕嘗試新技術,同時又努力保持高質量、高速度和良好開發體驗的公司。最後,React Native 是一個發行新功能的重要工具,它為我們提供了新的移動開發思路。如果你想參與其中,請告訴我們


這是系列部落格文章的第五部分,重點講述了我們使用 React Native 的經驗,以及 Airbnb 移動端接下來要做的事情。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章