react-native大型專案開發實踐

WittBulter發表於2017-02-23

RN流行有很長一段時間,也有不少人陸續在業務中使用,我從16年開始接觸RN開始運用在專案中也積累了一些經驗,這麼長時間給我感觸最深的一點是,即便有不少人願意嘗試RN,但也只是淺嘗輒止的心態,很少有人告訴我大型專案該如何架構,RN有哪些問題,每一步該怎樣做等等。為此,我付出了很多時間與精力,我希望這篇文章與現在流行的那些helloworld教程有所區別,能夠真正的幫助到大家,給你一些使用RN的信心。

在第一個RN專案開始之前,我對此還是一無所知,只能一遍寫程式碼一邊考慮架構與庫的選型,為此我付出了很多代價,專案反覆重構多次。像是資料通訊方式、資料管理方式、元件化的應用等等,無不是從頭摸索,當時我迫切的希望能夠有一些人站出來寫一些教程告訴我那些可以做那些不可以做,怎樣才是最佳實踐。現在我積累了一些自己的經驗,即便稱不上最佳實踐,但我很肯定這篇文章還是能夠給你的RN專案帶來一些改變,能夠給你一些新的觀點。

一次編寫,到處除錯

和官方宣稱的一次編寫,執行在不同裝置上的理念不同的是,這是一個漫長的過程,最後你會發現這不過是一次編寫到處除錯的過程。如果你真的希望能夠開發一次就完成所有事情那明顯是不可能的,特別是安卓與國產安卓,你會為此花費難以想象的時間。當然,無論如何,團隊的工作時間相較於全部原生開發還是有所減少,但我真正想說的是,這並不是一個能夠讓你們擠出大量時間的技術選型,特別是你想要做一個足夠好的應用時。

你會有一些疑問,這些問題都出在哪裡,如何解決這些問題。我並不願意細緻到每個功能點來講解RN的細節與實現的蹩腳之處,但以下幾個問題你肯定會在實際開發中遇到:

  • 官方與第三方庫並不全部支援安卓與iOS平臺
  • 庫的連結在不同版本會出現不同問題
  • UI在國產安卓上會有很大偏差
  • 全域性動畫在不同機型上表現差異過大
  • 可以實現的功能屬於安卓與iOS的交集(更少)

這裡可以簡單的舉幾個例子,你可能對此似曾相識,當然我並非是在盡數RN的不成熟之處,一個框架必然有其長短,總是好的未必一定都是好的! 你需要實現某個業務需求,它包含一個非常複雜的功能,github當然有成熟的庫供你使用,RN社群雖然很大,但由於本身更新速度很快,有不少第三方庫都不一定能夠跟上你的版本速度。為此你需要做一些相容性處理,還要考慮iOS與安卓雙方的相容情況,在第三方庫本身出現問題時你很可能會誤以為是庫的連結或其他bug,定位這些bug要付出相當長的時間。我曾經遇到過幾個第三方庫的問題,為此我反覆提了多個PR,但還要考慮作者review以及版本釋出的速度,最後你不得不為自己的選型付出代價,繞一段路來解決這些看似很簡單的問題。 如果你需要相容大多數版本的安卓,專案中javascript程式碼會變得臃腫很多,因為你必須不斷的判斷版本做出樣式上的相容處理,導致元件越來越大,這明顯也會影響到iOS包的大小。而且你所能使用的功能永遠是它們的交集,如果某個元件是iOS的特點,在安卓中實現就需要依賴js構建,這是一個很現實的問題:如果你希望它們表現相同就不得不用js模擬這個元件,相反的,你也可以為此構建兩套程式碼,但專案的複雜度和程式碼量也會隨之增大,RN的優勢就會被掩蓋

架構建議

上面例舉的這些問題是希望你能夠正視RN,而不是在一些helloworld教程裡面看了幾句話就不顧實際業務需求強行使用它,RN足夠好,但並非適合所有的專案。當然我最後還是選擇了它,並且完成了很多專案。好了,如果你正在使用它,我也希望能夠為你帶來一些有實際作用的建議。

1,資料庫
移動端資料庫有很多種,目前比較流行的有SQLite/LevelDB/Realm等等,我使用過其中幾個,最後在實際專案中由於資料量較大最後選擇了realm,當然還有一部分原因是它的文件很好,更新速度也足夠快。關於資料庫的選型你可以參考一些評測與實踐文章,根據自身的業務需求準備,RN自身也有一個簡化版的storage,小型專案也可以考慮直接使用自身的非同步儲存型資料庫。

