ReactNative分布式热更新系统

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

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

关键数据

基础数据的准备和实现

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

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

ios下载的例子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];
  }];
}

安卓下载的例子c++

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();
                        }
                    }
                });
    }

系统设计方案

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

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

数据库等设计

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

根据逻辑自行设计是彻底能够的😁web

数据库设计

咱们选择MySQL做为基础数据库,负责存储每次发布以后的数据保存。
数据库设计
fe_bundle表存储的是每次发布的bundle信息,主要分3个部分:redis

  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管理界面

列表管理只须要显示关键信息便可。列出查询的几个参数,方便查询。在点击删除的时候要弹出是否删除的提示,点击发布的时候也须要弹出提示。
bundle管理
编辑的时候给出几个固定选项。若是是灰度的时候还可以选择不一样的手机品牌、灰度的比例。若是是白名单模式,须要填入白名单对应的clientid。
发布管理

标签管理

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

后端接口开发

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

编辑查询接口

接口开发其实很是简单,若是对数据库使用不熟练的能够看看相应的文档或者教程。
sequelize简单教程

接口开发3个步骤:

  1. 获取请求的参数。这里最好添加默认值处理,异常校验。
  2. 查询数据库。处理正常返回和catch报错的2种状况。
  3. 按照约定的规范返回具体的内容。
这里约定,返回 status=0是查询成功,全部数据放在 data字段里。
返回 status=1表明查询失败,错误信息放在 msg字段里。

接口1
接口2

查询接口

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

缓存更新

当redis通知到须要更新的时候会带上版本号、平台的数据库。咱们本地缓存也是由这2个字段做为key缓存的。
缓存更新
searchFromService这个方法主要是从数据库拿对应的数据列表,而且在拿到数据以后手工把数据分为3个部分,分别用来处理白名单、灰度、全量的数据。他们对应的返回也是N个白名单、N个灰度、1个全量数据。

网络请求

网络请求逻辑较复杂,须要首先从缓存中拿数据,同时可能触发数据库拿数据并处理到缓存中,备份缓存拿数据并返回。
查询缓存
数据来源肯定以后就开始分阶段筛选。

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

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

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

源代码地址

前台页面地址:前台代码

后台接口地址:后台代码

数据库地址:数据库代码

相关文章
相关标签/搜索