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

列表管理只需要顯示關鍵信息即可。列出查詢的幾個參數,方便查詢。在點擊刪除的時候要彈出是否刪除的提示,點擊發布的時候也需要彈出提示。
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進制的方式轉化數據。拿到的就是真實的百分比了。

源代碼地址

前臺頁面地址:前臺代碼

後臺接口地址:後臺代碼

數據庫地址:數據庫代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章