58車商通RN落地與實踐
摘要:以上就是通信交互整個流程,但是在實際業務開發中我們發現,RN提供的基礎組件已經不能滿足我們的業務開發,部分需要依賴native原生功能來實現比如: 模塊間的跳轉、分享等,這就需要我們與native底層約定一些交互方法來滿足各種各樣的業務場景,基於此前端封裝了一箇中間交互的協議層。1. 首先來說現在業內對於React Native的項目更新還是個很大的問題,58車商通現在使用的版本是0.53,如果我們想升級RN那必須發版,並且基礎庫以及各個業務模塊可能都會受到影響。
導語
本文從RN的簡介到在車商通落地,從宏觀設計到深層次分析其通信原理,再到熱更新的進階設計三個方面,闡述了React Native在車商通中的設計以及流程 。
背景
58車商通
58車商 通是爲車商打造發佈及收車賣車、CRM管理、商機線索、庫存管理、全網同步、營銷推廣功能等全方位SAAS服務的移動端APP。
主要功能包括:
1. 發佈車源(核心功能)幫助車商快速發佈車源,和主App 58同城起到相輔相成的作用。 需求變化比較頻繁,開始採用原生開發,但是無法滿足需求的頻繁變化,於是2017年改版爲Hybrid。 後續考慮改版爲RN
2. 庫存管理(核心功能)幫助車商高效的管理自己的車輛,包括已經發布的,已經售出的,已經下架的,等等,以及定價,預警等相關功能。 需求變化相對穩定。
3. 客戶管理(核心功能)幫助車商管理自己的客戶,區分出客戶的購買意向程度,以及後續溝通等等。 變化頻率相對穩定
4. 營銷推廣(核心功能)幫助車商推廣車輛,包括置頂,刷新服務等,變化頻率相對較高
總結: 58車商通是58集團二手車部門的一個重要的App產物,幫助車商方便管理自己的車源,隨時發佈,同步其他市場。 而 RN 模塊第一次嘗試放在了車商通的每日任務模塊。
爲什麼會選中每日任務模塊
每日任務模塊: 用戶每日登陸之後可以通過每日的任務來賺取積分。 用於推廣等功能等。
那我們爲什麼要用每日任務模塊來做呢。
首先, 每日任務模塊體量較輕,入手起來相對比較簡單
其次, 線上有成熟的h5,如果出現嚴重問題的話,我們可以及時切換到線上h5進行補救
然後, 每日任務模塊雖然量輕,但是可以覆蓋協議的大部分功能。
最後就是這個模塊需求變化相對來說比較快,也可以檢驗熱更新模塊。
接下來我們來聊聊RN。
引言
開發已經經歷了幾個階段,從Native App 到 WebApp大火,再到蘋果公司禁Web,又發展到了Hybrid的Web與原生共生。 再到React Native,這種利用Js 轉成原生的取中方案,本文將就58車商通介紹下RN在其中的應用場景,以及發展階段。
RN特點
Learn Once,Write AnyWhere.
如下圖: 我們可以清楚的看到RN是構建在React和JSX的基礎上的。
使用RN的優點及不足
一個新的挑戰: 如何將開發成本和用戶體檢做好更好的平衡呢?
上面已經說了簡單說了一些RN開發的背景,接下來簡單介紹下RN的特性及優缺點
RN特性:
a) 提供了原生控件支持
使用RN可以使用底層原生控件,iOS可以使用UITabBar、
UINavigationController等標準的iOS平臺組件; 在Android平臺我們可以使用
Drawer控件; 這樣,就讓我們的App從使用上和視覺上擁有像原生App一樣的驗
b) 異步執行
所有的JavaScript邏輯與原生平臺之間的所有操作都採用異步執行模式,原生模
塊使用額外線程
c) 觸屏處理
RN引入了一個類似於iOS上Responder Chain響應鏈事件處理機制的響應體系,
並基於此爲開發者提供了諸如TouchableHighlight等更高級的組件,實現了高性
能的圖層點擊與接觸處理
但是,說到底RN畢竟也是前端基於JSCore通過Runtime機制或者反射機制來和Native端進行交互,所以RN還是有缺點存在的
RN缺點以及不足:
1. 首先來說現在業內對於React Native的項目更新還是個很大的問題,58車商通現在使用的版本是0.53,如果我們想升級RN那必須發版,並且基礎庫以及各個業務模塊可能都會受到影響。
2. React Native 的列表性能較差(不是滑動效果,是內存佔用方面),如果cell 很多,那容易崩潰,原因是RN的list控件不像Native 端有重用機制,不過現在業內借鑑native的重用機制的思路也有解決方案,也是目前我們的一個優化方向
3. React Native 的調用基於JSCore 和 Runtime的消息轉發機制,所以調起Native的組件,仍然是有一定的延遲,這個也是RN的一個瓶頸或者說上限,這也是爲什麼大家會說RN的效率是非常接近原生,但是遠遠高於WebApp的原因。
4. React Native 和Native 端是異步調用,所以有些操作必須要屏蔽用戶操作行爲,不過這個問題目前帶來的問題影響很小
其實,綜上來說,React Native 的開發總體上還是利遠大於弊,而且,熱更新對於App的Native開發,尤其是 iOS開發來說,價值很大,所以我們選擇落地RN項目,並實際運用到車商通中來。
58車商通RN模塊整體技術架構
車商通RN整體分爲三個部分:
- 客戶端: 提供部分基礎組件、提供交互協議、支持UI渲染展示頁面
- 服務端: 提供業務接口、提供項目bundle下載地址
- 熱更新平臺: 提供項目bundle文件、支持更新、回滾
58車商通客戶端設計和開發
客戶端整體框架如下:
1. 入口組件
每個應用程序都有一個對應的入口文件,前端頁面的開發項目目錄如下:
index.js是Android和iOS渲染前端UI的統一入口
import { AppRegistry } from "react-native";
import App from "./src/index"; //業務口
AppRegistry.registerComponent("入口名字", () => App);
把當前APP的前端UI對象註冊到AppRegistry組件中;
- AppRegistry 是運行所有 React Native 應用程序的 JS 入口點
- 應用程序入口組件需要通過 AppRegistry.registerComponent 來註冊它們自身
- 當註冊完應用程序組件後,Native就會加載jsbundle文件並觸發 AppRegistry.runApplication運行應用
2. RN通信機制
這部分內容,其實網上的資料比較多,而我們結合源碼,和已經一些已經公開的知識點,簡單跟大家分享一下。 後續如果大家感興趣,可以在單拿出一篇文章來分析討論。 下面我們來簡單看下RN從JS 端開始調起原生方法的原理(以OC爲例):
OC生成一張模塊配置表,包含所有模塊和模塊裏的方法,根據特定的標識宏(RCT_EXPORT_MODULE()),將可以暴露的方法暴露給JS。
OC-JS交互流程: (注: 此圖是網上的示意圖,但是表達的意思很明確,所以借用一下)
1. js調用OC模塊暴露出來的方法
2. 把調用方法分解爲ModuleName、MethodName、arguments,再丟給 MessageQueue處理
3. 把js的callback函數緩存在MessageQueue的一個成員變量裏面,同時生成一 個CallbackID來代表callback; 在通過保存在MessageQueue的模塊配置表 把ModuleName、MethodName轉成ModuleID、MethodID
4. 把ModuleID、MethodID、CallbackID和其他參數傳給OC ( JavaScriptCore)
5. OC接到消息,通過模塊配置表拿到對於的模塊和方法
6. RCTModuleMethod對js傳過來的參數進行處理
7. OC模塊方法執行完,執行block回調
8. 調用第6步中RCTModuleMethod生成的block
9. block帶着CallbackID和block傳過來的參數去掉用js裏的MessageQueue方法 invokeCallbackAndReturnFlushedQueue
10. MessageQueue通過CallbackID找到相應的js的callback方法
11. 調用callback方法,並把OC帶過來的參數一起傳過去完成回調
以上就是通信交互整個流程,但是在實際業務開發中我們發現,RN提供的基礎組件已經不能滿足我們的業務開發,部分需要依賴native原生功能來實現比如: 模塊間的跳轉、分享等,這就需要我們與native底層約定一些交互方法來滿足各種各樣的業務場景,基於此前端封裝了一箇中間交互的協議層。
3. 58車商通RN交互協議設計
協議層是native和前端對NativeModules進行了一些約定的封裝和處理,方便業務使用。
目前前端協議層通過一下幾種類型類集中封裝的:
RN跳轉類:
- 調起Native組件類 如: loading、toast等
- 調起native功能類 如: 分享、埋點、定位等
協議約定:
1. native封裝模塊宏CSTRNComponent到NativeModules下
2. 在模塊宏CSTRNComponent下聲明函數宏CSTRNHandler
3. 函數固定傳參兩個,第一個是協議交互的所有參數param(jsonString類型),第
二個固定傳入callback函數
4. 參數param格式
param = { action, params };
也是固定兩個參數
action:唯一確定調起的native組件,都是以CST開頭的 如:action = "CSTLoadPageRNWeb"表示RN跳轉H5
params:協議交互參數,每個協議都有不同的參數,具體協議的參數和native約定 如:
跳轉H5協議傳參
let params = {
url, //h5鏈接
jumpParameter, //參數
isDestoryBeforePage, //當前頁面是否銷燬關閉
title, //頁面titl
...other
};
5. 回調函數
CSTRNHandler的第二個參數就是協議的回調函數,傳入function類型
固定接收兩個參數
(error, event) => {
第一個參數error是一個錯誤對象(沒有發生錯誤的時候爲 null)
第二個參數event是native返回給前端的具體回調數據
(jsonString類型)
}
6. 前端封裝
根據以上約定規則,下面來看一下前端具體的協議封裝,以跳轉H5頁面爲例
const nativeBridge = NativeModules.CSTRNComponent.CSTRNHandler;
function loadPage(
{ url, isDestoryBeforePage = 0, jumpParameter, title, ...other },
disappearCallBack = (error, nativeData) => {}, //回調函數
action = "CSTLoadPageRNWeb"
) {
let params = {
url, //h5鏈接
jumpParameter, //參數
isDestoryBeforePage, //當前頁面是否銷燬關閉
title, //頁面title
...other
};
const error = checker(
arguments,
[
{
url: "s|r",
jumpParameter: "o",
isDestoryBeforePage: "n",
title: "s"
},"f","s"
],"loadPage"
);
// 線上環境報錯不調起 Native 的 API
if (error === "error") {
return error;
}
let paramToNative = { action, params };
nativeBridge(JSON.stringify(paramToNative), disappearCallBack);
4.58車商通-RN UI頁面開發
在上面我們大概瞭解了RN APP中的啓動流程和交互方式,那麼如何應用要我們的日常開發中能,下面來介紹一下前端的UI的開發。
1、本地開發流程
爲保證RN版本的匹配,和一些代碼規範的統一,在開發自己項目時需要克隆RN種子工程。 在開發過程中需要使用到本地調試,下面我們看看iOS端調試。
2、本地調試
a) iOS模擬器啓動頁面:
b) 啓動Chrome瀏覽器調試:
command+D彈出模擬器工具類 選擇選項Debug Js Remotely
c) 瀏覽器自動打開鏈接:http://localhost:8081/debugger-ui/
這時能在瀏覽器上查看所有前端js代碼了
d) 斷點調試
打開目的js文件,找到要調試的函數,直接打斷點,當執行該函數時就直接斷點攔截了。
58車商通-RN落地服務端開發設計
服務端在整個RNAPP中承擔的角色,就是爲APP提供基本的數據接口服務和提供RN項目資源信息和下載地址;
此處就講一下RN項目下載更新流程。
服務端接口返回數據模型:
{
"respData": {
"h5Url": "",
"business": {
"version": "90",
"remoteUrl": "https://j1.58cdn.com.cn/escstatic/rn/apptest/10021/android/10021_90_android_business.tgz"
},
"resource": {
"version": "90",
"remoteUrl": "https://j1.58cdn.com.cn/escstatic/rn/apptest/10021/android/10021_90_android_assets.tgz"
},
"bundleId": "10021",
"unpacking": true,
"downNow": false
},
"respCode": 0
}
服務端流程如下:
58車商通-熱更新平臺設計
1. 熱更新流程
在傳統的web開發中,我們修改完js之後在瀏覽器上就能直接看到效果,JavaScript本身就是一門動態語言,並不需要編譯,瀏覽器每次刷新都拉取新的js文件; 針對web應用最簡單也最有效的優化就是緩存,當js沒有更新,瀏覽器就不需要下載新的js文件;
在RN實現動態更新也是同樣的思路,RN中前端JS代碼最終都會打包成jsbundle文件,我們在需求更新時,在應用中從遠程下載這個文件,並重新加載,就可以完成動態更新同時無需通過App Store重新發布;
APP RN資源更新流程:
1、 APP啓動時:
2、 啓動項目時:
所有前面的更新流程都會依賴於前端的RN資源,那麼下面我們來看一下如何把一個項目在平臺上錄入、編譯打包和上線的。
2. 平臺資源錄入
在項目開發完成,提交到公司Git代碼倉庫,就可以使用RN資源管理平臺錄入資源了;
平臺功能如下:
項目錄入:
在填寫完信息後,平臺會根據填寫的git地址,把當前項目代碼下載到服務器,並npm install安裝當前項目需要的所有依賴; 這個過程因爲需要安裝項目依賴,所以時間比較長,項目初始化完成就可以對項目編譯打包了;
信息錄入時,會爲每個項目分配一個唯一的ID,也是客戶端區分項目的唯一標識。
3. 58車商通-RN打包編譯流程
RN項目編譯打包流程:
打包產物jsbundle
打包之後JSBundle文件的結構,基本分爲3部分:
- 頭部: 全局定義,主要是define,require等全局模塊的定義;
- 中間: 模塊定義,RN框架和業務的各個模塊定義;
- 尾部: 引擎初始化和入口函數執行;
在RN打包過程中,解析依賴關係,爲每個模塊添加一個id:
1、需求
在實際的RN業務開發中,我們會涉及到很多個業務,這些業務基本上也不會耦合,這時我們就需要創建多個RN項目,每個項目編譯打包成獨立的bundle; 對於RN APP來說,即使只有一個helloworld頁面,在使用官方命令react-native bundle打出來的jsbundle文件大約爲530KB以上,RN依賴模塊本身就佔了99%以上; 如果更新的話,需要從網絡上拉取整個包下載時間長,還會海鷗飛用戶流量,每次進入RN頁面還都要執行RN基礎模塊的定義; 在RN項目開發中基礎庫react和react-native是不變的,我們可以抽離這兩個依賴打成common.bundle內置於APP中;
2、目標
- 抽離react和react-native打包成common.bundle
- 減小線上下發業務bundle體積,減少下載時間、節省用戶流量
- 可預加載common.bundle,提升打開頁面速度
3、分析
- 通過分析bundle結構和依賴查找,最終可以通過標記法進行分包
- 打包bundle時,根據entryFile進行深度遍歷依賴分析,模塊id不斷遞增,即越早引用的模塊,id越小
- 在分析依賴時標記哪些模塊屬於common.bundle,哪些模塊屬於業務bundle
- 打包時先引入base.js保證common.bundle的模塊id都在前面,先收集common.bundle的模塊
- 在遍歷進行依賴收集,輸出業務bundle
//base.js
import React, { Component } from "react";
import {} from "react-native";
4. 拆分打包流程
編譯打包之後本地項目打包結果如下:
iOS端:
Android端:
5. 小結
優點:
- 一次性打包輸出common.bundle和業務bundle,效率高
- 用戶只需下載業務bundle,減少流量消耗和下載時間
缺點:
- 直接引用react-native作爲基礎,common中可能會引入一些用不到的模塊
總體上利大於弊,開發中也不可預知需要用到react-native哪些模塊,直接打包一個全集也未嘗不可。
6. 項目提測、上線
項目提測流程:
項目上線流程:
7. 項目回滾
在日常開發中,縱然有多輪測試,也避免不了發佈到線上不會存在問題,我們傳統的解決方案就是定位問題,找出原因,解決完之後重新發布上線; 如果是一個流量很大的需求,同時又出現了線上不容易解決的問題,此時線上就會出現長時間的功能無法使用的情況; 這是我們就可以考慮到回滾,先把項目回滾到一個可用的版本,然後再來解決自己的問題。
RN項目回滾流程如下:
8. 展望
理論上我們還可以指定規則,解耦業務,根據不同的業務模塊,劃分出更多的bundle,每個bundle的模塊id按照某個值開始,避免重複,類似android插件化處理資源id策略,按需加載業務bundle。
採坑
俗話說工欲善其事,必現踩其坑,下面我們分享一下我們在實踐過程中遇到的坑:
1. 統一封裝RN Fetch請求時android底層報錯
作爲前端開發人員,網絡請求工具對大家來說肯定不陌生。 iOS的AFNetworking,Android的okHttp等。 但是對於RN來說,我們最常用到的就是js原生的Fetch請求了。
React Native提供了和web標準一致的Fetch API,用於滿足開發者訪問網絡的需求。
錯誤原因:
在封裝RN fetch請求時header進行了統一設置設置如下:
在實際請求時android底層拋異常 如下:
查看RN Android底層源碼看到 RN的fetch請求在Android底層Content-Type只支持multipart/form-data類型。
但是iOS底層支持application/json、text/plain、application/x-www-form-urlencoded類型; 不支持multipart/form-data。
解決方案:
在封裝是需要分端去指定當前請求頭,android端headers中Content-Type只能是multipart/form-data
2. Android端Fetch POST請求無參數時,報異常
錯誤原因:
前端統一封裝POST請求如下:
如果POST請求無需傳參 此時FormData如下:
FormData {_parts: Array(0)}
Android底層會報錯,錯誤如下:
解決方案:
前端兼容POST參數,如果POST請求無需傳參 也就是params爲空時,前端拼接一個空的part,part的key、value值都置爲空。
3. RN頁面數據注入問題
3.1、頁面props注入
iOS端:
通過RCTRootView的初始化函數你可以將任意屬性傳遞給React Native應用。 參數initialProperties必須是NSDictionary的一個實例。 這一字典參數會在內部被轉化爲一個可供JS組件調用的JSON對象。
android端:
android底層目前無法實現參數注入的形式。
在實際業務開發中也無法使用此方面來實現RN頁面的數據注入。
3.2、使用消息監聽的方式
iOS端:
前端監聽:
OC端需要自定義實現消息監聽模塊,在前端首次調用消息監聽時OC端去註冊監聽事件,此時不會執行前端監聽回調,需要前端再次調用監聽事件。
android端:
前端監聽:
android端消息監聽使用的是RN封裝的DeviceEventEmitter消息監聽事件,可以在android端直接通過sendEvent方法發送廣播消息,前端監聽接收,實現消息的收發。
消息監聽的方式整體的是可行的方案,但是android端和iOS端消息監聽的實現差異太大,無法做到兩端統一,此方案也不是可選的最優方案。
3.3、事件交互獲取數據
最終我們的實現方案是前端和native端定義雙方交互事件,通過native事件通信的方式去獲取RN頁面想要的數據。
4. iOS 端內存不釋 放
在測試過程中我們發現,iOS端在數次退出進入RN模塊後會崩潰,糾其原因是
bridge沒有釋放,需要我們釋放一下
總結
本文總結了RN在58車商通的應用場景以及整體設計流程,還包括熱更新的幾個階段設計,從業務和技術兩個方面進行了闡述。 後續會持續的更新RN系列。 如果後續同學們想研究下RN的底層實現,我們可以在發一篇文章共大家參考。
經過大家的共同努力,我們終於在車商通3.7.3期順利上線了兩個RN模塊,但是我們也還有很多優化的空間。 後續我們也會持續跟進。 目前思路是從以下幾個方面來繼續優化
1. 前端工作,優化列表控件。 目前的列表性能還不是特別好,還需要下一步繼續優化
2. 熱更新繼續深入優化,最終做到增量更新(任重而道遠)。
3. 優化首屏加載白屏的問題,需要native 端與前端一起來做。
4. native 內部代碼結構還需要進一步調整,重構代碼。
由於我們的RN 項目是先在公司的主站RN項目開源前落地並開發完成的,目前階段,我們還沒有做更深一步的差量化更新(以補丁包的形式下發更新),所以,深度上講我們目前沒有涉及更深層次。 如今,公司主站App,更加成熟的RN體系已經開源,我們可以借鑑或者應用更加成熟的RN體系到應用的到項目中,以提高App的開發效率,縮短更新流程,降低更新風險
參考文章
1. ReactNative中文網
2. ReactNative GitHub源碼
3. Fetch API
作者簡介
桑銳/58同城/ iOS高級開發工程師
龔成輝 /58同城/ 前端高級開發工程師
閱讀推薦
2. 深度|數據智能在二手車業務場景中的探索與沉澱——關於用戶流量的預測與識別