乐趣区

ReactNative分布式热更新系统

热更新是一个非常方便的方案。在应对大量用户和深度定制的时候一定不能使用开源的方案。
一般第三方的这种方案,服务器带宽较小,或者不够灵活,不能满足自己的想法。
这里推荐自己实现对应的热更新方案。只需要少量代码即可支持。
下面推荐一种灵活的热更新方案。包括客户端的改造、接口设计、界面开发,同时是开源的!可以自由改造。

体验地址:demo 用户名密码都是:admin

基础数据的准备和实现

首先第一点,一个 APP 如果要支持热更新,需要在打开 APP(或者其他进入 RN 页面之前)就要判断是否需要更新 bundle 文件。这里就是我们实现热更新的节点。一旦需要热更新就开始下载文件,而判断的接口就是我们这次文章的核心内容。这里简单贴出安卓和 ios 两端的下载逻辑。

请求之前需要在 head 中附带上客户端的几个重要信息。客户端版本号 version、客户端唯一 id:clientid、客户端类型 platform、客户端品牌 brand。

ios 下载的例子

-(void)doCheckUpdate
{
  self.upView.viewButtonStart.hidden = YES;
  if ([XCUploadManager isFileExist:[XCUploadManager bundlePathUrl].path])
  {// 沙盒里已经有了下载好的 jsbundle,以沙盒文件优先
    self.oldSign = [FileHash md5HashOfFileAtPath:[XCUploadManager bundlePathUrl].path];
  }else
  {// 真机计算出的包内 bundlemd5 有变化,可能是压缩了,所以这里写死初始化的 md5
    //    NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"jsbundle"];
    //    self.oldSign = [FileHash md5HashOfFileAtPath:ipPath];
    self.oldSign = projectBundleMd5;
  }
  
  AFHTTPSessionManager *_sharedClient = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"http://test.com"]];
  
  [self initAFNetClient:_sharedClient];

  [_sharedClient GET:@"api/check" parameters:nil progress:nil success:^(NSURLSessionDataTask * __unused task, id JSON) {NSDictionary *dic = [JSON valueForKeyPath:@"data"];
    BOOL isNeedLoadBundle = YES;
    if ([dic isKindOfClass:[NSDictionary class]])
    {self.updateSign = [dic stringForKey:@"sign"];
      self.downLoadUrl = [dic stringForKey:@"downloadUrl"];

      if(self.updateSign.length && self.oldSign.length && (![self.updateSign isEqualToString:self.oldSign]))
      {
        // 需要更新 bundle 文件了
        self.upView.viewUpdate.hidden = NO;
        [self updateBundleNow];
        isNeedLoadBundle = NO;
      }else
      {
        // 不需要更新 bundle 文件,再处理跳过按钮显示逻辑
        [self.upView showSkipButtonOrNot];
      }
    }
    if (isNeedLoadBundle) {[self loadBundle];
    }
  } failure:^(NSURLSessionDataTask *__unused task, NSError *error) {[self loadBundle];
  }];
}

安卓下载的例子

private void requestData() {

        subscribe = DalingNetwork
                .getDalingApi()
                .getBundleVersion()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new BaseSubscriber<BundleVersionResponse>() {
                    @Override
                    public void onError(Throwable e) {startMainActivity();
                        e.printStackTrace();}

                    @Override
                    public void onNext(final BundleVersionResponse response) {
                        isJSNeedUpdate = false;
                        if (response.status == 0) {if (response.data != null) {if (MainApplication.getApplication().getBundleMD5().equalsIgnoreCase(response.data.sign)) {
                                    // 和本地版本相同,直接进入主页
                                    isJSNeedUpdate = false;
                                    tv_skip.setVisibility(View.VISIBLE);
                                    startMainActivity();} else {
                                    // 下载升级
                                    isJSNeedUpdate = true;
                                    downloadSign = response.data.sign;
                                    downloadUrl = response.data.downloadUrl;
                                    downLoad(response.data.downloadUrl, response.data.sign);
                                }
                            }
                        } else {startMainActivity();
                        }
                    }
                });
    }

系统设计方案

首先来看一下我们是怎样设计客户端获取更新逻辑的。

  1. 客户端请求的时候会带上版本号、平台 2 个重要信息。
  2. 接口拿到请求之后查询对应的本地缓存,没有则去数据库查询。
  3. 从查询结果中筛查对应的 3 段数据:白名单、灰度、全量,判断顺序从左到右。
  4. 返回查询之后对应的结果。

数据库等设计

上面的设计是基础的逻辑,下面我们继续细化逻辑。其中为了支持更好的性能和分布式做了一些其他的方案设计。

根据逻辑自行设计是完全可以的????

数据库设计

我们选择 MySQL 作为基础数据库,负责存储每次发布之后的数据保存。

fe_bundle表存储的是每次发布的 bundle 信息,主要分 3 个部分:

  1. 表本身需要的数据。id、状态、操作人、发布说明。
  2. 判断是否更新的依据字段。版本号、平台、客户端 id、bundle 的签名、地址、压缩包的地址。
  3. 作为附加数据在接口返回的。标签 id、标签内容。

