在uniapp中实现APP自动更新功能,主要涉及到客户端在功能不断迭代过程中,需要进行自动更新。uniapp一个详细的实现步骤,包括客户端和服务器端的配置:
服务器端配置
版本信息管理
- 服务器端需要维护一个数据库或配置文件,用于存储APP的最新版本信息,包括版本号、更新说明、下载链接等。
- 提供一个API接口,客户端可以通过该接口获取最新版本信息。
客户端实现
- 版本信息获取:
- 在uniapp的客户端,通过uni.request()方法调用服务器端的API接口,获取最新版本信息。
- 版本比对:
- 客户端获取到最新版本信息后,与当前APP的版本号进行比对。
- 版本号比对逻辑可以根据实际情况设计,常见的做法是比较字符串或将其转换为数字进行比较。
- 更新提示:
- 如果发现新版本,则弹出更新提示框,引导用户进行更新。
- 可以通过uni.showModal()方法显示更新提示框,并提供更新和取消的选项。
- 下载并安装:
- 用户确认更新后,客户端开始下载新版本APK文件。
- 下载完成后,使用plus.runtime.install()方法安装APK文件。
- 注意:安装APK文件需要用户授权,并且可能需要在Android的"设置"中开启"允许安装未知来源的应用"。
- 静默更新(可选):
- 对于一些不需要用户干预的更新,可以考虑实现静默更新。
- 静默更新通常涉及到wgt(widget)包的更新,而不是整个APK的替换。
- uniapp提供了相关的插件和API支持wgt包的更新,如uni-upgrade-center等。
组件扩展简化调用
我们只需要在我们的首页引入版本自动更新组件即可。
<template>
<view class="container container329152">
<!-- #ifdef APP -->
<diy-upgrade style="z-index: 999999999" image="/static/upgrade.png" upgradeUrl=""> </diy-upgrade>
<!-- #endif -->
<view class="clearfix"></view>
</view>
</template>
<script>
export default {
data() {
return {
//用户全局信息
userInfo: {},
//页面传参
globalOption: {},
//自定义全局变量
globalData: {}
};
},
onShow() {
this.setCurrentPage(this);
},
onLoad(option) {
this.setCurrentPage(this);
if (option) {
this.setData({
globalOption: this.getOption(option)
});
}
this.init();
},
methods: {
async init() {}
}
};
</script>
<style lang="scss" scoped>
.container329152 {
}
</style>
组件库代码实现
diy-upgrade组件代码实现,大家如果对此组件库可按需进行二次开发扩展。
<template>
<view class="mask flex-center" v-if="showUpdate">
<view class="content botton-radius" :class="[image?'':'no-imgae']">
<view class="content-top" >
<view class="content-top-text">
<text>{{title}}</text>
<text class="content-top-text-version">v.{{version}}</text>
</view>
<image v-if="image" class="content-top" style="top: 0;" width="100%" height="100%"
:src='image'>
</image>
<view v-else class="content-top" style="top: 0;" width="100%" height="100%"></view>
</view>
<view v-if="image" class="content-header"></view>
<view class="content-body">
<slot></slot>
<view class="body" v-if="contents">
<scroll-view class="box-des-scroll" scroll-y="true">
<rich-text :nodes="contents"></rich-text>
</scroll-view>
</view>
<view class="footer flex-center">
<template v-if="isAppStore">
<button class="content-button" :style="btnStyle" 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="btnBgColor" show-info stroke-width="10" />
<view class="flex flex-center" 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="btnStyle" style="border: none;color: #fff;" plain
@click="updateApp">
{{downLoadBtnText}}
</button>
</template>
<button v-else-if="downloadSuccess && !installed" :style="btnStyle" class="content-button"
style="border: none;color: #fff;" plain :loading="installing" :disabled="installing"
@click="installPackage">
{{installing ? '正在安装......' : '下载完成,立即安装'}}
</button>
<button v-if="installed && isWGT" :style="btnStyle" class="content-button" style="border: none;color: #fff;" plain
@click="restart">
安装完毕,点击重启
</button>
</template>
</view>
</view>
<text v-if="!is_mandatory" class="close-img diy-icon-close" @click.stop="closeUpdate"></text>
</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 {
props: {
//更新图片
image: {
type: String,
default: ''
},
//版本更新较验地址
upgradeUrl:{
type: String,
default: ''
},
// 进度条颜色
btnBgColor:{
default: '',
type: String
}
},
data() {
return {
showUpdate:false,
// 更新的版本号
version: '',
// 系统环境
platform: '',
// 下载链接
url: '',
// 跳转的应用市场列表
storeList: [],
type:'',
// 从之前下载安装
installForBeforeFilePath: '',
// 安装
installed: false,
installing: false,
// 下载
downloadSuccess: false,
downloading: false,
downLoadPercent: 50,
downloadedSize: 0,
packageFileSize: 0,
tempFilePath: '', // 要安装的本地包地址
// 默认安装包信息
title: '版本更新',
contents: '',
is_mandatory: false,
// 可自定义属性
downLoadBtnTextiOS: '立即跳转更新',
downLoadBtnText: '立即下载更新',
downLoadingText: '安装包下载中,请稍后',
pageLevelNum: 0
}
},
onBackPress() {
// 强制更新不允许返回
if (this.is_mandatory) {
return true
}
downloadTask && downloadTask.abort()
},
onHide() {
openSchemePromise = null
},
mounted() {
this.init()
},
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)
},
btnStyle(){
return this.btnBgColor?{background:this.btnBgColor}:{}
}
},
methods: {
// 获取更新内容片段
getContentHTML(content) {
let contentArr = content.split('\n');
return contentArr.map(item => `<p>${item}</p>`).join('\n')
},
async init(){
// #ifdef APP-PLUS
let thiz = this;
if(!thiz.upgradeUrl){
console.log('请配置版本较验地址')
console.log("{url:'你的APK下越地址',version:'1.0.1',title:'版本更新',contents:'版本更新内容'}")
uni.showToast({
title:'请配置版本较验地址'
})
return;
}
uni.getSystemInfo({
success: (res) => {
let platform = res.platform;
// 获取本机版本号
plus.runtime.getProperty(plus.runtime.appid,async (wgtinfo) => {
thiz.versionCode = wgtinfo.versionCode;
let res = await getApp().globalData.currentPage.$http.post(thiz.upgradeUrl,{
appid: plus.runtime.appid,
platform,
version: plus.runtime.version,
wgtVersion: wgtinfo.version
})
res = res.data;
console.log(res)
//如果API返回新的地址,并判断版本是否相同
if(res.url){
thiz.url = res.url
thiz.version = res.version
//判断API返回的版本是不是大于系统版本
if(compare(thiz.version,wgtinfo.version)){
// 跳转的应用市场列表
thiz.storeList = res.store_list || [];
thiz.title = res.title || '发现新版本';
thiz.type = res.type;
if(res.contents){
thiz.contents = thiz.getContentHTML(res.contents)
}
thiz.is_mandatory = res.is_mandatory||false
this.checkLocalStoragePackage()
}
}
});
},
fail(e){
console.log(e)
}
});
// #endif
},
goBack() {
this.showUpdate = false
},
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)
}
}
this.showUpdate = true;
},
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()
this.goBack()
}
}
});
return;
}
if (this.downloadSuccess && this.tempFilePath) {
// 包已经下载完毕,稍后安装,将包保存在本地
await this.saveFile(this.tempFilePath, this.version)
this.goBack()
return;
}
this.goBack()
},
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) {
this.goBack()
}
// #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>
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-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-top-text-version{
font-size: 24rpx;
}
}
.no-imgae .content-top{
padding-top:30px;
height: auto;
}
.no-imgae .content-top,.no-imgae .content-top .content-top-text{
position: relative;
top:0;
left:0;
color:#000;
width:100%;
}
.content-header {
height: 70rpx;
}
.title {
font-size: 33rpx;
font-weight: bold;
color: #3DA7FF;
line-height: 38px;
}
.footer {
height: 150rpx;
display: flex;
align-items: center;
justify-content: space-around;
}
.box-des-scroll {
box-sizing: border-box;
min-height: 100rpx;
max-height: 400rpx;
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);
font-size: 60rpx;
color:#fff;
}
.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);
}
.content-button.button-hover {
transform: translate(1rpx, 1rpx);
}
.flex-column {
display: flex;
flex-direction: column;
align-items: center;
}
</style>