前言:React Native 的其中一個賣點是程序可熱更新媳搪,當前官方和非官方對這類實操的完整指導不多铭段,所以在我們的項目實踐中,我們做了一套自己的方案秦爆,iOS 側已經上線運行序愚,理論上和實踐上沒啥問題,這里梳理出來等限,一方面作為后續(xù)我們在 Android 的對齊基準爸吮,另一方面與大家共享思路方便探討調優(yōu)。
要做好 React Native 的熱更新望门,主要需要處理好如下幾個情況:
本地啟動:為保證啟動速度形娇,不能全部依賴線上的 bundle,需保證還未下載到 bundle 的時候筹误,能如常載入 bundle 并啟動桐早,所以初始化 RCTBridge 或 RCTRootView 時用的 bundleURL 得指向本地而非網絡;
及時更新:為實現(xiàn)所用 bundle 能夠及時更新厨剪,需要在合適時機拉取最新版的 bundle 存放到本地勘畔,細則如下:在 app 啟動時,在 app 從后臺切到前臺后丽惶,以及在網絡狀態(tài)發(fā)生變化后炫七,發(fā)起請求拉取最新的配置信息,根據(jù)配置信息確定是否需要下載 bundle 以及后續(xù)處理钾唬。
流量節(jié)約:為實現(xiàn)可控的流量節(jié)約万哪,配置信息中包含了要使用的 bundle 信息如下:
- url:bundle 文件的存放地址;
- token:bundle 文件的標識字符串抡秆,每次將 bundle 文件成功保存到本地后奕巍,都同時在本地保存該值,以作下次拉取到配置時的比較依據(jù)儒士,當配置中的 token 與本地的一致的止,那就無需做后續(xù)的下載和更多相關操作;
- urging:更新該 bundle 的緊急程度着撩,可選值如下:
- 1:有 WIFI 就下載诅福,下好后重啟 app 時啟用 // 不緊急的時候用這個
- 2:有 WIFI 就下載匾委,下載好后,從后臺切回前臺的時候啟用 // 免流量氓润,界面刷新柔和赂乐,推薦這個
- 3:不管有沒有 WIFI 都下載,下載好后咖气,從后臺切回前臺的時候啟用 // 耗點流量挨措,界面刷新柔和,次推薦這個
- 4:不管有沒有 WIFI 都下載崩溪,下載好后浅役,立馬啟用 // 殺很大,一般不用這個
當讀取到上述信息后伶唯,基于配置中的 token 與本地值比較是否一致確認是否結束流程担租,如果不一致則以配置中的 url 發(fā)起一個請求,得到 bundle 后抵怎,保存到本地奋救,同時把配置中的 token 也保存到本地。
- 版本并存:為實現(xiàn)多版本同時并存反惕,提供 A/B Test尝艘、灰度發(fā)布等能力,需要做到:
- 約定每次發(fā)布 bundle姿染,都以新文件形式發(fā)布背亥,新老文件并存于服務器端,客戶端根據(jù)配置情況按需拉取悬赏、使用狡汉;
- 實現(xiàn)因應不同情況輸出不同配置信息的能力,有兩種做法:
a. 搭個動態(tài) server闽颇,提供個接口盾戴,接受表達客戶端情況的幾個參數(shù),根據(jù)這些參數(shù)的不同輸出不同的配置信息兵多,客戶端讀取配置信息時尖啡,都通過訪問 server 上的這個接口來;
b. 寫個 JavaScript 文件剩膘,在其中寫個函數(shù)衅斩,接受表達客戶端情況的幾個參數(shù),根據(jù)這些參數(shù)的不同輸出不同的配置信息怠褐,把這個 JavaScript 文件作為靜態(tài)資源部署到 server畏梆,客戶端讀取配置信息時,都通過訪問 server 拉取這個 JavaScript 文件,然后將其中的內容作為 JavaScriptCore 的 code 執(zhí)行一下奠涌,然后調用其中的函數(shù)來獲取配置信息宪巨;
由于懶得搭動態(tài) server,我們選擇了 b 做法铣猩,關鍵代碼如下;// versionControl.js茴丰, // 實際上這是個全局通用的資源版本控制配置文件达皿, // react-native bundle 作為其中一種資源存于其中。 // 注意:這里的代碼是要放到 JavaScriptCore 中直接執(zhí)行的贿肩,所以高級的 ES6 語法不能用峦椰。 var latestReactNativeBundleMetas = { ios: { url: 'http://cdn.xxx.com/react-native/1.1.0/04291109.ios.bundle', token: 'a69cc86a12115f0b962ef4bd8c0a8241' }, android: { url: 'http://cdn.xxx.com/react-native/1.0.3c.android.bundle', token: '' } }; var versionControlGetters = { production: function(platform, appVer, innerId) { // 每次在測試環(huán)境測試通過后,請將上邊的 latestReactNativeBundleMetas.ios 的值復制到這里汰规。 var meta = { url: 'http://cdn.xxx.com/react-native/1.1.0/04291109.ios.bundle', token: 'a69cc86a12115f0b962ef4bd8c0a8241' }; return { "react-native": { meta: meta, urging: 1 } }; }, test: function(platform, appVer, innerId) { return { "react-native": { // 這里的值一般維持不變汤功,使用 latestReactNativeBundleUrls.ios 的值即可。 meta: latestReactNativeBundleMetas[platform], urging: 3 } }; } } function getVersionControl(envType, platform, appVer, innerId) { return versionControlGetters[envType](platform, appVer, innerId); }
- (void)getVersionControl:(void(^)(NSDictionary *data))callback { if (callback) { NSString *url = @"http://cdn.xxx.com/config/versionControl.js"; AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:nil success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) { NSString *code = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]; JSContext *context = [JSContext new]; [context evaluateScript:code withSourceURL:[NSURL URLWithString:url]]; NSArray *args = @[[PlatFormUtil isNormalService] ? @"production" : @"test", @"ios", [PlatFormUtil AppVer], @(getCurrentInnerId())]; NSDictionary *data = [[context[@"getVersionControl"] callWithArguments:args] toDictionary]; callback([data objectForKey:@"react-native"]); } failure:^(AFHTTPRequestOperation * _Nonnull operation, NSError * _Nonnull error) { callback(nil); }]; } }
-
錯誤跟蹤:為實現(xiàn)諸如錯誤上報版本跟蹤溜哮、問題反饋版本跟蹤等需求滔金,需在代碼中提供版本號和 Build 號信息,為此茂嗓,提供一個 version 模塊餐茵,考慮到 iOS、Android 并存述吸,提供了一個公共的 version.base 模塊忿族,在 version.ios 和 version.android 中分別引用并擴展平臺相關的信息;
// version.base.js 'use strict'; export default class Version { code = '1.1.0'; build = '04291109'; folderUrl = 'http://cdn.xxx.com/react-native/'; platformCode = 'unknown'; };
// version.ios.js 'use strict'; import Version from './version.base'; export default new Version({ platformCode: 'ios' });
// version.android.js // 預留蝌矛,尚未啟用 'use strict'; import Version from './version.base'; export default new Version({ platformCode: 'android' });
鑒于 version.ios 和 version.android 的代碼是固定的道批,所以版本升級時,主要維護的是 version.base入撒,
發(fā)布流程自動化隆豹;
一般來說,一個發(fā)布過程應該包括如下過程:
- 修改 version.base 內的代碼茅逮,為 version 設置新的 code 和 build 信息噪伊;
- 通過 react-native bundle 把 bundle 生成出來,過程中注意命名氮唯,確保不與既有文件重名鉴吹,輸出新文件,發(fā)布之惩琉;
- 將上述生成的 bundle 復制一份豆励,覆蓋到 iOS、Android 項目的內嵌 bundle 文件所在位置;
- 然后根據(jù)新文件的路徑良蒸,調整 controlVersion.js技扼,發(fā)布之
這么個流程,人工搞是可以嫩痰,不過未免過于瑣碎繁瑣剿吻、易于出錯,所以建議搞腳本串纺,把這流程自動化起來丽旅。這個話題的細節(jié)比較多,后邊會單獨撰文詳述纺棺。