這裡主要的問題在於大型專案需要一個基礎資料庫,基礎資料庫中攜帶類似於後設資料的資訊,你既可以在專案開始執行之後從服務端下載,也可以考慮將資料庫放在包中直接安裝。在實踐多種不同的方式後,最後我選擇了原生程式碼拷貝基礎資料庫,js程式碼連結被拷貝的資料庫做法。

這樣做並不難,你只需要會幾句簡單的OC/JAVA就可以做到,必須使用原生程式碼的原因在於原生程式碼總是優先執行,然後才會連結bundle,特別是在iOS中,應用是處於沙箱環境中,用javascript去解決還是需要對映原生函式才有可能。它的好處之一是,在未來你的安卓與iOS版本不對齊的情況下,可以優先為兩者準備不同的基礎書刊庫去更新應用。(安卓總是可以更頻繁快速的更新,iOS則受限於應用稽核)

2,資料管理
RN的資料管理可以照搬react的方式,經驗在於你的熟練程度與專案的大小。需要考慮的是大多數時候這些資料都不會直接從http去請求,而是從資料庫中照搬,在合適的時候準備更新資料庫。當然較小的專案可以忽略這單。

如果你準備從資料庫中讀取大部分資料,Redux是一個非常不錯的選擇。你可以在任何時候從資料庫中同步讀取資料,然後發起合適的Action,這些Action只是為了更新資料庫而存在,同樣的,在Redux中你也完全不必關心它們具體做了什麼。我在合理的時間發起Action,然後它們執行了哪些程式碼對於頁面來說不必在意的。

說到這裡,你完全可以理解使用Realm的優越之處了:Realm的所有資料採取對映的方式,即從realm中取出的資料就是realm物件,它在realm被更新之後會自動更新,而且在你沒有使用時完全不會有任何消耗。這與Rx的訂閱機制類似。提到Rx是因為它也很適合RN的專案架構,特別你需要資料庫與http更新混合時,你可以在頁面中訂閱這些資料,可以讓頁面中所有的資料得到有效的顯示又能利用上realm的快取。

在react-native的佈局中,你會頻繁的使用到listView元件,它為了效能考慮只有在listView資料來源發生變化時才會重新渲染,有時候你不得不手動的cloneWith一次觸發listView的render,當然你還得考慮列表的翻頁、快取、頻繁diff的效能等等。這時候在專案中加入Rx也不失為明智之舉,你可以用一些簡單的例項操作符去過濾一些不想要的資料,控制資料的渲染速度等等,總之,它會大大的減少你的程式碼量。

3,專案分層

  • 圖片與靜態資源:我嘗試把所有的圖片與靜態資源全部用物件的方式標示,例如在image資料夾下建立一個index.js檔案,匯出所有的圖片物件,使用時不再頻繁的require這些靜態地址。靜態資源的處理有一個很大的優點是專案變得更加的可控,無論合適你想要替換/更名/刪除某一個靜態資源時都輕而易舉。多人協作也會更加簡單優雅。
  • 資料操作:引入一些基礎的庫並不代表你可以完全遮蔽資料操作,最後還是會多出類似於model這樣的管理層,我嘗試把它們根據使用場景抽象,比如client負責本地資料庫,server負責遠端服務等等。
  • 公共服務:在RN中我嘗試引入了ES7中的裝飾器(decorator),將所有的公共服務注入需要的類。react或react-native中最常見的問題之一就是專案越大,import次數就越多,這會明顯導致整體專案趨於臃腫難以維護。引入裝飾器的用處之一就是消除這些頻繁的引入,僅僅在需要的類前進行標明。
  • 元件劃分:從大的角度看元件只有兩類:木偶元件與智慧元件,將他們區分開是第一步要完成的事,隨後我們希望所有的元件可以按照使用型別來分佈,如fom/button/modal等等,在每一個元件區域下用一個index.js來將它們集合起來統一返回物件。這是合理的,特別是身處於頁面繁多的專案中。
  • 錯誤處理:如果你正在使用Redux可以很簡單的處理所有可能的錯誤,我的經驗是在任何Action集合中都保持一個error管理函式,它用來擴散屬於自己的錯誤資訊與型別,同時過濾公共的錯誤在公共錯誤中處理(如http錯誤),細分的錯誤處理可以在頁面訂閱錯誤時得到優美的體驗。任何一步出錯都可以在當前需要訂閱的UI處予以最合理的顯示。
  • 路由管理:Navigator是個好元件,你需要學會怎樣使用它。建議你在任何使用到Navigator元件的地方都將它們剝離出來形成一個全新的檔案,作為當前view資料夾的index.js。你可以選擇跳過這些路由,但使用路由來預設管理views是一個明智的選擇,特別是路由檔案越來越多,套用了3層以上路由時,你會發現所有層級的view都一目瞭然,它們之間的關係也顯而易見,當你需要對一個Navigator的UI做出改變時也很簡單。我希望你再仔細研讀Navigator的文件,正確合理的使用它是專案成功的關鍵一點。
  • 繼承與組合:大多數時候建議你使用組合解決問題,如config= new Config,而非使用繼承。如果繼承的父類中有太多例項方法可能會致使子類函式不清晰,過於依賴狀態等等。但也有一些例外,如你在構建一個基礎的http類時即希望它能夠被繼承又希望它保持一個例項(有多個服務端或服務端方法各不相同),我給你一個不錯的提示,可以建立一個公共的檔案用來例項化,它只被使用一次且匯出一個被共享的例項,需要繼承時再返回去尋找原來未被例項化的父類。當然,具體組合還是繼承也需要考慮專案本身的需求,使用類來做一些重複且具備快取的事情是非常明智的,但你需要時刻警惕它們是否共享一個例項和類本身的可維護性。

