前提
- 通過react-native官方腳手架初始化項目
- 項目內(nèi)集成react-native-code-push8.1.0版本
本文的源碼主要以js為主另萤,iOS為輔,沒有Android代碼宗苍。原生代碼旁涤,僅僅停留在原生模塊暴露給js使用的方法。這種方法名iOS和Android都是同名的,Android去源碼搜索同名方法即可蠢琳,不影響對本文的理解
流程概括
- 1、檢查更新
- 2镜豹、下載更新
- 3傲须、安裝更新
- 4、埋點上報邏輯
檢查更新
起點:
在js端發(fā)起趟脂,js在頁面入口調(diào)用 CodePush.sync()
泰讽。
// 自調(diào)用函數(shù),返回值是一個函數(shù)昔期。當(dāng)外面調(diào)用時已卸,即調(diào)用的當(dāng)前函數(shù)返回的函數(shù)
const sync = (() => {
// 通過一個標(biāo)記,用來標(biāo)識當(dāng)前是否在更新過程中
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
// 返回一個函數(shù)
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
// 下述代碼都是一些校驗相關(guān)的邏輯硼一,可以適當(dāng)略過
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
// 前述代碼都是一些校驗相關(guān)的邏輯累澡,可以適當(dāng)略過
// 修改標(biāo)記,開始進入更新的過程
syncInProgress = true;
// 核心邏輯都在這個函數(shù)里
// 先生成一個promise
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
// 然后調(diào)用這個promise函數(shù)般贼,這個函數(shù)里實現(xiàn)了核心邏輯
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
概括一下這個函數(shù)的邏輯如下:
- 添加是否在更新的過程中的標(biāo)記
- 進入更新過程中
- 把整個更新的邏輯愧哟,包裝成一個promise,更新結(jié)束后修改標(biāo)記,表示不在更新過程中
- 然后把這個promise返回去
通過上面的梳理哼蛆,可以看到核心邏輯都在syncPromise
中蕊梧,接下來我們看一下syncPromise
做了什么。函數(shù)的實現(xiàn)在syncInternal
中
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
let resolvedInstallMode;
// 聲明更新相關(guān)的配置信息
const syncOptions = {
deploymentKey: null,
ignoreFailedUpdates: true,
rollbackRetryOptions: null,
installMode: CodePush.InstallMode.ON_NEXT_RESTART,
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
minimumBackgroundDuration: 0,
updateDialog: null,
...options
};
// 更新過程中的回調(diào)腮介,通過該回調(diào)可以觀察更新狀態(tài)的變更肥矢,如果外部沒有指定,優(yōu)先使用內(nèi)置的邏輯
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
? syncStatusChangeCallback
: (syncStatus) => {
// 每次狀態(tài)發(fā)生變化叠洗,都會有日志輸出
switch(syncStatus) {
// 1甘改、檢查更新
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
log("Checking for update.");
break;
// 2、等待用戶操作
case CodePush.SyncStatus.AWAITING_USER_ACTION:
log("Awaiting user action.");
break;
// 3灭抑、下載文件
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
log("Downloading package.");
break;
// 4十艾、安裝更新
case CodePush.SyncStatus.INSTALLING_UPDATE:
log("Installing update.");
break;
// 5、更新完成
case CodePush.SyncStatus.UP_TO_DATE:
log("App is up to date.");
break;
// 6名挥、用戶取消更新
case CodePush.SyncStatus.UPDATE_IGNORED:
log("User cancelled the update.");
break;
// 7疟羹、更新安裝完畢
case CodePush.SyncStatus.UPDATE_INSTALLED:
// 8主守、下次啟動的時候,應(yīng)用更新內(nèi)容
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
log("Update is installed and will be run on the next app restart.");
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
// 9榄融、應(yīng)用進去后臺的若干秒后参淫,應(yīng)用更新內(nèi)容
if (syncOptions.minimumBackgroundDuration > 0) {
log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
} else {
// 10、下次resumes的時候愧杯,應(yīng)用更新內(nèi)容
log("Update is installed and will be run when the app next resumes.");
}
}
break;
// 11涎才、出現(xiàn)未知錯誤
case CodePush.SyncStatus.UNKNOWN_ERROR:
log("An unknown error occurred.");
break;
}
};
try {
// 發(fā)個通知出去,這個通知的本質(zhì)是一個上報力九,上報服務(wù)器本地的一些部署的信息耍铜。
await CodePush.notifyApplicationReady();
// 修改狀態(tài)為檢查更新
syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
// 檢查更新的邏輯,返回值是新版本的信息跌前。這個信息里通過mixin的方式添加了download的函數(shù)棕兼,便于后續(xù)下載更新
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
// 聲明一個函數(shù),內(nèi)部包含下載更新并安裝的邏輯
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
// 下載更新抵乓,下載成功后伴挚,本次更新就變成了本地包,所以返回值是本地包內(nèi)容(本地更新的內(nèi)容)內(nèi)部會添加install的函數(shù)灾炭,便于后續(xù)安裝
const localPackage = await remotePackage.download(downloadProgressCallback);
// Determine the correct install mode based on whether the update is mandatory or not.
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
// 修改狀態(tài)為安裝更新
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
// 安裝更新茎芋, 第三個參數(shù)是安裝成功的回調(diào),安裝成功后蜈出,修改狀態(tài)為已安裝
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};
// 判斷當(dāng)前的更新是否要被忽略
const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);
if (!remotePackage || updateShouldBeIgnored) {
if (updateShouldBeIgnored) {
log("An update is available, but it is being ignored due to having been previously rolled back.");
}
// 獲取本地包的信息
const currentPackage = await CodePush.getCurrentPackage();
if (currentPackage && currentPackage.isPending) {
// 修改狀態(tài)為正在安裝中
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
return CodePush.SyncStatus.UPDATE_INSTALLED;
} else {
// 修改狀態(tài)為已更新到最新版本
syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
return CodePush.SyncStatus.UP_TO_DATE;
}
} else if (syncOptions.updateDialog) {
// 如果需要展示dialog田弥,讓用戶來操作如何更新
// updateDialog supports any truthy value (e.g. true, "goo", 12),
// but we should treat a non-object value as just the default dialog
if (typeof syncOptions.updateDialog !== "object") {
syncOptions.updateDialog = CodePush.DEFAULT_UPDATE_DIALOG;
} else {
syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
}
return await new Promise((resolve, reject) => {
let message = null;
let installButtonText = null;
const dialogButtons = [];
if (remotePackage.isMandatory) {
message = syncOptions.updateDialog.mandatoryUpdateMessage;
installButtonText = syncOptions.updateDialog.mandatoryContinueButtonLabel;
} else {
message = syncOptions.updateDialog.optionalUpdateMessage;
installButtonText = syncOptions.updateDialog.optionalInstallButtonLabel;
// Since this is an optional update, add a button
// to allow the end-user to ignore it
dialogButtons.push({
text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
onPress: () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
resolve(CodePush.SyncStatus.UPDATE_IGNORED);
}
});
}
// Since the install button should be placed to the
// right of any other button, add it last
dialogButtons.push({
text: installButtonText,
onPress:() => {
doDownloadAndInstall()
.then(resolve, reject);
}
})
// If the update has a description, and the developer
// explicitly chose to display it, then set that as the message
if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
}
syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
});
} else {
// 開始下載更新并安裝
return await doDownloadAndInstall();
}
} catch (error) {
syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
log(error.message);
throw error;
}
};
概括一下上述邏輯:
- 聲明更新相關(guān)的配置信息
syncOptions
- 聲明不同狀態(tài)的回調(diào)函數(shù):通過這個回調(diào)函數(shù),可以發(fā)現(xiàn)铡原,整個更新過程包括多個階段(檢查更新偷厦、下載更新、安裝更新眷蜈、應(yīng)用更新)其中在應(yīng)用更新有多個時機可供選擇(重啟app生效沪哺、應(yīng)用進去后臺n秒后生效沈自、resumes時生效)
- 發(fā)個上報酌儒,上報部署的信息,如果之前已經(jīng)下載了最新版本枯途,沒有應(yīng)用忌怎,在第一次應(yīng)用的時候,會上報一次酪夷,后續(xù)不會上報
- 開始檢查更新
const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);
- 如果沒有更新或這個更新被忽略榴啸,直接獲取當(dāng)前本地包的信息
const currentPackage = await CodePush.getCurrentPackage();
,然后檢查本地包是正在安裝更新中晚岭,還是已更新到最新 - 如果需要彈出dialog鸥印,就需要讓用戶來操作如何更新
- 如果有新版本且不需要彈出dialog,則自動開始下載更新并安裝
await doDownloadAndInstall()
,下載成功的時候库说,還會有一個下載成功的上報狂鞋。
接下來我們需要分別分一下各個關(guān)鍵環(huán)節(jié)的邏輯
- 檢查更新
- 獲取本地包信息
- 下載并安裝更新
一、檢查更新
async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
/*
* 在我們詢問服務(wù)器是否存在更新之前潜的,
* 我們需要從本機端檢索三項信息:部署密鑰骚揍、應(yīng)用程序版本(例如 1.0.1)和當(dāng)前正在運行的更新的哈希值(如果有)。
* 這允許客戶端僅接收針對其特定部署和版本且實際上與其已安裝的 CodePush 更新不同的更新啰挪。
*/
// 從本地獲取配置信息
const nativeConfig = await getConfiguration();
/*
* 如果顯式提供了部署密鑰信不,
* 那么讓我們覆蓋從應(yīng)用程序本機端檢索到的部署密鑰。
* 這允許在不同的部署中動態(tài)“重定向”最終用戶(例如內(nèi)部人員的早期訪問部署)亡呵。
*/
const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
const sdk = getPromisifiedSdk(requestFetchAdapter, config);
// Use dynamically overridden getCurrentPackage() during tests.
const localPackage = await module.exports.getCurrentPackage();
/*
* 如果應(yīng)用程序之前安裝了更新抽活,
* 并且該更新針對當(dāng)前正在運行的同一應(yīng)用程序版本,
* 那么我們希望使用其包哈希來確定服務(wù)器上是否已發(fā)布新版本锰什。
* 否則酌壕,我們只需將應(yīng)用程序版本發(fā)送到服務(wù)器,
* 因為我們對當(dāng)前二進制版本的任何更新感興趣歇由,而不管哈希值如何卵牍。
*/
let queryPackage;
if (localPackage) {
queryPackage = localPackage;
} else {
queryPackage = { appVersion: config.appVersion };
if (Platform.OS === "ios" && config.packageHash) {
queryPackage.packageHash = config.packageHash;
}
}
// 檢查更新的核心邏輯,如果有更新沦泌,此時update就有值
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
/*
* checkForUpdate 有四種情況會解析為 null:
* ------------------------------------------------- ----------------
* 1) 服務(wù)器說沒有更新糊昙。 這是最常見的情況。
* 2) 服務(wù)器表示有更新谢谦,但需要更新的二進制版本(native app版本)释牺。
* 當(dāng)最終用戶運行比可用版本更舊的二進制版本時,就會發(fā)生這種情況回挽,
* 并且 CodePush 正在確保他們不會獲得可能無法獲得的更新 與他們正在運行的內(nèi)容不兼容没咙。
* 3) 服務(wù)器說有更新,但更新的哈希值與當(dāng)前運行的更新相同千劈。
* 這應(yīng)該永遠(yuǎn)不會發(fā)生祭刚,除非服務(wù)器中存在錯誤,
* 但我們添加此檢查只是為了仔細(xì)檢查客戶端應(yīng)用程序是否能夠應(yīng)對更新檢查的潛在問題墙牌。
* 4) 服務(wù)器說有更新涡驮,但更新的哈希值與二進制文件當(dāng)前運行版本的哈希值相同。
* 這應(yīng)該只發(fā)生在 Android 中 -
* 與 iOS 不同喜滨,我們不會將二進制文件的哈希附加到 updateCheck 請求捉捅,
* 因為我們希望避免必須針對二進制版本安裝差異更新,而這在 Android 上還無法做到虽风。
*/
if (!update || update.updateAppVersion ||
localPackage && (update.packageHash === localPackage.packageHash) ||
(!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {
if (update && update.updateAppVersion) {
log("An update is available but it is not targeting the binary version of your app.");
if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
handleBinaryVersionMismatchCallback(update)
}
}
return null;
} else {
// 生成remotePackage棒口,會把download方法添加進去寄月,也會添加下載成功的回調(diào)
const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
// 然后加一個安裝失敗的回調(diào),這個回調(diào)會直接調(diào)用到Native的邏輯
remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
// 把key修改為顯示指定的key或者是從native獲取到的key
remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
return remotePackage;
}
}
上述的邏輯概括如下:
- 從native獲取配置信息
const nativeConfig = await getConfiguration();
- 獲取本地的currentPackage信息
const localPackage = await module.exports.getCurrentPackage();
- 通過本地信息生成網(wǎng)絡(luò)請求的參數(shù)
- 檢查遠(yuǎn)程服務(wù)器上是否有更新
const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);
.沒有更新的四種情況:1无牵、遠(yuǎn)程沒有更新剥懒。2、遠(yuǎn)程有更新合敦,但是本地app版本過低初橘。3、遠(yuǎn)程有更新充岛,但是和當(dāng)前本地最新版本的內(nèi)容一致保檐。4、遠(yuǎn)程有更新崔梗,但是和本地內(nèi)置的內(nèi)容hash一致夜只。這種只會發(fā)生在Android。 - 如果有更新的話蒜魄,以遠(yuǎn)程更新的信息為基礎(chǔ)扔亥,再添加幾個字段,然后返回出去谈为。添加的內(nèi)容主要是下載方法和下載成功的回調(diào)旅挤,以及是否正在下載的標(biāo)記
1、從native獲取配置
const getConfiguration = (() => {
let config;
return async function getConfiguration() {
if (config) {
return config;
} else if (testConfig) {
return testConfig;
} else {
config = await NativeCodePush.getConfiguration();
return config;
}
}
})();
優(yōu)先使用緩存伞鲫,如果沒有緩存數(shù)據(jù)粘茄,使用測試數(shù)據(jù),沒有測試數(shù)據(jù)秕脓,則從native獲取config = await NativeCodePush.getConfiguration();
iOS:
RCT_EXPORT_METHOD(getConfiguration:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSDictionary *configuration = [[CodePushConfig current] configuration];
NSError *error;
if (isRunningBinaryVersion) {
// isRunningBinaryVersion will not get set to "YES" if running against the packager.
NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error];
if (error) {
CPLog(@"Error obtaining hash for binary contents: %@", error);
resolve(configuration);
return;
}
if (binaryHash == nil) {
// The hash was not generated either due to a previous unknown error or the fact that
// the React Native assets were not bundled in the binary (e.g. during dev/simulator)
// builds.
resolve(configuration);
return;
}
NSMutableDictionary *mutableConfiguration = [configuration mutableCopy];
[mutableConfiguration setObject:binaryHash forKey:PackageHashKey];
resolve(mutableConfiguration);
return;
}
resolve(configuration);
}
Android
2柒瓣、獲取本地的currentPackage信息
async function getCurrentPackage() {
return await getUpdateMetadata(CodePush.UpdateState.LATEST);
}
async function getUpdateMetadata(updateState) {
let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
if (updateMetadata) {
updateMetadata = {...PackageMixins.local, ...updateMetadata};
updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
}
return updateMetadata;
}
上述邏輯概括:
- 從native獲取
updateMetadata
- 然后給這個
updateMetadata
添加一些回調(diào)函數(shù):安裝失敗的回調(diào)、第一次運行的回調(diào)
對應(yīng)到原生的代碼如下:
/*
* This method is the native side of the CodePush.getUpdateMetadata method.
*/
RCT_EXPORT_METHOD(getUpdateMetadata:(CodePushUpdateState)updateState
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy];
if (error) {
return reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
} else if (package == nil) {
// The app hasn't downloaded any CodePush updates yet,
// so we simply return nil regardless if the user
// wanted to retrieve the pending or running update.
return resolve(nil);
}
// We have a CodePush update, so let's see if it's currently in a pending state.
BOOL currentUpdateIsPending = [[self class] isPendingUpdate:[package objectForKey:PackageHashKey]];
if (updateState == CodePushUpdateStatePending && !currentUpdateIsPending) {
// The caller wanted a pending update
// but there isn't currently one.
resolve(nil);
} else if (updateState == CodePushUpdateStateRunning && currentUpdateIsPending) {
// The caller wants the running update, but the current
// one is pending, so we need to grab the previous.
resolve([CodePushPackage getPreviousPackage:&error]);
} else {
// The current package satisfies the request:
// 1) Caller wanted a pending, and there is a pending update
// 2) Caller wanted the running update, and there isn't a pending
// 3) Caller wants the latest update, regardless if it's pending or not
if (isRunningBinaryVersion) {
// This only matters in Debug builds. Since we do not clear "outdated" updates,
// we need to indicate to the JS side that somehow we have a current update on
// disk that is not actually running.
[package setObject:@(YES) forKey:@"_isDebugOnly"];
}
// Enable differentiating pending vs. non-pending updates
[package setObject:@(currentUpdateIsPending) forKey:PackageIsPendingKey];
resolve(package);
}
}
iOS
/*
* This method isn't publicly exposed via the "react-native-code-push"
* module, and is only used internally to populate the RemotePackage.failedInstall property.
*/
RCT_EXPORT_METHOD(isFailedUpdate:(NSString *)packageHash
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
BOOL isFailedHash = [[self class] isFailedHash:packageHash];
resolve(@(isFailedHash));
}
/*
* This method isn't publicly exposed via the "react-native-code-push"
* module, and is only used internally to populate the LocalPackage.isFirstRun property.
*/
RCT_EXPORT_METHOD(isFirstRun:(NSString *)packageHash
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
BOOL isFirstRun = _isFirstRunAfterUpdate
&& nil != packageHash
&& [packageHash length] > 0
&& [packageHash isEqualToString:[CodePushPackage getCurrentPackageHash:&error]];
resolve(@(isFirstRun));
}
Android
3吠架、調(diào)用接口從服務(wù)端獲取更新信息
queryUpdateWithCurrentPackage
的實現(xiàn)如下:(代碼在code-push這個包里芙贫,不在react-native-code-push這個包里)
AcquisitionManager.prototype.queryUpdateWithCurrentPackage = function (currentPackage, callback) {
var _this = this;
if (!currentPackage || !currentPackage.appVersion) {
throw new code_push_error_1.CodePushPackageError("Calling common acquisition SDK with incorrect package"); // Unexpected; indicates error in our implementation
}
// 拼裝請求的參數(shù)
var updateRequest = {
deployment_key: this._deploymentKey,
app_version: currentPackage.appVersion,
package_hash: currentPackage.packageHash,
is_companion: this._ignoreAppVersion,
label: currentPackage.label,
client_unique_id: this._clientUniqueId
};
// 生成請求的URL
var requestUrl = this._serverUrl + this._publicPrefixUrl + "update_check?" + queryStringify(updateRequest);
// 發(fā)送網(wǎng)絡(luò)請求
this._httpRequester.request(0 /* Http.Verb.GET */, requestUrl, function (error, response) {
if (error) {
callback(error, /*remotePackage=*/ null);
return;
}
if (response.statusCode !== 200) {
var errorMessage = void 0;
if (response.statusCode === 0) {
errorMessage = "Couldn't send request to ".concat(requestUrl, ", xhr.statusCode = 0 was returned. One of the possible reasons for that might be connection problems. Please, check your internet connection.");
}
else {
errorMessage = "".concat(response.statusCode, ": ").concat(response.body);
}
callback(new code_push_error_1.CodePushHttpError(errorMessage), /*remotePackage=*/ null);
return;
}
try {
var responseObject = JSON.parse(response.body);
// 解析數(shù)據(jù)
var updateInfo = responseObject.update_info;
}
catch (error) {
callback(error, /*remotePackage=*/ null);
return;
}
if (!updateInfo) {
callback(error, /*remotePackage=*/ null);
return;
}
else if (updateInfo.update_app_version) {
callback(/*error=*/ null, { updateAppVersion: true, appVersion: updateInfo.target_binary_range });
return;
}
else if (!updateInfo.is_available) {
callback(/*error=*/ null, /*remotePackage=*/ null);
return;
}
// 生成remotePackage
var remotePackage = {
deploymentKey: _this._deploymentKey,
description: updateInfo.description,
label: updateInfo.label,
appVersion: updateInfo.target_binary_range,
isMandatory: updateInfo.is_mandatory,
packageHash: updateInfo.package_hash,
packageSize: updateInfo.package_size,
downloadUrl: updateInfo.download_url
};
callback(/*error=*/ null, remotePackage);
});
};
概括如下:
- 組裝參數(shù)
- 發(fā)送請求
- 解析數(shù)據(jù)
- 組裝出remotePackage,返回回去
二傍药、下載并安裝更新
此處邏輯對應(yīng)的doDownloadAndInstall
的實現(xiàn)
const doDownloadAndInstall = async () => {
syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
// 下載邏輯
const localPackage = await remotePackage.download(downloadProgressCallback);
// Determine the correct install mode based on whether the update is mandatory or not.
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
// 安裝邏輯
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
});
return CodePush.SyncStatus.UPDATE_INSTALLED;
};
概括如下:
- 下載更新
- 安裝更新
1磺平、 下載更新
對應(yīng)
const localPackage = await remotePackage.download(downloadProgressCallback);
的邏輯。
實際的download
實現(xiàn)如下:
async download(downloadProgressCallback) {
if (!this.downloadUrl) {
throw new Error("Cannot download an update without a download url");
}
let downloadProgressSubscription;
// 添加下載進度的監(jiān)聽
if (downloadProgressCallback) {
const codePushEventEmitter = new NativeEventEmitter(NativeCodePush);
// Use event subscription to obtain download progress.
downloadProgressSubscription = codePushEventEmitter.addListener(
"CodePushDownloadProgress",
downloadProgressCallback
);
}
// Use the downloaded package info. Native code will save the package info
// so that the client knows what the current package version is.
try {
const updatePackageCopy = Object.assign({}, this);
Object.keys(updatePackageCopy).forEach((key) => (typeof updatePackageCopy[key] === 'function') && delete updatePackageCopy[key]);
// 調(diào)用原生方法怔檩,實現(xiàn)下載功能
const downloadedPackage = await NativeCodePush.downloadUpdate(updatePackageCopy, !!downloadProgressCallback);
// 下載成功的上報
if (reportStatusDownload) {
reportStatusDownload(this)
.catch((err) => {
log(`Report download status failed: ${err}`);
});
}
// 次數(shù)的返回值褪秀,除了會把downloadedPackage信息放進來以為,還會添加install函數(shù)薛训,以及是否正在install的標(biāo)記。下一步可直接調(diào)用install
return { ...downloadedPackage, ...local };
} finally {
downloadProgressSubscription && downloadProgressSubscription.remove();
}
},
概括如下:
- 添加對下載進度監(jiān)聽
- 把參數(shù)傳給原生端仑氛,原生實現(xiàn)下載功能
- 當(dāng)下載完畢后乙埃,上報一下下載完成
- 最后把原生的返回值闸英,添加install方法后,返回出去
iOS
/*
* This is native-side of the RemotePackage.download method
*/
RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
notifyProgress:(BOOL)notifyProgress
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSDictionary *mutableUpdatePackage = [updatePackage mutableCopy];
NSURL *binaryBundleURL = [CodePush binaryBundleURL];
if (binaryBundleURL != nil) {
[mutableUpdatePackage setValue:[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL]
forKey:BinaryBundleDateKey];
}
if (notifyProgress) {
// Set up and unpause the frame observer so that it can emit
// progress events every frame if the progress is updated.
_didUpdateProgress = NO;
self.paused = NO;
}
NSString * publicKey = [[CodePushConfig current] publicKey];
[CodePushPackage
downloadPackage:mutableUpdatePackage
expectedBundleFileName:[bundleResourceName stringByAppendingPathExtension:bundleResourceExtension]
publicKey:publicKey
operationQueue:_methodQueue
// The download is progressing forward
progressCallback:^(long long expectedContentLength, long long receivedContentLength) {
// Update the download progress so that the frame observer can notify the JS side
_latestExpectedContentLength = expectedContentLength;
_latestReceivedConentLength = receivedContentLength;
_didUpdateProgress = YES;
// If the download is completed, stop observing frame
// updates and synchronously send the last event.
if (expectedContentLength == receivedContentLength) {
_didUpdateProgress = NO;
self.paused = YES;
[self dispatchDownloadProgressEvent];
}
}
// The download completed
doneCallback:^{
NSError *err;
NSDictionary *newPackage = [CodePushPackage getPackage:mutableUpdatePackage[PackageHashKey] error:&err];
if (err) {
return reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}
resolve(newPackage);
}
// The download failed
failCallback:^(NSError *err) {
if ([CodePushErrorUtils isCodePushError:err]) {
[self saveFailedUpdate:mutableUpdatePackage];
}
// Stop observing frame updates if the download fails.
_didUpdateProgress = NO;
self.paused = YES;
reject([NSString stringWithFormat: @"%lu", (long)err.code], err.localizedDescription, err);
}];
}
Android
2馒吴、安裝更新
對應(yīng)
await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => { syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED); });
實際的install
實現(xiàn)如下:
async install(installMode = NativeCodePush.codePushInstallModeOnNextRestart, minimumBackgroundDuration = 0, updateInstalledCallback) {
const localPackage = this;
const localPackageCopy = Object.assign({}, localPackage); // In dev mode, React Native deep freezes any object queued over the bridge
// 調(diào)用原生方法炫贤,實現(xiàn)install的邏輯
await NativeCodePush.installUpdate(localPackageCopy, installMode, minimumBackgroundDuration);
updateInstalledCallback && updateInstalledCallback();
// 根據(jù)生效時機的配置益缎,操作如何生效
if (installMode == NativeCodePush.codePushInstallModeImmediate) {
// 如果是立即生效,則調(diào)用原生方法辙喂,立刻生效
NativeCodePush.restartApp(false);
} else {
// 如果不是立即生效,也是調(diào)用原生方法鸠珠,做一些數(shù)據(jù)的清除操作
NativeCodePush.clearPendingRestart();
localPackage.isPending = true; // Mark the package as pending since it hasn't been applied yet
}
},
概括:
- 把相關(guān)參數(shù)傳給原生端巍耗,原生端實現(xiàn)install功能
- 安裝完畢后,根據(jù)生效時機渐排,調(diào)用不同的后續(xù)處理邏輯
- 如果需要立即生效炬太,則調(diào)用原生模塊提供的
restartApp
方法 - 如果不需要立刻生效,則調(diào)用原生模塊提供的
clearPendingRestart
方法驯耻,清除沒用的數(shù)據(jù)
iOS
/*
* This method is the native side of the LocalPackage.install method.
*/
RCT_EXPORT_METHOD(installUpdate:(NSDictionary*)updatePackage
installMode:(CodePushInstallMode)installMode
minimumBackgroundDuration:(int)minimumBackgroundDuration
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSError *error;
[CodePushPackage installPackage:updatePackage
removePendingUpdate:[[self class] isPendingUpdate:nil]
error:&error];
if (error) {
reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
} else {
[self savePendingUpdate:updatePackage[PackageHashKey]
isLoading:NO];
_installMode = installMode;
if (_installMode == CodePushInstallModeOnNextResume || _installMode == CodePushInstallModeOnNextSuspend) {
_minimumBackgroundDuration = minimumBackgroundDuration;
if (!_hasResumeListener) {
// Ensure we do not add the listener twice.
// Register for app resume notifications so that we
// can check for pending updates which support "restart on resume"
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive)
name:UIApplicationDidBecomeActiveNotification
object:RCTSharedApplication()];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:RCTSharedApplication()];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive)
name:UIApplicationWillResignActiveNotification
object:RCTSharedApplication()];
_hasResumeListener = YES;
}
}
// Signal to JS that the update has been applied.
resolve(nil);
}
}
Android
數(shù)據(jù)上報
除了文件的下載安裝等路邏輯以外亲族,還需要注意一些關(guān)鍵信息的上報,當(dāng)前主要是兩個節(jié)點
- 下載成功: 表示本地已經(jīng)將服務(wù)器上的更新可缚,下載到本地霎迫。
- 部署成功: 表示本地下載的更新內(nèi)容已經(jīng)生效。
下載成功
AcquisitionManager.prototype.reportStatusDownload = function (downloadedPackage, callback) {
var url = this._serverUrl + this._publicPrefixUrl + "report_status/download";
var body = {
client_unique_id: this._clientUniqueId,
deployment_key: this._deploymentKey,
label: downloadedPackage.label
};
this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
if (callback) {
if (error) {
callback(error, /*not used*/ null);
return;
}
if (response.statusCode !== 200) {
callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
return;
}
callback(/*error*/ null, /*not used*/ null);
}
});
};
部署成功
AcquisitionManager.prototype.reportStatusDeploy = function (deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey, callback) {
var url = this._serverUrl + this._publicPrefixUrl + "report_status/deploy";
var body = {
app_version: this._appVersion,
deployment_key: this._deploymentKey
};
if (this._clientUniqueId) {
body.client_unique_id = this._clientUniqueId;
}
if (deployedPackage) {
body.label = deployedPackage.label;
body.app_version = deployedPackage.appVersion;
switch (status) {
case AcquisitionStatus.DeploymentSucceeded:
case AcquisitionStatus.DeploymentFailed:
body.status = status;
break;
default:
if (callback) {
if (!status) {
callback(new code_push_error_1.CodePushDeployStatusError("Missing status argument."), /*not used*/ null);
}
else {
callback(new code_push_error_1.CodePushDeployStatusError("Unrecognized status \"" + status + "\"."), /*not used*/ null);
}
}
return;
}
}
if (previousLabelOrAppVersion) {
body.previous_label_or_app_version = previousLabelOrAppVersion;
}
if (previousDeploymentKey) {
body.previous_deployment_key = previousDeploymentKey;
}
callback = typeof arguments[arguments.length - 1] === "function" && arguments[arguments.length - 1];
this._httpRequester.request(2 /* Http.Verb.POST */, url, JSON.stringify(body), function (error, response) {
if (callback) {
if (error) {
callback(error, /*not used*/ null);
return;
}
if (response.statusCode !== 200) {
callback(new code_push_error_1.CodePushHttpError(response.statusCode + ": " + response.body), /*not used*/ null);
return;
}
callback(/*error*/ null, /*not used*/ null);
}
});
};
上報的本質(zhì)就是調(diào)用api帘靡,發(fā)送一次請求女气,把一些參數(shù)傳給服務(wù)器,服務(wù)器會做好記錄测柠,便于統(tǒng)計炼鞠。