[React Native] 加载、维护 bundle 的正确姿势

前言:React Native 的其中一个卖点是程序可热更新,当前官方和非官方对这类实操的完整指导不多,所以在我们的项目实践中,我们做了一套自己的方案,iOS 侧已经上线运行,理论上和实践上没啥问题,这里梳理出来,一方面作为后续我们在 Android 的对齐基准,另一方面与大家共享思路方便探讨调优。

要做好 React Native 的热更新,主要需要处理好如下几个情况:

  1. 本地启动:为保证启动速度,不能全部依赖线上的 bundle,需保证还未下载到 bundle 的时候,能如常载入 bundle 并启动,所以初始化 RCTBridge 或 RCTRootView 时用的 bundleURL 得指向本地而非网络;

  2. 及时更新:为实现所用 bundle 能够及时更新,需要在合适时机拉取最新版的 bundle 存放到本地,细则如下:在 app 启动时,在 app 从后台切到前台后,以及在网络状态发生变化后,发起请求拉取最新的配置信息,根据配置信息确定是否需要下载 bundle 以及后续处理。

  3. 流量节约:为实现可控的流量节约,配置信息中包含了要使用的 bundle 信息如下:

  • url:bundle 文件的存放地址;
  • token:bundle 文件的标识字符串,每次将 bundle 文件成功保存到本地后,都同时在本地保存该值,以作下次拉取到配置时的比较依据,当配置中的 token 与本地的一致,那就无需做后续的下载和更多相关操作;
  • urging:更新该 bundle 的紧急程度,可选值如下:
    • 1:有 WIFI 就下载,下好后重启 app 时启用 // 不紧急的时候用这个
    • 2:有 WIFI 就下载,下载好后,从后台切回前台的时候启用 // 免流量,界面刷新柔和,推荐这个
    • 3:不管有没有 WIFI 都下载,下载好后,从后台切回前台的时候启用 // 耗点流量,界面刷新柔和,次推荐这个
    • 4:不管有没有 WIFI 都下载,下载好后,立马启用 // 杀很大,一般不用这个

当读取到上述信息后,基于配置中的 token 与本地值比较是否一致确认是否结束流程,如果不一致则以配置中的 url 发起一个请求,得到 bundle 后,保存到本地,同时把配置中的 token 也保存到本地。

  1. 版本并存:为实现多版本同时并存,提供 A/B Test、灰度发布等能力,需要做到:
  • 约定每次发布 bundle,都以新文件形式发布,新老文件并存于服务器端,客户端根据配置情况按需拉取、使用;
  • 实现因应不同情况输出不同配置信息的能力,有两种做法:
    a. 搭个动态 server,提供个接口,接受表达客户端情况的几个参数,根据这些参数的不同输出不同的配置信息,客户端读取配置信息时,都通过访问 server 上的这个接口来;
    b. 写个 JavaScript 文件,在其中写个函数,接受表达客户端情况的几个参数,根据这些参数的不同输出不同的配置信息,把这个 JavaScript 文件作为静态资源部署到 server,客户端读取配置信息时,都通过访问 server 拉取这个 JavaScript 文件,然后将其中的内容作为 JavaScriptCore 的 code 执行一下,然后调用其中的函数来获取配置信息;
    由于懒得搭动态 server,我们选择了 b 做法,关键代码如下;
     // versionControl.js,
     // 实际上这是个全局通用的资源版本控制配置文件,
     // react-native bundle 作为其中一种资源存于其中。
     // 注意:这里的代码是要放到 JavaScriptCore 中直接执行的,所以高级的 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) {
         // 每次在测试环境测试通过后,请将上边的 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. 错误跟踪:为实现诸如错误上报版本跟踪、问题反馈版本跟踪等需求,需在代码中提供版本号和 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. 发布流程自动化;

一般来说,一个发布过程应该包括如下过程:

  • 修改 version.base 内的代码,为 version 设置新的 code 和 build 信息;
  • 通过 react-native bundle 把 bundle 生成出来,过程中注意命名,确保不与既有文件重名,输出新文件,发布之;
  • 将上述生成的 bundle 复制一份,覆盖到 iOS、Android 项目的内嵌 bundle 文件所在位置;
  • 然后根据新文件的路径,调整 controlVersion.js,发布之

这么个流程,人工搞是可以,不过未免过于琐碎繁琐、易于出错,所以建议搞脚本,把这流程自动化起来。这个话题的细节比较多,后边会单独撰文详述。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容