[React Native] 加載、維護 bundle 的正確姿勢

前言:React Native 的其中一個賣點是程序可熱更新媳搪,當前官方和非官方對這類實操的完整指導不多铭段,所以在我們的項目實踐中,我們做了一套自己的方案秦爆,iOS 側已經上線運行序愚,理論上和實踐上沒啥問題,這里梳理出來等限,一方面作為后續(xù)我們在 Android 的對齊基準爸吮,另一方面與大家共享思路方便探討調優(yōu)。

要做好 React Native 的熱更新望门,主要需要處理好如下幾個情況:

  1. 本地啟動:為保證啟動速度形娇,不能全部依賴線上的 bundle,需保證還未下載到 bundle 的時候筹误,能如常載入 bundle 并啟動桐早,所以初始化 RCTBridge 或 RCTRootView 時用的 bundleURL 得指向本地而非網絡;

  2. 及時更新:為實現(xiàn)所用 bundle 能夠及時更新厨剪,需要在合適時機拉取最新版的 bundle 存放到本地勘畔,細則如下:在 app 啟動時,在 app 從后臺切到前臺后丽惶,以及在網絡狀態(tài)發(fā)生變化后炫七,發(fā)起請求拉取最新的配置信息,根據(jù)配置信息確定是否需要下載 bundle 以及后續(xù)處理钾唬。

  3. 流量節(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 也保存到本地。

  1. 版本并存:為實現(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);
                  }];
         }
    }
    
  1. 錯誤跟蹤:為實現(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入撒,

  2. 發(fā)布流程自動化隆豹;

一般來說,一個發(fā)布過程應該包括如下過程:

  • 修改 version.base 內的代碼茅逮,為 version 設置新的 code 和 build 信息噪伊;
  • 通過 react-native bundle 把 bundle 生成出來,過程中注意命名氮唯,確保不與既有文件重名鉴吹,輸出新文件,發(fā)布之惩琉;
  • 將上述生成的 bundle 復制一份豆励,覆蓋到 iOS、Android 項目的內嵌 bundle 文件所在位置;
  • 然后根據(jù)新文件的路徑良蒸,調整 controlVersion.js技扼,發(fā)布之

這么個流程,人工搞是可以嫩痰,不過未免過于瑣碎繁瑣剿吻、易于出錯,所以建議搞腳本串纺,把這流程自動化起來丽旅。這個話題的細節(jié)比較多,后邊會單獨撰文詳述纺棺。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末榄笙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子祷蝌,更是在濱河造成了極大的恐慌茅撞,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巨朦,死亡現(xiàn)場離奇詭異米丘,居然都是意外死亡,警方通過查閱死者的電腦和手機糊啡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門蠕蚜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悔橄,你說我怎么就攤上這事靶累。” “怎么了癣疟?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵挣柬,是天一觀的道長。 經常有香客問我睛挚,道長邪蛔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任扎狱,我火速辦了婚禮侧到,結果婚禮上,老公的妹妹穿的比我還像新娘淤击。我一直安慰自己匠抗,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布污抬。 她就那樣靜靜地躺著汞贸,像睡著了一般绳军。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矢腻,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天门驾,我揣著相機與錄音,去河邊找鬼多柑。 笑死奶是,一個胖子當著我的面吹牛,可吹牛的內容都是我干的竣灌。 我是一名探鬼主播聂沙,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼帐偎!你這毒婦竟也來了逐纬?” 一聲冷哼從身側響起蛔屹,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤削樊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后兔毒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漫贞,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年育叁,在試婚紗的時候發(fā)現(xiàn)自己被綠了迅脐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡豪嗽,死狀恐怖谴蔑,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情龟梦,我是刑警寧澤隐锭,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站计贰,受9級特大地震影響钦睡,放射性物質發(fā)生泄漏。R本人自食惡果不足惜躁倒,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一荞怒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秧秉,春花似錦褐桌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春完丽,著一層夾襖步出監(jiān)牢的瞬間恋技,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工逻族, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蜻底,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓聘鳞,卻偏偏與公主長得像薄辅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抠璃,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容