APP更新一般有两种形式
1、整包更新,通过hbuliderx提供的在线云打包就属于整包更新,属于全量更新,缺点就是打包时间长、要重新走市场审核。费时
2、wgt资源包热更新,通过hbuliderx打wgt包 ,速度快,能在应用打开就更新,跳过市场审核更新,很方便,但仅更新js代码,无法改变原生配置项
核心流程
热更新的核心流程
1、打wgt包 打的wgt包内的版本号(manifest.json里面的应用版本名称versionName和应用版本号versionCode)需要大于客体应用内的版本号,不然无法安装,会提示版本号匹配
2、触发更新打完最新的wgt包后 将其存放到服务器,应用内置在首页每次进入调接口,查询服务器的wgt的版本号是否大于本地应用版本号,如果大于则触发更新,出现热更新弹窗
3、下载wgt包出现热更新弹窗后,用户点击下载新包,通过uni.downloadFile方法去将wgt包的在线地址下载到本地 拿到临时路径tempFilePath字段
4、安装wgt包拿到临时路径后,接下来就是安装包,通过plus.runtime.install去安装应用包
5、重启生效安装完成 通过plus.runtime.restart 重启即可
代码
请勿直接复制粘贴,我的代码耦合性强 还匹配了自己公司的后端接口,仅供参考。
创建热更新弹窗页面的vue代码
<template>
<view class="mask flex-center">
<view class="content botton-radius">
<view class="content-top">
<image class="content-top" style="top: 0" width="100%" height="100%" src="../images/bg_top.png"/>
<text class="content-top-text">{{ title }}</text>
<view class="content-top-sub">V{{versionNum}}</view>
</view>
<view class="content-header"></view>
<view class="content-body">
<view class="title">
<text>{{ subTitle }}</text>
<!-- <text style="padding-left:20rpx;font-size: 0.5em;color: #666;">v.{{version}}</text> -->
</view>
<view class="body">
<scroll-view class="box-des-scroll" scroll-y="true">
<text class="box-des">
{{ contents || versionDesc }}
</text>
</scroll-view>
</view>
<view class="footer flex-center">
<template v-if="isiOS">
<button class="content-button" style="border: none; color: #fff" plain @click="jumpToAppStore">
{{ downLoadBtnTextiOS }}
</button>
</template>
<template v-else>
<template v-if="!downloadSuccess">
<view class="progress-box flex-column" v-if="downloading">
<progress class="progress" border-radius="35" :percent="downLoadPercent" activeColor="#03bdac" show-info stroke-width="10" />
<view style="width: 100%; font-size: 28rpx; display: flex; justify-content: space-around">
<text>{{ downLoadingText }}</text>
</view>
</view>
<button v-else class="content-button" style="border: none; color: #fff" plain @click="downloadPackage">
{{ downLoadBtnText }}
</button>
</template>
<button
v-else-if="downloadSuccess && !installed"
class="content-button"
style="border: none; color: #fff"
plain
:loading="installing"
:disabled="installing"
@click="installPackage"
>
{{ installing ? '正在安装……' : '下载完成,立即安装' }}
</button>
<button v-if="installed && isWGT" class="content-button" style="border: none; color: #fff" plain @click="restart">安装完毕,点击重启</button>
</template>
</view>
</view>
<image v-if="!is_mandatory" class="close-img" src="../images/app_update_close.png" @click.stop="closeUpdate"></image>
</view>
</view>
</template>
<script>
const localFilePathKey = '__localFilePath__';
const platform_iOS = 'iOS';
let downloadTask = null;
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("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;
}
export default {
data() {
return {
// 从之前下载安装
installForBeforeFilePath: '',
// 安装
installed: false,
installing: false,
// 下载
downloadSuccess: false,
downloading: false,
downLoadPercent: 0,
downloadedSize: 0,
packageFileSize: 0,
tempFilePath: '', // 要安装的本地包地址
// 默认安装包信息
title: '发现新版本',
contents: '',
versionDesc: '',
is_mandatory: false,
// 可自定义属性
subTitle: '更新内容',
downLoadBtnTextiOS: '立即跳转更新',
downLoadBtnText: '立即下载更新',
downLoadingText: '安装包下载中,请稍候...',
versionNum:''
};
},
onLoad({ local_storage_key }) {
if (!local_storage_key) {
console.error('local_storage_key为空,请检查后重试');
uni.navigateBack();
return;
}
const localPackageInfo = uni.getStorageSync(local_storage_key);
if (!localPackageInfo) {
console.error('安装包信息为空,请检查后重试');
uni.navigateBack();
return;
}
const requiredKey = ['versionNum', 'resourceUrl', 'appType']; //版本号 ,资源地址,是否热更新 强制更新
for (let key in localPackageInfo) {
if (requiredKey.indexOf(key) !== -1 && localPackageInfo[key] == '') {
console.error(`参数 ${key} 必填,请检查后重试`);
uni.navigateBack();
return;
}
}
this.is_mandatory = localPackageInfo['isNeedUpdate'];
Object.assign(this, localPackageInfo);
this.checkLocalStoragePackage();
this.versionNum=localPackageInfo['versionNum'];
},
onBackPress() {
// 强制更新不允许返回
if (this.is_mandatory) {
return true;
}
downloadTask && downloadTask.abort();
},
computed: {
isWGT() {
return this.appType === 'wgt';
},
isiOS() {
return !this.isWGT ? plus.os.name.toLocaleLowerCase().includes(platform_iOS) : false;
},
showPrenct(){
return parseInt(this.downLoadPercent)+'%';
},
},
methods: {
checkLocalStoragePackage() {
// 如果已经有下载好的包,则直接提示安装
const localFilePathRecord = uni.getStorageSync(localFilePathKey);
if (localFilePathRecord) {
const { version, savedFilePath, installed } = localFilePathRecord;
// 比对版本
if (!installed && compare(version, this.versionNum) === 0) {
this.downloadSuccess = true;
this.installForBeforeFilePath = savedFilePath;
this.tempFilePath = savedFilePath;
} else {
// 如果保存的包版本小 或 已安装过,则直接删除
this.deleteSavedFile(savedFilePath);
}
}
},
async closeUpdate() {
if (this.downloading) {
if (this.is_mandatory) {
return uni.showToast({
title: '下载中,请稍候……',
icon: 'none',
duration: 500,
});
}
uni.showModal({
title: '是否取消下载?',
cancelText: '否',
confirmText: '是',
success: (res) => {
if (res.confirm) {
downloadTask && downloadTask.abort();
uni.navigateBack();
}
},
});
return;
}
if (this.downloadSuccess && this.tempFilePath) {
// 包已经下载完毕,稍候安装,将包保存在本地
await this.saveFile(this.tempFilePath, this.versionNum);
uni.navigateBack();
return;
}
uni.navigateBack();
},
downloadPackage() {
this.downloading = true;
//下载包
downloadTask = uni.downloadFile({
url: this.resourceUrl,
success: (res) => {
if (res.statusCode == 200) {
this.downloadSuccess = true;
this.tempFilePath = res.tempFilePath;
// 强制更新,直接安装
if (this.is_mandatory) {
this.installPackage();
}
}
},
complete: () => {
this.downloading = false;
this.downLoadPercent = 0;
this.downloadedSize = 0;
this.packageFileSize = 0;
downloadTask = null;
},
});
downloadTask.onProgressUpdate((res) => {
this.downLoadPercent = res.progress;
this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
});
},
installPackage() {
// #ifdef APP-PLUS
// wgt资源包安装
if (this.isWGT) {
this.installing = true;
}
plus.runtime.install(
this.tempFilePath,
{
force: false,
},
async (res) => {
this.installing = false;
this.installed = true;
// wgt包,安装后会提示 安装成功,是否重启
if (this.isWGT) {
// 强制更新安装完成重启
if (this.is_mandatory) {
uni.showLoading({
icon: 'none',
title: '安装成功,正在重启……',
});
setTimeout(() => {
uni.hideLoading();
this.restart();
}, 1000);
}
} else {
const localFilePathRecord = uni.getStorageSync(localFilePathKey);
uni.setStorageSync(localFilePathKey, {
...localFilePathRecord,
installed: true,
});
}
},
async (err) => {
// 如果是安装之前的包,安装失败后删除之前的包
if (this.installForBeforeFilePath) {
await this.deleteSavedFile(this.installForBeforeFilePath);
this.installForBeforeFilePath = '';
}
// 安装失败需要重新下载安装包
this.installing = false;
this.installed = false;
uni.showModal({
title: `更新失败${this.isWGT ? '' : ',APK文件不存在'},请重新下载`,
content: err.message,
showCancel: false,
});
}
);
// 非wgt包,安装跳出覆盖安装,此处直接返回上一页
if (!this.isWGT) {
uni.navigateBack();
}
// #endif
},
restart() {
this.installed = false;
// #ifdef APP-PLUS
//更新完重启app
plus.runtime.restart();
// #endif
},
async saveFile(tempFilePath, version) {
const [err, res] = await uni.saveFile({
tempFilePath,
});
if (err) {
return;
}
uni.setStorageSync(localFilePathKey, {
version,
savedFilePath: res.savedFilePath,
});
},
deleteSavedFile(filePath) {
uni.removeStorageSync(localFilePathKey);
return uni.removeSavedFile({
filePath,
});
},
jumpToAppStore() {
plus.runtime.openURL(this.resourceUrl);
},
},
};
</script>
<style>
page {
background: transparent;
}
.flex-center {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
}
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.65);
}
.botton-radius {
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
}
.content {
position: relative;
top: 0;
width: 618rpx;
background-color: #fff;
box-sizing: border-box;
padding: 0 50rpx;
font-family: Source Han Sans CN;
}
.text {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
line-height: 200px;
text-align: center;
color: #ffffff;
}
.content-top {
position: absolute;
top: -195rpx;
left: 0;
width: 618rpx;
height: 310rpx;
}
.content-top-text {
font-size: 34rpx;
font-weight: 600;
color: #333;
position: absolute;
bottom: 40rpx;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.content-top-sub{
position: absolute;
bottom: 0rpx;
left: 0;
right: 0; font-size: 28rpx;
font-weight: 400;
color: #333; display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.content-header {
height:140rpx;
}
.title {
font-size: 28rpx;
font-weight: bold;
color: #333;
line-height: 38rpx;
}
.footer {
height: 150rpx;
display: flex;
align-items: center;
justify-content: space-around;
}
.box-des-scroll {
box-sizing: border-box;
padding: 0 40rpx 0 0;
height: 200rpx;
text-align: left;
}
.box-des {
font-size: 24rpx;
color: #666;
line-height: 34rpx;
}
.progress-box {
width: 100%;
}
.progress {
width: 90%;
height: 40rpx;
border-radius: 35px;
}
.close-img {
width: 70rpx;
height: 70rpx;
z-index: 1000;
position: absolute;
bottom: -120rpx;
left: calc(50% - 70rpx / 2);
}
.content-button {
text-align: center;
flex: 1;
font-size: 30rpx;
font-weight: 400;
color: #333;
border-radius: 40rpx;
margin: 0 18rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(to right, #03bdac, #03bdac);
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
注册这个热更新弹窗页面
page.json配置
{
"path": "pages/upgrade/pages/upgrade-popup",
"style": {
"disableScroll": true,
"disableSwipeBack": true,
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"disableSwipeBack": true,
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
触发是否弹起热更新弹窗代码(主要是检测当前版本号是否大于热更新版本号)
我将其命名成update.js
import conf from '@/common/config.js';
import api from "@/common/api.js"; //调用API接口
const PACKAGE_INFO_KEY = '__muji__package_info__';
const APP_NAME = conf.conf.APP_NAME
const UPDATE_ROOT = conf.conf.UPDATE_ROOT
export default () => {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.runtime.getProperty(plus.runtime.appid, function (widgetInfo) {
const url = `${UPDATE_ROOT}/rest/queryNewVersion`
const params = {
appName: APP_NAME,
appVersionNum: widgetInfo.versionCode,
operatingSystemType: plus.os.name === 'iOS' ? 1 : 0,
versionNum: widgetInfo.version,
}
console.log(url,params);
api.ajaxApi(url, params, "POST", false)
.then((res) => {
console.log(res);
if (res.code === 200) {
if (res.result && res.result.hasNew) {
let data = Object.assign(res.result, {
platform: plus.os.name,
});
console.log(data);
uni.setStorageSync(PACKAGE_INFO_KEY, data);
uni.navigateTo({
url: `/pages/upgrade/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`,
fail: (err) => {
console.error('更新弹框跳转失败', err);
uni.removeStorageSync(PACKAGE_INFO_KEY);
},
});
} else {
console.log('没有最新更新');
resolve('没有最新更新');
}
} else {
console.log('更新失败', res.message);
reject('error');
}
})
.catch((err) => {
console.log('更新失败异常');
reject('error');
});
});
});
// #endif
};
在首页使用这个方法
import maliCheckUpdate from '@/utils/update';
onLoad() {
setTimeout(() => {
//等待页面加载完成后弹出
maliCheckUpdate()
}, 2000);
},
大功告成
当然我没有贴上后台管理wgt包系统的代码,其本质核心还是通过接口拉取最新的wgt包资源地址,主要工作还是在app上的代码。
能在多个app应用