其他建議與風格

雖然這有些不合時宜,但我還是非常推薦你儘可能使程式碼Immutable化,特別是在RN這樣依賴狀態的框架專案中。類似於函式式的程式設計可以大大簡化函式對於狀態的依賴,減少它們之間的互相作用,無論在什麼情況下,一個函式的終點可以得到確認。這很關鍵,比如你有多個或複雜的listView需要維護,我想你不願意一些狀態隨意的觸發render,為此你會做出很多的狀態判斷,那為什麼不試試從專案開始就避免這些state的多狀態變化呢?即便現在你很難體會它的優雅之處,但總有一天當你考慮優化某個函式時,會一拍大腿再次開啟這篇文章。

這與Redux的思想也並不違背,比如reducer就是純函式,在智慧元件與頁面中也是一樣,收到無論是Rx/Realm/Redux或是你的其他自建資料層傳送的狀態時,將它們在一個函式或幾個函式段中統一進行處理,然後再分發,比ui/元件中處處接受狀態要好得多。

我本意是給大家更多關於ReactNative的建議,但只要我想說一些關於程式設計的建議總會不由自主地為別人強加一些程式設計思想,希望你不會對此反感。我希望大家都能夠寫出足夠好的程式碼,對RN或是任何框架都有自己的理解,但這在之前,嘗試一些新東西也不為過,這裡列出的幾種方案可能你聽說過但並不願意嘗試,今天請給它們一次機會,如果真的對你並不適合在去除也不是一件壞事。

附,常見錯誤及解決方案

  1. 新建邏輯或檔案沒有任何效果:這很可能是你的檔案沒有被引入所致,在新建一個元件/靜態資源/邏輯程式碼時都不要忘記將它們引入,如果已經引入可以嘗試再次編譯。
  2. 資料庫已連線但查詢不到資料:如果你拷貝了資料庫,檢查iox/android的基礎資料庫是否存在問題,或更新資料庫版本;如果沒有請檢查資料庫是否被連結,是否有資料庫物件,以及資料庫版本。
  3. 無法找到xxxx(not found xxx):在編譯過程中你新增了某個檔案未能得到有效的watch,請嘗試重新build。
  4. http錯誤:列印錯誤碼對比逐步排除。
  5. 頁面動畫緩慢:檢查ios模擬器是否開啟了慢動畫(debug/slowAnimations)或是否存在記憶體洩露。
  6. 編譯失敗:build過程報錯多數是原生程式碼有問題,請根據命令列提示修改java/oc模板程式碼。
  7. 編譯成功但無法找到模擬器:android出現此情況請優先考慮是否成功載入模擬器,模擬器是否連結網路等。ios出現此問題多數是本機使用了全域性代理所致。
  8. string/float/bool/…型別錯誤:多數是資料庫的schema有問題,檢查模型定義與存入的資料型別是否一致。
  9. realm/xxxdata/… version …:可能存在一個比當前版本更高的資料庫版本,升級當前版本可解決。
  10. 頁面滑入時透明無高度:頁面需要一個背景色。
  11. 出入頁面時向右上方飛出飛入或類似問題:引入全域性動畫動畫或引入元件攜帶了全域性動畫所致,在動畫元件裝載或解除安裝時view未能有效的計算高度。解除安裝全域性動畫元件或在更早/更晚時規定view的高度。
  12. Obejct/any prototype not define:判斷物件是否存在此方法或屬性。

相關文章