本來準備寫一篇完整的文章介紹下我司的 React Native 整體方案的實踐,但是下筆之後發現話題實在太多太長,很難寫成一篇觀點集中的文章,所以計劃分幾篇文章來介紹,希望對正在或者準備在 RN 上搞事情的同學有所啟發,另外一定要根據自己的業務場景來考慮問題,切勿人云亦云,不知所以。所以本文除了介紹我們最終是怎樣做的之外,我會著重強調“為什麼這樣做”。
本文主要講的是 RN 版本依賴和熱更新相關的話題,這是我們團隊的 RN 方案和業界流行的方案差異最大的地方,也是我們耗費心力最多的部分。本文會先講一下方案的主要思想,然後介紹下場景和緣由,最後詳解一些細節的實現。
下文以 SRN 代表我們團隊的 React Native 方案(souche react native)。
首先介紹核心思想,在我們團隊內部,最開始對 React Native 的版本依賴就達成這樣一個共識:
- 客戶端版本和 RN 業務版本強依賴,特定客戶端版本會鎖定特定版本的RN業務,RN 業務迭代需要客戶端發版。
- 同時提供熱更新能力,但是熱更新只能用來 hotfix 修復bug。
其實我們整個方案都是圍繞這兩個重點來實現的,很多同學看到這兩個核心點可能會非常困惑,可能就此結束閱讀此文,但是我是希望大家能夠繼續看下去,看一下為什麼我們要這樣做再做決定。
為什麼釋出 RN 業務需要發版?
要討論這個話題,其實首先需要討論一個終極問題:為什麼要引入 RN?不同的團隊可能會給出不同的答案,這都無可厚非,畢竟每個團隊的問題和場景都不同,自然引入一個新技術的理由也不同,唯一最差的理由可能就是“沒有理由”。
對於我們團隊來說,引入 RN 並不是因為它的程式碼可以熱更新這個特性,H5程式碼自帶遠端屬性,為什麼不用H5而是要用RN?我們的場景,偏B端,重互動而輕營銷,產品關注的主要是穩定性,而不是隨時變化的能力。對於需要靈活變化的場景,用H5完全可以滿足需求,互動和體驗/開發效率都沒有太大問題。所以我後來一直跟大家強調,RN 絕不是用來替換 H5 的。那為什麼要引入 RN 呢,其實在我們正式引入 RN 之前,我們已經在 RN 上積累了半年多,甚至元件庫都做出來了,但是從來沒有在業務中嘗試。後來,有一個契機,公司在C輪之後走向了團隊規模極速擴大的道路,之前我們公司很多B端產品都是重客戶端的,這時候團隊的瓶頸開始凸顯,業務的客戶端開發成本很高,Native的一些缺點開始暴露,例如 開發不夠靈活/跨端技術棧發展不統一/人力成本double(這是最突出的問題),對於需要快速試錯快速迭代的公司階段來說,RN 的優勢凸顯出來了,而這,就是我們引入 RN 的最重要的原因。
想清楚了為什麼要引入 RN 之後,應該如何使用 RN 也就很清楚了,我們並不特別關注 RN 的熱更新能力,而最關注的主要是兩點:
一、如何將 RN 的開發工程化,不管是客戶端開發還是前端開發,都可以用這套方案快速開發普通的業務。
二、穩健性。包含多個方面,底層的穩定,程式碼健壯性,版本依賴管理。
第一點,我們之後再講,這是我們整套體系中的一部分,做了很多事情來保證業務可以被快速生產,包括腳手架和開發框架/元件庫等。而第二點,就是文章開頭我們提到的核心思想。
為什麼要強依賴?什麼是強依賴?
我先解釋下,什麼叫做強依賴。一句話表達就是:特定版本的app中某個 RN 業務的bundle的版本也會被鎖定在某個版本,區別於覆蓋式熱更新和H5的實時訪問機制。
為什麼需要這麼做?有以下幾個因素考慮:
- 其一,跨端依賴。RN 中會大量依賴客戶端的功能,我們的 RN 業務都是內嵌到已經很成熟的APP中(我們公司現在有4個主要的APP),除了 SRN 內部封裝的一些固定的 Native 功能的呼叫,最主要的是會依賴一些 Native 的協議跳轉,例如 從某個 RN 頁面開啟 Native 一個頁面,然後這個 Native 頁面可以再開啟另外一個 RN 業務的頁面,具體如何實現的可以改天講,一句話來說就是通過 scheme 協議來做 RN 和 Native 互相的跳轉,這個協議其實就是一個強依賴,如果 RN 新發布了功能,但是客戶端不支援新引入的一個協議,線上客戶端更新了這個 RN 的包,跳轉就會出現問題,此為其一。
- 其二,底層升級。RN 官方還在快速迭代版本,很多重要功能也在不斷髮布,例如 0.43 的 FlatList,如果沒有版本鎖定的功能,可能很難升級底層 SDK 的版本,之前在聽一些分享的時候很多公司都會困擾與此,但是對於我們團隊來說,這裡完全沒有問題,基本可以隨時升級 底層 SDK 的版本,最近我們剛升級到 0.43。當我們要給一個 app 升級底層 SDK 的時候,只需要保證新版本的業務包和SDK相容即可,無需關心線上已經存在的版本,因為他們依賴的RN bundle 被鎖定在了之前的版本。
- 其三,功能升級。SRN 出爐後,我們快速上線了十幾個業務,但是後來我們給整個方案加入了很多新功能,例如 Native Bridge 新增方法/bundle 拆分等,這些功能都是不能做到向上相容的,而對於我們實現的機制,我們也無需考慮向上相容。另外,最近我們整個釋出整合的流程做了很大的改造,但是這種更新我們無需考慮各種版本依賴的問題,只需要考慮新版本 RN 包相容新方案新SDK即可。
不知道大家在使用覆蓋式熱更新的時候有沒有遇到這些問題,希望能有所啟發。
既然強依賴了,為什麼還有熱更新?
強依賴是個好東西,但是熱更新也是個好東西,我們不能捨本逐末,我們一直在弱化熱更新的優勢,但是不能否認它在某些場景下的必要性,例如緊急修復線上bug,例如一些偏營銷的業務場景,也會通過熱更新帶來快速釋出的能力。
但是,我們在熱更新和強依賴之間做出了一個平衡,將二者結合在一起,業務方可以將某個版本的app依賴的某個RN業務鎖定在某個版本,但是這個版本同時也具備熱更新能力,可能很多同學已經猜到了,我們是用兩個版本號來維護這個邏輯的,準確來說並不是兩個版本號,而是一個業界統一的版本格式:語義化版本控制規範(SemVer)。
格式如 ${major}.${feature}.${patch},遵循 semver 規範的版本號
選擇需要遞增的版本號
major: 主版本號,用於斷代更新或大版本釋出
feature: 特性版本號,用於向下相容的特性新增
patch: 修訂版本號,用於 bug 修復
遞增位的右側位需要清零,如 1.1.2 => 1.2.0複製程式碼
接下來我們就講講這裡的依賴邏輯。
如何鎖定依賴?如何熱更新?
接著上面語義化版本號來講我們的具體實現,可以發現我們需要版本鎖定和熱更新的需求,其實和SemVer的訴求完全一致,自然而然的,我們用 ${feature} 來鎖定版本,用 ${patch} 來熱更新,從語義上來說,鎖定版本後需要更新功能需要升級 ${feature} 版本,這就是一個 feature,需要熱更新就是修復問題,升級 ${patch} ,這就是一個 hotfix(or patch)。
最終,其實就是我們的app在使用者手機上執行時,會發起一個熱更新的請求,而這個熱更新的具體邏輯只會判斷“相同 ${feature} 下的最新的 ${patch} 版本號”,本文整篇的精華就是這句話,大家細細體會下。
具體到 SRN 中,其實這個邏輯不是放在客戶端,也不是放在RN程式碼中。而是放在一個專門維護版本依賴關係的Node服務中,將整個過程解耦,這樣依賴邏輯可以隨時統一更換。
每個RN業務釋出的時候都會講包先上傳到 CDN,然後將這次釋出的資訊記錄到這個 Node 服務中(如果不小心發錯了版本,在Node服務中操作一下就可以刪掉一次釋出),然後每個使用者app啟動的時候,會把使用者本地的所有RN業務包和他們的版本傳送給Node服務,Node服務會在服務端判斷 我們剛才提到的這個邏輯,返回“相同 ${feature} 下的最新的 ${patch} 版本號”,然後客戶端就會啟動熱更新邏輯去更新Node服務返回的最新版本的bundle。
這裡還有一點需要提到,其實我們對 ${major}.${feature}.${patch} 還做了擴充套件,以便支援測試/開發/預發 環境上的版本,例如測試環境上的版本號會是這樣:0.1.2-beta.8,也就是釋出測試版本不會增加 ${patch} 版本,而是會在擴充套件欄位上增加 ${beta} 版本,對於擴充套件欄位,因為是開發環境,處理規則是和 ${patch} 版本一樣,直接熱更新,所以除了線上環境,所有其他環境更改程式碼都不需要重新定義依賴。
另外一個問題就是這麼複雜的版本規則,如何友好的暴露給開發者?其實對於開發者來說,根本不需要自己維護版本號,也不需要知道這個規則,在業務釋出前,腳手架會給選擇,“環境:測試、預發、線上”,“釋出型別:功能迭代、bug修復”,這些都是互動式命令,選擇對應命令後,會自動升級當前業務的版本號,自動釋出和打tag,無需開發手動維護。
用邏輯圖表示:
等等,是不是少了點什麼?
關於版本依賴鎖定和熱更新其實到這裡基本結束了的,但是仔細深入思考的同學應該發現好像少了點什麼,的確,有這些還不夠,你的整個流程還跑不起來,少了最關鍵的一步:依賴宣告和本地整合。
特別是第一次發版的時候,客戶端如何知道當前版本的app依賴了哪些業務和鎖定這個業務的哪個 ${feature} 版本呢?
在開始的時候,我們的方案是在app的工程裡維護一個package.json 一樣的檔案,內部宣告好 dependence,和 npm 包的 package.json 格式類似。然後在jenkins或者ide裡打包app的時候,會執行一段指令碼,這個指令碼會將這個package.json的內容上傳到剛才提到的 Node 服務中,服務會走一個跟線上熱更新同樣的邏輯,將當前鎖定的${feature}版本的最新${patch}版本返回,然後這個指令碼會去下載這個最新的RN bundle,將其直接整合到工程程式碼中,並且更新 package.json 內的依賴宣告。
當釋出了一個線上bundle的時候,如果選擇了“功能迭代”,會自動給業務的 ${feature} 加一,這時候需要釋出新版本的客戶端,就要手動去改一下 package.json 中的這個 bundle 依賴的版本號,例如業務從 1.2.3 到 1.3.0,就把package.json中的版本號改為 1.3.0 ,如果後面你發現有bug又釋出了一次,不需要再改 1.3.1 了,打包的時候執行的指令碼會自動幫你改掉。也就是每次發版後,這個版的app內都會預設跟一個業務的最新版的bundle。
這樣做看起來挺完美的,但是後來我們發現還是有不少問題的,例如,我們的腳手架沒有把這個版本號的規則太多的暴露給開發者,但是開發者卻需要在每次發版前維護客戶端中的這個依賴,我們的app以及業務發版都很頻繁,造成整個流程不順暢。另外打包的指令碼穩定性和執行效能都會影響打包的過程,這個侵入對客戶端和測試同學體驗不好。
現在,我們馬上就會上一個新的方案,原理是將所有RN的包從概念上包裝成 Native的包,例如iOS的pod包,Android的maven包。每次釋出的時候,除了之前的流程,現在最後一步,我們會把js bundle分別包裝成一個 iOS 的 pod包,Android 的 maven 包。然後客戶端直接將 RN 的業務當做一個正常的Native業務來使用。
這樣,去掉了兩個動作:app打包時不需要執行整合指令碼了,對 app 打包流程沒有任何侵入;另外,不需要在客戶端宣告額外的依賴了,客戶端只需要用傳統的方式,在 podfile 或者 gradle 裡宣告依賴就可以,pod包和maven包的版本和RN業務的版本完全一致,規則也一致。
當然,這裡省略了很多細節,如果大家有需要詳細瞭解的,可以私下聯絡我。
總結
因為有版本依賴鎖定,最近我們的 SRN 整個升級到 0.43 以及 整合 bundle 拆分的功能 還有整個整合過程的徹底改變,都非常輕鬆,升級過程完全無痛,如果大家有遇到同樣的困惑,可以參考一下。後續其實我們也打算在某些偏營銷的場景中,可以不通過發版整合直接在一個app中訪問一個RN業務,其實在現有的機制上做一點小改動即可,還是那句話,看場景,不要徹底否定某些方案,可能只是大家遇到的場景不同,而技術的價值正式解決業務場景中的問題,而不是為了技術而技術。
有問題歡迎加我微信:mier963 諮詢,感謝閱讀。