非死book 是如何構建首個跨平臺 React Native 應用的?
今年早些時候,我們發布了 React Native for iOS。React Native 將開發者在 web 上所使用的 React — 擁有聲明式的自包含組件以及快速的開發周期 — 帶到了移動平臺, 同時保留了原生應用程序的運行速度、保真度及外觀。今天,我們很高興地發布了 React Native for Android。
現在我們已經在 非死book 的生產環境中使用 React Native 超過一年了。幾乎就是一年之前,我們的團隊著手開發廣告管理應用。 當時我們的目標是創建一個新的應用,它能讓數以百萬計在 非死book 上做廣告的人們能隨時隨地的管理他們的賬戶,并創建新的廣告。最后的成果不僅僅是 非死book 第一完全使用 React Native 開發的應用, 而且是首次實現跨平臺的一個應用。本文將跟您分享我們是如何構建這個應用的,React Native 是如何是的我們行動更快的,以及我們所積累的教訓。
選擇 React Native
不久之前,React Native 仍然還是一中沒有在生產環境中得到驗證的新技術。所以利用該技術開發一個新的應用要承擔一些風險,不過這些風險已經被其潛在能帶來的好處蓋過去了。
首 先,我們最初由三個產品工程師組成的團隊已經對 React 很熟悉。其次,這個應用需要包含許多復雜的業務邏輯來精細的處理各種不同廣告格式、時間軸、日期格式、貨幣、貨幣約定等等東西。這些許多都已經用 JavaScript 寫好了。全部用 Objective-C 寫一遍之后為開發 Android 版本再用 Java 寫一遍,這種方案的前景對我們并沒有什么吸引力——當然也并不高效。第三,要實現大多數我們想要構建的 UI 界面 — 以列表、表格或者圖像的方式展示許多的數據——在 React Native 中做會比較容易。了解了 React,產品工程師應該可以快速高效的實現這些視圖。
當然,有些特性對這個新平臺而言卻是是一種挑戰 — 例如圖像編輯器,它可以用來給廣告主縮放和裁剪一張照片,還有地圖視圖,它可以用來給廣告主在以一個位置為中心的特定半徑內定位人員。另外一個例子就是面 包屑導航,它能幫助廣告主看清楚他們賬戶中的廣告層級。這些都向我們提供了將這個平臺推向更遠的機會。
首先為 iOS 構建廣告管理器
我 們的團隊決定首先實現這個應用的一個 iOS 版本,這樣的安排還不多,React Native 一開始也是針對 iOS 而開發的。接下來的幾個月我們的團隊成員從 3 名工程師發展到了 8 名。新聘人員對 React 不熟悉 — 而他們其中一些人對 JavaScript 也不熟悉 — 但他們都渴望為我們的廣告主構建一種優秀的移動體驗,所以他們成長得很快。
React Native 團隊中有經驗的 iOS 工程師幫助我們在 React Native 中橋接了一些終端設備上暫時還用不著的功能特性,比如提供對手機攝像頭的訪問。他們還幫助我們將已經被應用在其它 非死book 應用中的,執行用戶認證、分析、奔潰報告、網絡以及推送通知這些操作的一些 iOS 庫,捆綁到了應用。這樣就讓我們的團隊只用去關注構建這個產品。
如上所述,我們能夠重用許多之前已經有了的 JavaScript 庫。一個這樣的庫就是 Relay,非死book 的一個通過 GraphQL 將 數據傳遞到 React 的框架。另外還有一些處理國際化和本地化(這些邏輯在涉及到時間域和貨幣時會變得有些棘手)的庫。通常這些庫會從網站的一個 JSON 端點加載正確的配置。我們已經編寫了腳本將所有支持的語言導出為 JSON 文件,使用 iOS 的本地化包來引入這些文件,然后擁幾行 native 代碼就能將 JSON 數據暴露給 JavaScript。這讓我們的庫幾乎不做什么修改就能拿來用。
我們所面對過的比較大一個挑戰之一就是導航流。為了對廣告主現有的廣告和宣傳活動進行導航,我們想要有一個導航條。針對廣告的創作流程,我們需要一 個向導式的導航條。最重要的是,讓應用擁有正確的漸變動畫以觸摸手勢也很關鍵,否則應用感覺上會更像是一個漂亮的移動網站,而不是一個原生應用。
我們的方案就是 Navigator 組 件, React Native 的 CustomComponents 目錄下附帶就有這個東西能拿來用。實質上,它就是一個由其他的 React 組件堆積起來的 React 組件集合. 它可以顯示這些組件中的一個組件,并且這些組件會在按下按鈕或者發生觸摸時動畫輪換。它還有一個可插入式的導航條組件, 讓我們可以實現一個 iOS 風格的,用于大多數一般視圖的導航條,用于導航廣告和宣傳活動的面包屑導航,以及一個用于創建流程的向導式導航。導航條組件的動畫進度條可以接收通知,并 且匹配性的顯示進度增加的動畫。這意味著所有的動畫,針對視圖的以及針對導航條的,都是用 JavaScript 來進行計算的, 而測試顯示我們仍然可以以 60 fps 的幀率執行它們。
油Tube 視頻地址:https://youtu.be/a6yJ7M8FoEo
只有一種情況下導航動畫才會有問題,那就是當 Javascript 線程在一個長操作過程中被阻塞時。這種情況基本都是由處理大量新獲取的數據引起的。當然當你切換到新的頁面,讀取和處理新的數據是不可避免的。在一個網速 快的環境下, 這個問題可以用正在加載的導航動畫來解決。在這里我們采取另一種方案:在動畫完成前,是用 InteractionManager 組件顯式延遲數據處理(這個是 Reactive Native 內置的組件)。首先我們先切換到包含模板的頁面,然后使用 Relay 來做數據處理,接著他會自動調用必要的 React 組件來重新渲染界面。
部署到 Android
既然 iOS 的廣告管理器快要部署完成了,我們現在給這個 app 部署一個 Android 版本。使用 Reactive Native 的 Android 移植版應該是最好的選擇。幸運的是,Reactive Native 團隊已經將 Android 移植版做的夠好了。通常我們想盡可能重用代碼,不僅是業務邏輯也有 UI,因為大多數頁面是基本相同的,為調整樣式節省了時間。當然也有一些地方 Android 版的外觀和 iOS 版是需要不同的,比如導航,日期選擇器和開關按鈕等等。
油Tube 視頻地址:https://youtu.be/MNNR01NF290
幸運的是,React Native 打包器的黑名單功能和 React 的抽象機制幫助我們盡可能的在兩個平臺之間復用代碼,盡可能減少對平臺的檢查。對于 iOS,可以告訴打包器忽略以 .android.js 結尾的文件。而開發Android 的時候,則是忽略 .ios.js 結尾的文件。這樣,我們就可以對同一個組件用 Android 和 iOS 分別實現一次,而使用組件的代碼可以是平臺無關的。我們不是顯示的用 if/else 的方式來檢測平臺,而是重構平臺相關的UI部分,分割成不同的需要 Android 和 iOS 分別實現的組件。在發布 Android 版廣告管理器的時候,代碼的復用率達到了大約85%。
我們面臨的 一個大的挑戰是如何管理代碼。在 非死book,Android 和 iOS 的代碼在不同的代碼倉庫里。廣告管理器的 iOS 版本的代碼在 iOS 代碼庫里,而由于各種原因,Android 版本的代碼只能在 Android 代碼庫里。就像 iOS 版那樣,我們需要使用一些 Android 代碼庫里的 非死book 的 Android 庫。另外,所有的構建、自動化和持續集成工具都綁定了 Android 代碼庫。如果把已有的 iOS 版代碼重構,抽象出平臺無關的組件用于 Android 版的移植,則意味著我們要經常 fork 和 merge 相同的代碼的兩個不同版本。對我們來說,這種情形不能接受。
最后我們決定把 iOS 代碼庫當做可信的代碼源,主要是因為代碼已經存在并且 iOS 版的 App 也已經成熟了。我們通過 cron 的任務每天把所有 JavaScript 代碼從 iOS 同步幾次到 Android 代碼庫。直接提交 JavaScript 代碼到 Android 倉庫是不被允許的,除非同時提交到 iOS 倉庫。如果同步腳本檢測到有不一致的地方,會生成一個任務,后續去進一步檢查。
我 們同時還做到了讓 JavaScript 打包服務器從 iOS 倉庫運行 Android 代碼。這樣的話,那些主要開發 JavaScript,不涉及 native 代碼的開發人員就可以基于 iOS 代碼庫開發和測試自己的代碼改動。不過在測試在兩個平臺上的改動時,還是需要從 Android 代碼庫構建 Android 應用,從 iOS 代碼庫構建 iOS 應用,這是一件很麻煩的事情。為了優化 JavaScript 程序員的開發流程,我們開發了一些腳本,用來從持續集成服務器上下載合適的 native 二進制代碼文件。這使得大多數的開發者不需要保留 Android 代碼庫的副本,他們可以直接在可信的 iOS 源代碼上開發,這樣就可以像在 非死book 的 web stack 上一樣快速迭代。
React Native 團隊在這個 App 開發的過程中開發出了 React Native 平臺,提供我們需要的組件和接口,使得我們的 App 成為可能。將來這些組件也會給所有的 App 開發者帶來便利。即使我們不得不自己實現一些組件,在純 native 之上使用 React Native技術 也是值得一試的。這些組件是我們不得不實現的,并且可能將來也不會被其他團隊重用。
對于我們的教訓是,即使有大量的工具 和自動化腳本,要跨兩個分離的 iOS 和 Android 代碼庫進行工作是很困難的。在我們開發這個 App 的時候,非死book 使用的是這種模式,所有的構建自動化工具和開發流程都做了相應的配置。不過,如果一個產品有一個共享的 JavaScript 代碼庫,這種模式并不合適。幸運的是,非死book 正在往一個統一的代碼庫遷移 ,所有的平臺合在一起,只需要一份公共的 JavaScript 代碼拷貝,代碼同步也不再需要了。
我們遇到的另一個問題跟測試有關。有改動時,所有的工程師都需要小心的在所有的平臺上測一遍,整個流程很容易產生人為的錯誤。然而,使用同一個代碼 庫開發跨平臺的應用,這種問題是必然存在的。即便如此,React Native 帶來的開發效率以及一開始就在跨平臺開發中重用代碼帶來的優勢,遠遠超出測試不充分導致的偶發問題造成的代價。要知道的是,這個問題不光是產品開發工程師 會遇到,React Native 平臺開發工程師在開發 Objective-C 和 Java 的時候也會遇到。這些工程師大部分時間都不會只面對一門編程語言。同樣,這個問題也會影響JavaScript,例如,開發組件 API 或者部分共享(partially shared)的實現。通常,純 iOS 開發工程師是不需要測試 Android 上的改動的,對 Android 工程師也是如此。這主要還是一種文化上的差異,需要時間和努力去消除,在這個過程中我們產品的穩定性也在增強。
為了解決這個問題,我們還開發了每次構建都會執行到的集成測試。然而已有的持續集成系統只能在 iOS 平臺和 Android 平臺分別發現 iOS 和 Android 問題,不能做到在 iOS 的構建上運行 Android 測試,反過來也不行。這需要工程師去解決,不過還是有很多問題,可能會導致app出錯。
當這些都搞定之后,我們可以在兩個平臺上一起發布 非死book 第一個完全構建在 React Native 上的 app,有著 native 的外觀和體驗,由同一個 JavaScript 工程師團隊完成。當這些工程師加入團隊的時候,不是每一個人都熟悉 React Native,然而他們在僅僅5個月的時間就開發出了有著 native 外觀和體驗的 iOS 版本的 app。在3個月之后,他們發布 app 的 Android 版本。