[TOC]
# uniapp熱更新和整包更新
我們知道,在打包Android App之前,我們需要先通過HX生成打包資源。如果是通過cli創建的項目,則通過以下命令生成打包資源:
```
yarn build:app-plus
```
生成打包資源后的目錄長這樣:

然后將整個目錄中的所有文件拷貝到Android項目的 `assets/apps/<appid>/www` 中:

可以看出,所有生成的文件,其實只是一個資源目錄。
熱更新的原理就是:**替換資源目錄中的所有打包資源**
## 熱更新包分析
我們通過HX生成的熱更新包:

生成的熱更新包長這樣:

可以看出,wgt其實就是一個壓縮文件,將生成的資源文件全部打包。
知道原理后,我們就不一定需要通過HX創建wgt了,我們可以使用`yarn build:app-plus`命令先生成打包資源目錄,再將其壓縮為zip包,修改擴展名為wgt即可:

注意到我兩次都將`manifest.json`圈紅,目的是強調:**wgt包中,必須將manifest,json所在路徑當做根節點進行打包**。
打完包后,我們可以將其上傳到OSS。
## 熱更新方案
熱更新方案:通過增加當前APP資源的版本號(versionCode),跟上一次打包時的APP資源版本號進行對比,如果比之前的資源版本號高,即進行熱更新。
熱更新原理:uniapp的熱更新,其實是將build后的APP資源,打包為一個zip壓縮包(擴展名改為wgt)。
涉及到的版本信息文件:
- src/manifest.json
- app.json (自己創建,用于版本對比)
- platforms/android/app/build.gradle
注意事項:
保證以上文件的`versionName`和`versionCode`均保持一致。
## 熱更新核心代碼
以下為熱更新的核心代碼:
```js
// #ifdef APP-PLUS
let downloadPath = "https://xxx.cn/apk/app.wgt"
uni.downloadFile({
url: downloadPath,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
plus.runtime.install(downloadResult.tempFilePath, {
force: true // 強制更新
}, function() {
console.log('install success...');
plus.runtime.restart();
}, function(e) {
console.error(e);
console.error('install fail...');
});
}
}
})
// #endif
```
這里是下載wgt包,并進行安裝的代碼。以上代碼無論如何都會下載wgt進行安裝。
## 更新接口
實際上,在這之前,我們還需要判斷是否需要更新,這就涉及到接口的部分。在此,只講講思路:
1. 獲取安裝的版本名、版本號等信息,將其當做參數調用對應的更新接口;
2. 接口取到這些信息,與最新版本進行對比,如果版本已經更新,返回需要更新的信息;
3. 接口可以自行約定,怎么方便這么來。
我自己做的話,根本沒寫什么接口,只是創建了一個`app.json`文件,用于存放最新版本信息:
```json
{
"versionCode": "100",
"versionName": "1.0.0"
}
```
將其上傳到OSS,然后在下載wgt包之前進行版本檢查即可:
```js
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
console.log(widgetInfo);
uni.request({
url: 'https://xxx.cn/apk/app.json',
success: (result) => {
let { versionCode, versionName } = result.data
console.log({ versionCode, versionName });
// 判斷版本名是否一致
if (versionName === widgetInfo.version) {
// 如果安裝的版本號小于最新發布的版本號,則進行更新
if (parseInt(widgetInfo.versionCode) < parseInt(versionCode)) {
// 下載wgt更新包
let downloadPath = "https://xxx.cn/apk/app.wgt"
uni.downloadFile({
url: downloadPath,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
plus.runtime.install(downloadResult.tempFilePath, {
force: true // 強制更新
}, function() {
console.log('熱更新成功');
plus.runtime.restart();
}, function(e) {
console.error('熱更新失敗,錯誤原因:' + e);
});
}
}
})
} else {
console.log('你的版本為最新,不需要熱更新');
}
} else {
console.log('版本名不一致,請使用整包更新');
}
}
});
});
// #endif
```
OK,至此,熱更新就完成了。
## Android整包更新
看到上面更新邏輯,如果版本名不一致,則需要下載最新的apk進行安裝,在下載之前,建議給用戶一個更新提示:
```js
console.log('版本名不一致,請使用整包更新');
let url = "https://xxx.cn/apk/app.apk"
uni.showModal({ //提醒用戶更新
title: "更新提示",
content: "有新的更新可用,請升級",
success: (res) => {
if (res.confirm) {
plus.runtime.openURL(url);
}
}
})
```
以上代碼是官方提供的,其實也可以下載apk成功后,直接調用`install`進行安裝:
```js
console.log('版本名不一致,請使用整包更新');
let downloadPath = "https://zys201811.boringkiller.cn/shianonline/apk/app.apk"
uni.showModal({ //提醒用戶更新
title: "更新提示",
content: "有新的更新可用,請升級",
success: (res) => {
if (res.confirm) {
// plus.runtime.openURL(downloadPath);
uni.downloadFile({
url: downloadPath,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
console.log('正在更新...');
plus.runtime.install(downloadResult.tempFilePath, {
force: true // 強制更新
}, function() {
console.log('整包更新成功');
plus.runtime.restart();
}, function(e) {
console.error('整包更新失敗,錯誤原因:' + e);
});
}
}
})
}
}
})
```
## 熱更新的自動化處理
知道原理后,就好辦了,我們可以將其繁雜的工作自動化,以減少重復勞動。
修改`package.json`的相關打包腳本:
```json
{
"name": "shianaonline",
"version": "0.1.224",
"private": true,
"scripts": {
"apk": "node deploy/scripts/build-apk.js",
"wgt": "node deploy/scripts/build-wgt.js",
"build:app-plus-android": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus UNI_OUTPUT_DIR=./platforms/android/app/src/main/assets/apps/your appid/www vue-cli-service uni-build",
"build:app-plus-ios": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus UNI_OUTPUT_DIR=./platforms/iOS/apps/your appid/www vue-cli-service uni-build",
}
}
```
其中,需要替換的地方是`your appid`,換為自己的uniapp appid
創建`app.json`,用于存儲當前app的版本信息:
```json
{
"versionName": "1.0.27",
"versionCode": 336,
"appPath": "https://xxx.oss.com/apk/app-release.apk",
"wgtPath": "https://xxx.oss.com/apk/www.wgt"
}
```
創建自動化打包腳本`build-wgt.js`:
```js
const fs = require('fs')
const { execSync } = require('child_process')
const join = require('path').join
// 修改版本號
let app = require('../../app.json')
let manifest = require('../../src/manifest.json')
if (app.versionName !== manifest.versionName) {
console.info('manifest.json和app.json的versionName不一致,請檢查')
return
}
if (app.versionCode !== manifest.versionCode) {
console.info('manifest.json和app.json的versionCode不一致,請檢查')
return
}
// 獲取build.gradle的版本名
let gradleFilePath = '../../platforms/android/app/build.gradle'
let data = fs.readFileSync(__dirname + '/' + gradleFilePath, {
encoding: 'utf-8'
})
let reg = new RegExp(`versionCode ${app.versionCode}`, "gm")
if (!reg.test(data)) {
console.log('platforms/android/app/build.gradle的versionCode不一致,請檢查')
return
}
app.versionCode += 1
manifest.versionCode += 1
console.log('====================');
console.log('newVersion:' + app.versionName + "." + app.versionCode);
console.log('====================');
let appJSON = JSON.stringify(app, null, 2)
let manifestJSON = JSON.stringify(manifest, null, 2)
let replaceFiles = [{
path: '../../app.json',
name: 'app.json',
content: appJSON
}, {
path: '../../src/manifest.json',
name: 'manifest.json',
content: manifestJSON
}]
replaceFiles.forEach(file => {
fs.writeFileSync(__dirname + '/' + file.path, file.content, {
encoding: 'utf-8'
})
console.log(file.name + ': 替換成功');
})
// 替換build.gradle的版本名
let result = data.replace(reg, `versionCode ${app.versionCode}`)
fs.writeFileSync(__dirname + '/' + gradleFilePath, result, {
encoding: 'utf-8'
})
console.log('platforms/android/build.gradle: 替換成功')
console.log('====================');
// 編譯
console.log(execSync('yarn build:app-plus-android', { encoding: 'utf-8'}))
// 打包
const compressing = require('compressing');
const tarStream = new compressing.zip.Stream();
const targetPath = './platforms/android/app/src/main/assets/apps/your appid/www'
const targetFile = './www.wgt'
let paths = fs.readdirSync(targetPath);
paths.forEach(function (item) {
let fPath = join(targetPath, item);
tarStream.addEntry(fPath);
});
tarStream
.pipe(fs.createWriteStream(targetFile))
.on('finish', upToOss)
// 上傳至OSS
let OSS = require('ali-oss');
function upToOss() {
let client = new OSS({
region: 'oss-cn-shenzhen',
accessKeyId: 'your accessKeyId',
accessKeySecret: 'your accessKeySecret'
});
client.useBucket('your bucketName');
let ossBasePath = `apk`
put(`${ossBasePath}/www.wgt`, 'www.wgt')
put(`${ossBasePath}/wgts/${app.versionCode}/www.wgt`, 'www.wgt')
put(`webview/vod.html`, 'src/hybrid/html/vod.html')
put(`${ossBasePath}/app.json`, 'app.json')
async function put (ossPath, localFile) {
try {
await client.put(ossPath, localFile);
console.log(`${localFile}上傳成功:${ossPath}`);
} catch (err) {
console.log(err);
}
}
}
console.log('====================');
console.log('更新完畢,newVersion:' + app.versionName + "." + app.versionCode);
console.log('====================');
```
以上打包腳本,做了以下工作:
1. 驗證版本號和版本名是否正確,如果不正確,終止腳本
2. 修改當前APP版本號
3. 生成APP打包資源
4. 將打包資源做成zip包(擴展名改為wgt)
5. 上傳wgt資源包到OSS
一鍵式操作,打包為wgt只需要執行:
```
yarn wgt
```
## Android整包更新的自動化處理
Android整包更新需要在`AndroidManifest.xml`中配置:
```xml
<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
```
Android整包更新的業務代碼跟熱更新一樣,都可以調用`plus.runtime.install`來實現。
主要還是說一下打包apk的自動化腳本`build-apk.js`
```js
const fs = require('fs')
const { execSync } = require('child_process')
let app = require('../../app.json')
let manifest = require('../../src/manifest.json')
if (app.versionName !== manifest.versionName) {
console.log('manifest.json和app.json的versionName不一致,請檢查')
return
}
if (app.versionCode !== manifest.versionCode) {
console.log('manifest.json和app.json的versionCode不一致,請檢查')
return
}
// 獲取build.gradle的版本名
let gradleFilePath = '../../platforms/android/app/build.gradle'
let data = fs.readFileSync(__dirname + '/' + gradleFilePath, {
encoding: 'utf-8'
})
let reg = new RegExp(`versionName "${app.versionName}"`, "gm")
if (!reg.test(data)) {
console.info('platforms/android/app/build.gradle的versionName不一致,請檢查')
return
}
let regCode = new RegExp(`versionCode ${app.versionCode}`, "gm")
if (!regCode.test(data)) {
console.info('platforms/android/app/build.gradle的versionCode不一致,請檢查')
return
}
// 修改版本名
let appVersionName = app.versionName.split('.')
let manifestVersionName = manifest.versionName.split('.')
let appVersionLast = Number(appVersionName[2])
let manifestVersionLast = Number(manifestVersionName[2])
appVersionLast += 1
manifestVersionLast += 1
app.versionName = appVersionName[0] + '.' + appVersionName[1] + '.' + appVersionLast
manifest.versionName = manifestVersionName[0] + '.' + manifestVersionName[1] + '.' + manifestVersionLast
console.log('====================');
console.log('newVersion:' + app.versionName + "." + app.versionCode);
console.log('====================');
let appJSON = JSON.stringify(app, null, 2)
let manifestJSON = JSON.stringify(manifest, null, 2)
// 替換項目版本名
let replaceFiles = [{
path: '../../app.json',
name: 'app.json',
content: appJSON
}, {
path: '../../src/manifest.json',
name: 'manifest.json',
content: manifestJSON
}]
replaceFiles.forEach(file => {
fs.writeFileSync(__dirname + '/' + file.path, file.content, {
encoding: 'utf-8'
})
console.log(file.name + ': 替換成功');
})
// 替換build.gradle的版本名
let result = data.replace(reg, `versionName "${app.versionName}"`)
fs.writeFileSync(__dirname + '/' + gradleFilePath, result, {
encoding: 'utf-8'
})
console.log('platforms/android/build.gradle: 替換成功')
console.log('====================');
// 打包資源
console.log(execSync(`yarn build:app-plus-android`, { encoding: 'utf-8'}))
// 打包apk
console.log(execSync(`cd platforms/android && gradle assembleRelease`, { encoding: 'utf-8'}))
// 上傳至OSS
let OSS = require('ali-oss');
function upToOss() {
let client = new OSS({
region: 'oss-cn-shenzhen',
accessKeyId: 'your accessKeyId',
accessKeySecret: 'your accessKeySecret'
});
client.useBucket('your bucketName');
let ossBasePath = `apk`
put(`${ossBasePath}/app-release.apk`, 'platforms/android/app/build/outputs/apk/release/app-release.apk')
put(`${ossBasePath}/apks/${app.versionName}/app-release.apk`, 'platforms/android/app/build/outputs/apk/release/app-release.apk')
put(`${ossBasePath}/apks/${app.versionName}/output.json`, 'platforms/android/app/build/outputs/apk/release/output.json')
put(`webview/vod.html`, 'src/hybrid/html/vod.html')
put(`${ossBasePath}/app.json`, 'app.json')
async function put (ossPath, localFile) {
try {
await client.put(ossPath, localFile);
console.log(`${localFile}上傳成功:${ossPath}`);
} catch (err) {
console.log(err);
}
}
}
upToOss()
console.log('====================');
console.log('更新完畢,newVersion:' + app.versionName + "." + app.versionCode);
console.log('====================');
```
以上打包腳本,做了以下工作:
1. 驗證版本號和版本名是否正確,如果不正確,終止腳本
2. 修改當前APP版本名
3. 生成APP打包資源
4. 打包Android APP(擴展名apk)
5. 上傳apk到OSS
一鍵式操作,打包為apk只需要執行:
```
yarn apk
```
## 安裝更新
我們看看`plus.runtime.install`的官方文檔:
```js
void plus.runtime.install(filePath, options, installSuccessCB, installErrorCB);
```
支持以下類型安裝包:
1. 應用資源安裝包(wgt),擴展名為'.wgt';
2. 應用資源差量升級包(wgtu),擴展名為'.wgtu';
3. 系統程序安裝包(apk),要求使用當前平臺支持的安裝包格式。 注意:僅支持本地地址,調用此方法前需把安裝包從網絡地址或其他位置放置到運行時環境可以訪問的本地目錄。
知道了調用方式就好辦了,我們封裝一個檢測更新的方法:
```js
class Utils {
...
// 獲取APP版本信息
getVersion() {
let {versionName, versionCode} = manifest
return {
versionName,
versionCode,
version: `${versionName}.${versionCode}`
}
}
// 檢測更新
detectionUpdate(needRestartHotTip = false, needRestartFullTip = false) {
return new Promise(async (resolve, reject) => {
let appInfo = this.getVersion()
uni.request({
url: 'https://xxx.oss.com/apk/app.json',
success: async (result) => {
let { versionCode, versionName, appPath, wgtPath } = result.data
let versionInfo = {
appPath,
wgtPath,
newestVersion: `${versionName}.${versionCode}`,
newestVersionCode: versionCode,
newestVersionName: versionName,
currentVersion: appInfo.version,
currentVersionCode: appInfo.versionCode,
currentVersionName: appInfo.versionName
}
// 判斷版本名是否一致
try {
if (versionName === appInfo.versionName) {
// 如果安裝的版本號小于最新發布的版本號,則進行更新
if (appInfo.versionCode < versionCode) {
// 下載wgt更新包
if (needRestartHotTip) {
uni.showModal({
title: '提示',
content: `檢測到新版本 ${versionInfo.newestVersion} (當前版本:${versionInfo.currentVersion}),是否立即更新并重啟應用,以使更新生效?`,
success: async (res) => {
if (res.confirm) {
await this.downloadAndInstallPackage(wgtPath)
plus.runtime.restart();
resolve({code: 1, data: versionInfo})
} else if (res.cancel) {
await this.downloadAndInstallPackage(wgtPath)
resolve({code: 1, data: versionInfo})
}
}
})
} else {
await this.downloadAndInstallPackage(wgtPath)
resolve({code: 1, data: versionInfo})
}
} else {
resolve({code: 0, data: versionInfo})
console.log('你的版本為最新,不需要熱更新');
}
} else {
// 整包更新
console.log('版本名不一致,請使用整包更新');
if (needRestartFullTip) {
uni.showModal({
title: '提示',
content: `檢測到新版本 ${versionInfo.newestVersion} (當前版本:${versionInfo.currentVersion}),是否立即更新應用?`,
success: async (res) => {
if (res.confirm) {
// await this.downloadAndInstallPackage(appPath)
plus.runtime.openURL(appPath)
resolve({code: 2, data: versionInfo})
} else if (res.cancel) {}
}
})
} else {
// await this.downloadAndInstallPackage(appPath)
plus.runtime.openURL(appPath)
resolve({code: 2, data: versionInfo})
}
}
} catch (e) {
reject(e)
}
}
});
})
}
// 下載并安裝更新包
downloadAndInstallPackage(url) {
console.log('開始下載更新包:' + url)
return new Promise((resolve, reject) => {
uni.downloadFile({
url: url,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
console.log('正在更新...');
plus.runtime.install(downloadResult.tempFilePath, {
force: true // 強制更新
}, function() {
console.log('更新成功');
resolve()
}, function(e) {
console.error('更新失敗,錯誤原因:' + JSON.stringify(e));
reject(e)
});
}
}
})
})
}
}
...
```
創建Utils的實例,并掛載到Vue的原型中,調用起來非常方便:
```js
...
let res = await this.$utils.detectionUpdate(false, true)
if (res.code === 1) {
uni.showModal({
title: '提示',
content: `發現新的熱更新包,是否立即重啟APP以使更新生效?`,
success: async (res) => {
if (res.confirm) {
plus.runtime.restart()
} else if (res.cancel) {}
}
})
}
```
```js
...
let res = await this.$utils.detectionUpdate(true, true)
if (res.code === 0) {
let {currentVersion} = res.data
uni.showModal({
title: '提示',
content: `你的APP為最新版本 ${currentVersion},不需要更新!`,
showCancel: false,
success: async (res) => {
if (res.confirm) {
} else if (res.cancel) {}
}
})
}
```
## 參考資料
- [uni-app 資源在線升級/熱更新](https://ask.dcloud.net.cn/article/35667)
- [uni-app 整包升級/更新方案](https://ask.dcloud.net.cn/article/34972)
- [app升級項目,新增強制更新(可靜默),支持熱更新(wgt),可支持高版本安卓系統](https://ext.dcloud.net.cn/plugin?id=237)
- [Android平臺云端打包權限配置](https://ask.dcloud.net.cn/article/36982)
- [H5+:plus.runtime.install](http://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.install)
- uniapp項目搭建
- 通過cli創建uniapp項目
- uniapp平臺特性
- uniapp基礎
- 在uniapp中使用字體圖標
- uniapp全局變量的幾種實現方式
- uniapp自定義頁面返回邏輯
- uniapp進階
- 在網頁中打開uniapp應用
- uniapp狀態欄與導航欄
- 在uniapp中優雅地使用WebView
- uniapp Android離線打包
- Android原生工程搭建
- 在uni-app項目中集成Android原生工程
- uniapp熱更新和整包更新
- Android Q啟動白屏的問題
- uniapp原生插件開發與使用
- Android 原生插件使用
- uniapp基礎模塊配置
- uniapp定位及地圖
- uniapp第三方支付、登錄
- 常見問題及解決方案
- Android端常見問題解決方案
- H5端常見問題解決方案
- 微信小程序常見問題解決方案