uniapp热更新和整包更新思路
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
我们知道,在打包Android App之前,我们需要先通过HX生成打包资源。如果是通过cli创建的项目,则通过以下命令生成打包资源:
yarn build:app-plus
生成打包资源后的目录长这样:
然后将整个目录中的所有文件拷贝到Android项目的 assets/apps/<appid>/www
中:
可以看出,所有生成的文件,其实只是一个资源目录。
热更新的原理就是:替换资源目录中的所有打包资源
热更新包分析
我们通过HX生成的热更新包:
生成的热更新包长这样:
可以看出,wgt其实就是一个压缩文件,将生成的资源文件全部打包。
知道原理后,我们就不一定需要通过HX创建wgt了,我们可以使用yarn build:app-plus
命令先生成打包资源目录,再将其压缩为zip包,修改扩展名为wgt即可
注意:wgt包中,必须将manifest,json所在路径当做根节点进行打包。
打完包后,我们可以将其上传到OSS。
热更新方案
热更新方案:通过增加当前APP资源的版本号(versionCode),跟上一次打包时的APP资源版本号进行对比,如果比之前的资源版本号高,即进行热更新。
热更新原理:uniapp的热更新,其实是将build后的APP资源,打包为一个zip压缩包(扩展名改为wgt)。
涉及到的版本信息文件:
- src/manifest.json
- app.json (自己创建,用于版本对比)
- platforms/android/app/build.gradle
注意事项:
保证以上文件的versionName
和versionCode
均保持一致。
热更新核心代码
以下为热更新的核心代码:
// #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进行安装。
更新接口
实际上,在这之前,我们还需要判断是否需要更新,这就涉及到接口的部分。在此,只讲讲思路:
- 获取安装的版本名、版本号等信息,将其当做参数调用对应的更新接口;
- 接口取到这些信息,与最新版本进行对比,如果版本已经更新,返回需要更新的信息;
- 接口可以自行约定,怎么方便这么来。
我自己做的话,根本没写什么接口,只是创建了一个app.json
文件,用于存放最新版本信息:
{ "versionCode": "100", "versionName": "1.0.0" }
将其上传到OSS,然后在下载wgt包之前进行版本检查即可:
// #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进行安装,在下载之前,建议给用户一个更新提示:
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
进行安装:
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
的相关打包脚本:
{ "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的版本信息:
{ "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
:
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('====================');
以上打包脚本,做了以下工作:
- 验证版本号和版本名是否正确,如果不正确,终止脚本
- 修改当前APP版本号
- 生成APP打包资源
- 将打包资源做成zip包(扩展名改为wgt)
- 上传wgt资源包到OSS
一键式操作,打包为wgt只需要执行:
yarn wgt
Android整包更新的自动化处理
Android整包更新需要在AndroidManifest.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
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('====================');
以上打包脚本,做了以下工作:
- 验证版本号和版本名是否正确,如果不正确,终止脚本
- 修改当前APP版本名
- 生成APP打包资源
- 打包Android APP(扩展名apk)
- 上传apk到OSS
一键式操作,打包为apk只需要执行:
yarn apk
安装更新
我们看看plus.runtime.install
的官方文档:
void plus.runtime.install(filePath, options, installSuccessCB, installErrorCB);
支持以下类型安装包:
- 应用资源安装包(wgt),扩展名为'.wgt';
- 应用资源差量升级包(wgtu),扩展名为'.wgtu';
- 系统程序安装包(apk),要求使用当前平台支持的安装包格式。 注意:仅支持本地地址,调用此方法前需把安装包从网络地址或其他位置放置到运行时环境可以访问的本地目录。
知道了调用方式就好办了,我们封装一个检测更新的方法:
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的原型中,调用起来非常方便:
... 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) {} } }) }
... 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) {} } }) }
实战案例代码及过程
思路
服务器中存储着最新版本号,前端进行查询 可以在首次进入应用时进行请求版本号进行一个匹对 如果版本号一致则不提示,反之则提示进行更新执行更新操作
1.封装一个对比版本号的函数
/** * 对比版本号,如需要,请自行修改判断规则 * 支持比对 ("3.0.0.0.0.1.0.1", "3.0.0.0.0.1") ("3.0.0.1", "3.0") ("3.1.1", "3.1.1.1") 之类的 * @param {Object} v1 * @param {Object} v2 * v1 > v2 return 1 * v1 < v2 return -1 * v1 == v2 return 0 */ function compare(v1 = '0', v2 = '0') { v1 = String(v1).split('.') v2 = String(v2).split('.') const minVersionLens = Math.min(v1.length, v2.length); let result = 0; for (let i = 0; i < minVersionLens; i++) { const curV1 = Number(v1[i]) const curV2 = Number(v2[i]) if (curV1 > curV2) { result = 1 break; } else if (curV1 < curV2) { result = -1 break; } } if (result === 0 && (v1.length !== v2.length)) { const v1BiggerThenv2 = v1.length > v2.length; const maxLensVersion = v1BiggerThenv2 ? v1 : v2; for (let i = minVersionLens; i < maxLensVersion.length; i++) { const curVersion = Number(maxLensVersion[i]) if (curVersion > 0) { v1BiggerThenv2 ? result = 1 : result = -1 break; } } } return result; }
2.封装更新函数
var updateUseModal = (packageInfo) => { const { title, // 标题 contents, // 升级内容 is_mandatory, // 是否强制更新 url, // 安装包下载地址 platform, // 安装包平台 type // 安装包类型 } = packageInfo; let isWGT = type === 'wgt' let isiOS = !isWGT ? platform.includes('iOS') : false; let confirmText = isiOS ? '立即跳转更新' : '立即下载更新' return uni.showModal({ title, content: contents, showCancel: !is_mandatory, confirmText, success: res => { if (res.cancel) return; // 安装包下载 if (isiOS) { plus.runtime.openURL(url); return; } let waiting = plus.nativeUI.showWaiting("正在下载 - 0%"); // uni.showLoading({ // title: '安装包下载中' // }); // wgt 和 安卓下载更新 const downloadTask = uni.downloadFile({ url, success: res => { if (res.statusCode !== 200) { console.error('下载安装包失败', err); return; } // 下载好直接安装,下次启动生效 plus.runtime.install(res.tempFilePath, { force: false }, () => { uni.hideLoading() if (is_mandatory) { //更新完重启app plus.runtime.restart(); return; } uni.showModal({ title: '安装成功是否重启?', success: res => { if (res.confirm) { //更新完重启app plus.runtime.restart(); } } }); }, err => { uni.hideLoading() uni.showModal({ title: '更新失败', content: err.message, showCancel: false }); }); }, //接口调用结束 complete: ()=>{ uni.hideLoading(); downloadTask.offProgressUpdate();//取消监听加载进度 } }); //监听下载进度 downloadTask.onProgressUpdate(res => { // state.percent = res.progress; waiting.setTitle("正在下载 - "+res.progress+"%"); // console.log('下载进度百分比:' + res.progress); // 下载进度百分比 // console.log('已经下载的数据长度:' + res.totalBytesWritten); // 已经下载的数据长度,单位 Bytes // console.log('预期需要下载的数据总长度:' + res.totalBytesExpectedToWrite); // 预期需要下载的数据总长度,单位 Bytes }); } }); }
3.用变量接收实现函数(在函数中使用上方封装的函数)并导出
fRequestWithToken为我封装的请求方法,可自行进行使用axios进行请求也行!!!
var fCheckVersion = (cb) => { // #ifdef APP-PLUS plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) { // console.log(widgetInfo.version) // console.log(plus.runtime.version) // console.log(widgetInfo.version) var nVerSta = compare(plus.runtime.version, widgetInfo.version), sLaststVer = plus.runtime.version; if (widgetInfo.version) { if (nVerSta == 1) { console.log(plus.runtime.version) sLaststVer = plus.runtime.version } else if (nVerSta == -1) { console.log(widgetInfo.version) sLaststVer = widgetInfo.version } } console.log(sLaststVer) //发送请求进行匹对,我这里数据库设定的是如果返回null则版本号一致,反之需要更新!!! fRequestWithToken({ ajaxOpts: { url: URLS_COM.d_lastVer, data: { versionCode: sLaststVer } }, showloading: false, silence:true }).then(data => { console.log(data) // console.log('################') if (data) { var sUrl = '', type = ''; if (data.wgtName) { sUrl = data.wgtName; type = "wgt" } else { sUrl = data.pkgName; type = "pkg"; } updateUseModal({ title: data.title||"", contents: data.note||'', is_mandatory: true, url: sUrl, platform: 'android', type: type // 安装包类型 }) } }).catch((res)=>{ cb&&cb() console.log(res) }) }) // #endif } export { fCheckVersion }
使用
可在App.vue中进行使用,根据项目需求而定
1.引入封装好的函数
路径自己记得填写自己封装的位置
import{fCheckVersion} from '@/common/project/checkversion.js'
2.然后可以在onLoad函数中进行触发
onLoad() { fCheckVersion();//检查更新 }
这样就实现了热更新
然后的话只需要进行打包个热更新的包
后端进行上传至服务器进行更新数据
本地再进行一个云打包,记得在mainifest.json文件中进行版本号的修改,修改成低于热更新包的版本号即可