熱更新是一個非常方便的方案。在應對大量使用者和深度定製的時候一定不能使用開源的方案。
一般第三方的這種方案,伺服器頻寬較小,或者不夠靈活,不能滿足自己的想法。
這裡推薦自己實現對應的熱更新方案。只需要少量程式碼即可支援。
下面推薦一種靈活的熱更新方案。包括客戶端的改造、介面設計、介面開發,同時是開源的!可以自由改造。
體驗地址: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();
}
}
});
}
系統設計方案
首先來看一下我們是怎樣設計客戶端獲取更新邏輯的。
- 客戶端請求的時候會帶上版本號、平臺2個重要資訊。
- 介面拿到請求之後查詢對應的本地快取,沒有則去資料庫查詢。
- 從查詢結果中篩查對應的3段資料:白名單、灰度、全量,判斷順序從左到右。
- 返回查詢之後對應的結果。
資料庫等設計
上面的設計是基礎的邏輯,下面我們繼續細化邏輯。其中為了支援更好的效能和分散式做了一些其他的方案設計。
根據邏輯自行設計是完全可以的?
資料庫設計
我們選擇MySQL作為基礎資料庫,負責儲存每次釋出之後的資料儲存。fe_bundle
表儲存的是每次釋出的bundle資訊,主要分3個部分:
- 表本身需要的資料。id、狀態、操作人、釋出說明。
- 判斷是否更新的依據欄位。版本號、平臺、客戶端id、bundle的簽名、地址、壓縮包的地址。
- 作為附加資料在介面返回的。標籤id、標籤內容。
fe_labels
表就是作為附加資料儲存的。如果想要在介面上返回一些複雜的操作,比如顯示隱藏某個介面、是否載入某個bundle、是否強制更新等,都可以在這裡設定。這個表本身只支援新增和是否啟用,不支援刪除,防止誤操作。
根據實際情況減少欄位的長度可以優化資料庫的查詢效能。比如暱稱的長度不會超過10個字元。
大量資料的情況下新增索引也會提高資料庫效能。查詢的時候只查詢需要的欄位也可以減少查詢的時間。
釋出訂閱設計
使用釋出訂閱模式主要是為了同步每次釋出的結果。這樣做可以解耦釋出和本地快取更新,多個伺服器支援也不會出現資源爭奪或者更新不及時的情況。
這裡使用的是redis的釋出訂閱模式,可以選的其他方案有MQ的訊息佇列等方式。在收到訊息的時候主動更新本地快取。
本地快取設計
介面響應速度快不快的關鍵就是在本地快取這裡了。畢竟在使用者大量訪問的情況下,一個資料庫是非常難支撐的。這裡利用本地快取減少資料庫的查詢,不管是面對多少使用者,實際在工作的就只有介面所在的伺服器執行緒。而且這裡利用了nodejs的高併發優勢,只要機器抗的住,我們的服務就不會卡頓或者掛掉。服務能支援的併發數幾乎等於機器支援的併發數。
- 本地快取的優點就是查詢速度快,沒有網路請求的消耗。
- 在遇到快取沒有的情況下,去資料庫讀取資料並快取在本地。
- 使用雙快取,避免多個請求來臨的情況下併發打垮資料庫。
- 雙快取只是應對特殊情況,比如本地快取失效、伺服器重啟等情況下的大量請求。正常情況下發布訂閱已經解決了本地快取的問題。
前臺介面開發
前臺介面使用React+Mobx+ElementUI實現。這裡選擇這個技術棧主要是為了方便,畢竟會RN的開發者大概率是可以很快上手React的。
- React作為基礎框架,利用框架的優勢快速開發。
- Mobx作為狀態管理,這次專案中只利用到了使用者資訊的全域性管理。
- ElementUI的幾個UI還不錯,這裡利用現成的UI開發,剩下大量的設計精力。
登入介面
登入只需要簡單的一個背景+登入資訊輸入框即可。有興趣的可以優化一下,讓介面更好看。
這裡利用Mobx將使用者的登入資訊儲存在全域性快取中。這個設計比較簡陋,在公司內部用一下還可以了。如果是開發給更多人用一定要完善一下,把使用者鑑權做的更安全一些。
bundle管理介面
列表管理只需要顯示關鍵資訊即可。列出查詢的幾個引數,方便查詢。在點選刪除的時候要彈出是否刪除的提示,點選發布的時候也需要彈出提示。
編輯的時候給出幾個固定選項。如果是灰度的時候還能夠選擇不同的手機品牌、灰度的比例。如果是白名單模式,需要填入白名單對應的clientid。
標籤管理
標籤的核心就是新增和使用。在新增的時候定義好新增的欄位和值型別。只需要一次新增即可完成。客戶端相容?️值情況下的相容就好了。
後端介面開發
介面分2個部分,一部分是應對後臺的編輯列表等介面,另外一個部分是應對大量使用者的查詢介面。
編輯查詢介面
介面開發其實非常簡單,如果對資料庫使用不熟練的可以看看相應的文件或者教程。
sequelize簡單教程
介面開發3個步驟:
- 獲取請求的引數。這裡最好新增預設值處理,異常校驗。
- 查詢資料庫。處理正常返回和catch報錯的2種情況。
- 按照約定的規範返回具體的內容。
這裡約定,返回status=0
是查詢成功,所有資料放在data
欄位裡。
返回status=1
代表查詢失敗,錯誤資訊放在msg
欄位裡。
查詢介面
查詢介面分2個執行緒,一個執行緒是網路請求執行緒,管理來訪的網路請求和篩選返回。另外一個執行緒管理本地更新,通過redis的訂閱模式觸發對應的資料更新。
快取更新
當redis通知到需要更新的時候會帶上版本號、平臺的資料庫。我們本地快取也是由這2個欄位作為key快取的。searchFromService
這個方法主要是從資料庫拿對應的資料列表,並且在拿到資料之後手工把資料分為3個部分,分別用來處理白名單、灰度、全量的資料。他們對應的返回也是N個白名單、N個灰度、1個全量資料。
網路請求
網路請求邏輯較複雜,需要首先從快取中拿資料,同時可能觸發資料庫拿資料並處理到快取中,備份快取拿資料並返回。
資料來源確定之後就開始分階段篩選。
- 篩選是否存在合適的白名單資料。
- 篩選是否存在合適的灰度資料
- 判斷對應的全量資料是否存在。
以上判斷全部完成之後就可以知道本次請求是否有合適的bundle了。沒有的話客戶端也不需要更新。使用者可以正常開啟並瀏覽。
判斷灰度的時候clientid中可能會帶字母。這情況下需要將字母轉為資料再判斷。
這裡的轉化是簡單的字母數字對應,具體表現就是百分比前移。前60%的使用者量會大於後40%的使用者量。如果對這個有要求的可以按照26進位制轉10進位制的方式轉化資料。拿到的就是真實的百分比了。
原始碼地址
前臺頁面地址:前臺程式碼
後臺介面地址:後臺程式碼
資料庫地址:資料庫程式碼