app热更新是app项目最常见的功能,接下来我总结了当时做这个功能的过程,来交流学习一哈
热更新的流程步骤
- 在用户进入app就获取当前版本号与调用后端接口返回的版本号对比是否是最新的版本
- 不是最新弹出弹窗让用户确认是否更新,点击更新下载wgt资源包更新,重启应用即可
代码流程
1.封装两个判断js文件
- call-check-version.js 判断是否需要更新
import { getVersion } from '@/api/user.js' // 获取最新版本信息的接口
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("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 function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
const data = {
action: 'checkVersion',
appid: plus.runtime.appid,
appVersion: plus.runtime.version, // 当前版本
wgtVersion: widgetInfo.version // wgt版本
}
// console.log("data: ",data);
getVersion().then(res => {
// let {appVersionPackage,app_version}=res.data.list
// let result={
// code:1,
// url:appVersionPackage,
// version:app_version,
// platform:'Android',
// contents:'1.修改头像\n2.分享内容', //更新内容
// is_mandatory:false, //强制更新
// is_silently:0, //静默更新 1是 0否
// type:'apk' //wgt/apk
// }
// console.log(result)
let v = compare(res.data.info.version, data.appVersion)
let w = compare(res.data.info.version, data.wgtVersion)
if (v == 1 && w == 1) {
// 同时大于当前版本和wgt版本,则需要更新
res.data.info.code = 2
} else {
//不用更新
res.data.info.code = -1
}
resolve({ result: res.data.info })
}).catch(() => reject())
// uniCloud.callFunction({
// name: 'uni-upgrade-center',
// data,
// success: (e) => {
// console.log("e: ", e);
// resolve(e)
// },
// fail: (error) => {
// reject(error)
// }
// })
})
})
// #endif
// #ifndef APP-PLUS
return new Promise((resolve, reject) => {
reject({ message: '请在App中使用' })
})
// #endif
}
- check-update.js 更新方式选择(弹窗)
import callCheckVersion from './call-check-version'
// 推荐再App.vue中使用
const PACKAGE_INFO_KEY = '__package_info__'
export default function() {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
callCheckVersion().then(async (e) => {
if (!e.result) return;
const {
code,
is_silently, // 是否静默更新
url, // 安装包下载地址
platform, // 安装包平台
type // 安装包类型
} = e.result;
if(code<0){
reject(null)
return
}
// 此处逻辑仅为实例,可自行编写
resolve(e)
// 静默更新,只有wgt有
if (is_silently) {
uni.downloadFile({
url: e.result.url,
success: res => {
if (res.statusCode == 200) {
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
});
}
}
});
return;
}
/**
* 提示升级一
* 使用 uni.showModal
*/
// return updateUseModal(e.result)
/**
* 提示升级二
* 官方适配的升级弹窗,可自行替换资源适配UI风格
*/
uni.setStorageSync(PACKAGE_INFO_KEY, e.result)
uni.navigateTo({
url: `/uni_modules/pages/upgrade-popup?local_storage_key=${PACKAGE_INFO_KEY}`, // 更新弹窗页面
fail: (err) => {
console.error('更新弹框跳转失败', err)
uni.removeStorageSync(PACKAGE_INFO_KEY)
}
})
return
return resolve(e)
}).catch(err => {
// TODO 云函数报错处理
console.error(err.message)
reject(err)
})
});
// #endif
}
/**
* 使用 uni.showModal 升级
*/
function 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;
}
uni.showToast({
title: '后台下载中……',
duration: 1000
});
// wgt 和 安卓下载更新
downloadTask = uni.downloadFile({
url,
success: res => {
if (res.statusCode !== 200) {
console.error('下载安装包失败', err);
return;
}
// 下载好直接安装,下次启动生效
plus.runtime.install(res.tempFilePath, {
force: false
}, () => {
if (is_mandatory) {
//更新完重启app
plus.runtime.restart();
return;
}
uni.showModal({
title: '安装成功是否重启?',
success: res => {
if (res.confirm) {
//更新完重启app
plus.runtime.restart();
}
}
});
}, err => {
uni.showModal({
title: '更新失败',
content: err
.message,
showCancel: false
});
});
}
});
}
});
}
- 我的目录结构,你们可以根据自己的习惯更改,记得修改文件里的引用路劲
2.弹窗界面效果
- upgrade-popup.vue 弹窗动画
<template>
<view class="mask flex-center">
<view class="content botton-radius">
<view class="content-top">
<text class="content-top-text">{{title}}</text>
<image class="content-top" style="top: 0;" width="100%" height="100%" src="../images/bg_top.png">
</image>
</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}}
</text>
</scroll-view>
</view>
<view class="footer flex-center">
<template v-if="isAppStore">
<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="#3DA7FF" show-info stroke-width="10" />
<view style="width:100%;font-size: 28rpx;display: flex;justify-content: space-around;">
<text>{{downLoadingText}}</text>
<text>({{downloadedSize}}/{{packageFileSize}}M)</text>
</view>
</view>
<button v-else class="content-button" style="border: none;color: #fff;" plain
@click="updateApp">
{{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 = 'UNI_ADMIN_UPGRADE_CENTER_LOCAL_FILE_PATH'
const platform_iOS = 'iOS';
let downloadTask = null;
let openSchemePromise
/**
* 对比版本号,如需要,请自行修改判断规则
* 支持比对 ("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: '',
is_mandatory: false,
// 可自定义属性
subTitle: '发现新版本',
downLoadBtnTextiOS: '立即跳转更新',
downLoadBtnText: '立即下载更新',
downLoadingText: '安装包下载中,请稍后'
}
},
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 = ['version', 'url', 'type']
for (let key in localPackageInfo) {
if (requiredKey.indexOf(key) !== -1 && !localPackageInfo[key]) {
console.error(`参数 ${key} 必填,请检查后重试`)
uni.navigateBack()
return;
}
}
Object.assign(this, localPackageInfo)
this.checkLocalStoragePackage()
},
onBackPress() {
// 强制更新不允许返回
if (this.is_mandatory) {
return true
}
downloadTask && downloadTask.abort()
},
onHide() {
openSchemePromise = null
},
computed: {
isWGT() {
return this.type === 'wgt'
},
isiOS() {
return !this.isWGT ? this.platform.includes(platform_iOS) : false;
},
isAppStore() {
return this.isiOS || (!this.isiOS && !this.isWGT && this.url.indexOf('.apk') === -1)
}
},
methods: {
checkLocalStoragePackage() {
// 如果已经有下载好的包,则直接提示安装
const localFilePathRecord = uni.getStorageSync(localFilePathKey)
if (localFilePathRecord) {
const {
version,
savedFilePath,
installed
} = localFilePathRecord
// 比对版本
if (!installed && compare(version, this.version) === 0) {
this.downloadSuccess = true;
this.installForBeforeFilePath = savedFilePath;
this.tempFilePath = savedFilePath
} else {
// 如果保存的包版本小 或 已安装过,则直接删除
this.deleteSavedFile(savedFilePath)
}
}
},
async closeUpdate() {
console.log('关闭更新')
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.version)
uni.navigateBack()
return;
}
uni.navigateBack()
// this.$navTo('/pages/index/index')
},
updateApp() {
this.checkStoreScheme().catch(() => {
this.downloadPackage()
})
},
// 跳转应用商店
checkStoreScheme() {
const storeList = (this.store_list || []).filter(item => item.enable)
if (storeList && storeList.length) {
storeList
.sort((cur, next) => next.priority - cur.priority)
.map(item => item.scheme)
.reduce((promise, cur, curIndex) => {
openSchemePromise = (promise || (promise = Promise.reject())).catch(() => {
return new Promise((resolve, reject) => {
plus.runtime.openURL(cur, (err) => {
reject(err)
})
})
})
return openSchemePromise
}, openSchemePromise)
return openSchemePromise
}
return Promise.reject()
},
downloadPackage() {
this.downloading = true;
//下载包
downloadTask = uni.downloadFile({
url: this.url,
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: '更新失败,请重新下载',
content: err.message,
showCancel: false
});
});
// 非wgt包,安装跳出覆盖安装,此处直接返回上一页
if (!this.isWGT && !this.is_mandatory) {
uni.navigateBack()
}
// #endif
},
restart() {
this.installed = false;
// #ifdef APP-PLUS
//更新完重启app
plus.runtime.restart();
// #endif
},
saveFile(tempFilePath, version) {
return new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath,
success({
savedFilePath
}) {
uni.setStorageSync(localFilePathKey, {
version,
savedFilePath
})
},
complete() {
resolve()
}
})
})
},
deleteSavedFile(filePath) {
uni.removeStorageSync(localFilePathKey)
return uni.removeSavedFile({
filePath
})
},
jumpToAppStore() {
plus.runtime.openURL(this.url);
}
}
}
</script>
<style lang="scss">
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, .65);
}
.botton-radius {
border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx;
}
.content {
position: relative;
top: 0;
width: 600rpx;
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: 600rpx;
height: 270rpx;
}
.content-top-text {
font-size: 45rpx;
font-weight: bold;
color: #F8F8FA;
position: absolute;
top: 120rpx;
left: 50rpx;
z-index: 1;
}
.content-header {
height: 70rpx;
}
.title {
font-size: 33rpx;
font-weight: bold;
/* #3DA7FF $my-theme-olor */
color: #3DA7FF;
line-height: 38px;
}
.footer {
height: 150rpx;
display: flex;
align-items: center;
justify-content: space-around;
}
.box-des-scroll {
box-sizing: border-box;
padding: 0 40rpx;
height: 200rpx;
text-align: left;
}
.box-des {
font-size: 26rpx;
color: #000000;
line-height: 50rpx;
}
.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: #FFFFFF;
border-radius: 40rpx;
margin: 0 18rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(to right, #1785ff, #3DA7FF);
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
- 需要在pages.json文件中配置路由信息
// ……app更新
{
"path": "uni_modules/pages/upgrade-popup", // 文件路劲
"style": {
"disableScroll": true,
"app-plus": {
"backgroundColorTop": "transparent",
"background": "transparent",
"titleNView": false,
"scrollIndicator": false,
"popGesture": "none",
"animationType": "fade-in",
"animationDuration": 200
}
}
}
3.在App.vue页面的 onLaunch
中调用刚才的方法即可
<script>
import update from '@/uni_modules/utils/check-update.js'
export default {
/**
* 全局变量
*/
globalData: {},
/**
* 初始化完成时触发
*/
onLaunch() {
// 小程序主动更新
this.updateManager()
//app更新
// #ifdef APP-PLUS
update()
// #endif
},
methods: {
/**
* 小程序主动更新
*/
updateManager() {
const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate(res => {
// 请求完新版本信息的回调
// console.log(res.hasUpdate)
})
updateManager.onUpdateReady(() => {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,即将重启应用',
showCancel: false,
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(() => {
// 新的版本下载失败
uni.showModal({
title: '更新提示',
content: '新版本下载失败',
showCancel: false
})
})
}
}
}
</script>
<style lang="scss">
/* 引入uView库样式 */
@import "uview-ui/index.scss";
</style>
<style>
/* 项目基础样式 */
@import "./app.scss";
.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom,
.uni-app--showlayout+uni-tabbar.uni-tabbar-bottom .uni-tabbar,
.uni-app--showlayout+uni-tabbar.uni-tabbar-top,
.uni-app--showlayout+uni-tabbar.uni-tabbar-top .uni-tabbar {
left: var(--window-left);
right: var(--window-right);
}
</style>
打包wgt包 部署
- 1.修改版本号,在manifest.json的基础配置里修改
- 打包wgt包,在发行里点击打包wgt
- 3.将wgt包下载上传到服务器,后端配置相同的版本号即可