fe_labels表就是作为附加数据存储的。如果想要在接口上返回一些复杂的操作,比如显示隐藏某个界面、是否加载某个 bundle、是否强制更新等,都可以在这里设置。这个表本身只支持添加和是否启用,不支持删除,防止误操作。

根据实际情况减少字段的长度可以优化数据库的查询性能。比如昵称的长度不会超过 10 个字符。
大量数据的情况下添加索引也会提高数据库性能。查询的时候只查询需要的字段也可以减少查询的时间。

发布订阅设计

使用发布订阅模式主要是为了同步每次发布的结果。这样做可以解耦发布和本地缓存更新,多个服务器支持也不会出现资源争夺或者更新不及时的情况。

这里使用的是 redis 的发布订阅模式,可以选的其他方案有 MQ 的消息队列等方式。在收到消息的时候主动更新本地缓存。

本地缓存设计

接口响应速度快不快的关键就是在本地缓存这里了。毕竟在用户大量访问的情况下,一个数据库是非常难支撑的。这里利用本地缓存减少数据库的查询,不管是面对多少用户,实际在工作的就只有接口所在的服务器线程。而且这里利用了 nodejs 的高并发优势,只要机器抗的住,我们的服务就不会卡顿或者挂掉。服务能支持的并发数几乎等于机器支持的并发数。

  1. 本地缓存的优点就是查询速度快,没有网络请求的消耗。
  2. 在遇到缓存没有的情况下,去数据库读取数据并缓存在本地。
  3. 使用双缓存,避免多个请求来临的情况下并发打垮数据库。
  4. 双缓存只是应对特殊情况,比如本地缓存失效、服务器重启等情况下的大量请求。正常情况下发布订阅已经解决了本地缓存的问题。

前台界面开发

前台界面使用 React+Mobx+ElementUI 实现。这里选择这个技术栈主要是为了方便,毕竟会 RN 的开发者大概率是可以很快上手 React 的。

  1. React 作为基础框架,利用框架的优势快速开发。
  2. Mobx 作为状态管理,这次项目中只利用到了用户信息的全局管理。
  3. ElementUI 的几个 UI 还不错,这里利用现成的 UI 开发,剩下大量的设计精力。
登录界面

登录只需要简单的一个背景 + 登录信息输入框即可。有兴趣的可以优化一下,让界面更好看。

这里利用 Mobx 将用户的登录信息保存在全局缓存中。这个设计比较简陋,在公司内部用一下还可以了。如果是开发给更多人用一定要完善一下,把用户鉴权做的更安全一些。

bundle 管理界面

列表管理只需要显示关键信息即可。列出查询的几个参数,方便查询。在点击删除的时候要弹出是否删除的提示,点击发布的时候也需要弹出提示。

编辑的时候给出几个固定选项。如果是灰度的时候还能够选择不同的手机品牌、灰度的比例。如果是白名单模式,需要填入白名单对应的 clientid。

标签管理

标签的核心就是添加和使用。在添加的时候定义好添加的字段和值类型。只需要一次添加即可完成。客户端兼容????️值情况下的兼容就好了。

后端接口开发

接口分 2 个部分,一部分是应对后台的编辑列表等接口,另外一个部分是应对大量用户的查询接口。

编辑查询接口

接口开发其实非常简单,如果对数据库使用不熟练的可以看看相应的文档或者教程。
sequelize 简单教程

接口开发 3 个步骤:

  1. 获取请求的参数。这里最好添加默认值处理,异常校验。
  2. 查询数据库。处理正常返回和 catch 报错的 2 种情况。
  3. 按照约定的规范返回具体的内容。

这里约定,返回 status=0 是查询成功,所有数据放在 data 字段里。
返回 status=1 代表查询失败,错误信息放在 msg 字段里。


查询接口

查询接口分 2 个线程,一个线程是网络请求线程,管理来访的网络请求和筛选返回。另外一个线程管理本地更新,通过 redis 的订阅模式触发对应的数据更新。

缓存更新

当 redis 通知到需要更新的时候会带上版本号、平台的数据库。我们本地缓存也是由这 2 个字段作为 key 缓存的。

searchFromService这个方法主要是从数据库拿对应的数据列表,并且在拿到数据之后手工把数据分为 3 个部分,分别用来处理白名单、灰度、全量的数据。他们对应的返回也是 N 个白名单、N 个灰度、1 个全量数据。

网络请求

网络请求逻辑较复杂,需要首先从缓存中拿数据,同时可能触发数据库拿数据并处理到缓存中,备份缓存拿数据并返回。

数据来源确定之后就开始分阶段筛选。

  1. 筛选是否存在合适的白名单数据。
  2. 筛选是否存在合适的灰度数据
  3. 判断对应的全量数据是否存在。

以上判断全部完成之后就可以知道本次请求是否有合适的 bundle 了。没有的话客户端也不需要更新。用户可以正常打开并浏览。

判断灰度的时候 clientid 中可能会带字母。这情况下需要将字母转为数据再判断。
这里的转化是简单的字母数字对应,具体表现就是百分比前移。前 60% 的用户量会大于后 40% 的用户量。如果对这个有要求的可以按照 26 进制转 10 进制的方式转化数据。拿到的就是真实的百分比了。

源代码地址

前台页面地址:前台代码

后台接口地址:后台代码

数据库地址:数据库代码

退出移动版