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

体验地址: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管理界面

列表管理只需要显示关键信息即可。列出查询的几个参数,方便查询。在点击删除的时候要弹出是否删除的提示,点击发布的时候也需要弹出提示。
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进制的方式转化数据。拿到的就是真实的百分比了。

源代码地址

前台页面地址:前台代码

后台接口地址:后台代码

数据库地址:数据库代码


疯狂紫萧
342 声望441 粉丝