uniapp实现app自动更新:
实现步骤:
- 需要从后端读取最新版本的相关信息
- 前端用户进入首页的时候,需要判断当前版本与后端返回来的版本是否一致,不一致且后端版本大于当前版本的话,就需要提示用户是否需要更新,如果后端传了强制更新,那么就强制用户点击更新
相关js文件:
封装了三个js文件,可以直接用的:
dialog.js:
js
/**
* @Descripttion: app升级弹框
* @Version: 1.0.0
* @Author: leefine
*/
import config from './upgrade-config.js'
import upgrade from './upgrade'
const {
title = '发现新版本',
confirmText = '立即更新',
cancelTtext = '稍后再说',
confirmBgColor = '#409eff',
showCancel = true,
titleAlign = 'left',
descriAlign = 'left',
icon
} = config.upgrade;
class AppDialog {
constructor() {
this.maskEl = {}
this.popupEl = {}
this.screenHeight = 600;
this.popupHeight = 230;
this.popupWidth = 300;
this.viewWidth = 260;
this.descrTop = 130;
this.viewPadding = 20;
this.iconSize = 80;
this.titleHeight = 30;
this.textHeight = 18;
this.textSpace = 10;
this.popupContent = []
this.apkUrl = '';
this.showCancel = true; // 是否显示"稍后再说"按钮
}
// 显示
show(apkUrl, changelog, isForceUpdate = false) {
this.showCancel = !isForceUpdate; // 设置是否显示取消按钮
this.drawView(changelog);
this.maskEl.show();
this.popupEl.show();
this.apkUrl = apkUrl;
}
// 隐藏
hide() {
this.maskEl.hide()
this.popupEl.hide()
}
// 绘制
drawView(changelog) {
this.screenHeight = plus.screen.resolutionHeight;
this.popupWidth = plus.screen.resolutionWidth * 0.8;
this.popupHeight = this.viewPadding * 3 + this.iconSize + 100;
this.viewWidth = this.popupWidth - this.viewPadding * 2;
this.descrTop = this.viewPadding + this.iconSize + this.titleHeight;
this.popupContent = [];
if (icon) {
this.popupContent.push({
id: 'logo',
tag: 'img',
src: icon,
position: {
top: '0px',
left: (this.popupWidth - this.iconSize) / 2 + 'px',
width: this.iconSize + 'px',
height: this.iconSize + 'px'
}
});
} else {
this.popupContent.push({
id: 'logo',
tag: 'img',
src: '_pic/upgrade.png',
position: {
top: '0px',
left: (this.popupWidth - this.iconSize) / 2 + 'px',
width: this.iconSize + 'px',
height: this.iconSize + 'px'
}
});
}
// 标题
if (title) {
this.popupContent.push({
id: 'title',
tag: 'font',
text: title,
textStyles: {
size: '18px',
color: '#333',
weight: 'bold',
align: titleAlign
},
position: {
top: this.descrTop - this.titleHeight - this.textSpace + 'px',
left: this.viewPadding + 'px',
width: this.viewWidth + 'px',
height: this.titleHeight + 'px'
}
})
} else {
this.descrTop -= this.titleHeight;
}
this.drawText(changelog)
// 取消
if (this.showCancel) {
const width = (this.viewWidth - this.viewPadding) / 2;
const confirmLeft = width + this.viewPadding * 2;
this.drawBtn('cancel', width, cancelTtext)
this.drawBtn('confirm', width, confirmText, confirmLeft)
} else {
this.drawBtn('confirmBox', this.viewWidth, confirmText)
}
this.drawBox(showCancel)
}
// 描述内容
drawText(changelog) {
if (!changelog) return [];
const textArr = changelog.split('')
const len = textArr.length;
let prevNode = 0;
let nodeWidth = 0;
let letterWidth = 0;
const chineseWidth = 14;
const otherWidth = 7;
let rowText = [];
for (let i = 0; i < len; i++) {
// 包含中文
if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
// 包含字母
let textWidth = ''
if (letterWidth > 0) {
textWidth = nodeWidth + chineseWidth + letterWidth * otherWidth;
letterWidth = 0;
} else {
// 不含字母
textWidth = nodeWidth + chineseWidth;
}
if (textWidth > this.viewWidth) {
rowArrText(i, chineseWidth)
} else {
nodeWidth = textWidth;
}
} else {
// 不含中文
// 包含换行符
if (/\n/g.test(textArr[i])) {
rowArrText(i, 0, 1)
letterWidth = 0;
} else if (textArr[i] == '\\' && textArr[i + 1] == 'n') {
rowArrText(i, 0, 2)
letterWidth = 0;
} else if (/[a-zA-Z0-9]/g.test(textArr[i])) {
// 包含字母数字
letterWidth += 1;
const textWidth = nodeWidth + letterWidth * otherWidth;
if (textWidth > this.viewWidth) {
const preNode = i + 1 - letterWidth;
rowArrText(preNode, letterWidth * otherWidth)
letterWidth = 0;
}
} else {
if (nodeWidth + otherWidth > this.viewWidth) {
rowArrText(i, otherWidth)
} else {
nodeWidth += otherWidth;
}
}
}
}
if (prevNode < len) {
rowArrText(len, -1)
}
this.drawDesc(rowText)
function rowArrText(i, nWidth = 0, type = 0) {
const typeVal = type > 0 ? 'break' : 'text';
rowText.push({
type: typeVal,
content: changelog.substring(prevNode, i)
})
if (nWidth >= 0) {
prevNode = i + type;
nodeWidth = nWidth;
}
}
}
// 描述
drawDesc(rowText) {
rowText.forEach((item, index) => {
if (index > 0) {
this.descrTop += this.textHeight;
this.popupHeight += this.textHeight;
}
this.popupContent.push({
id: 'content' + index + 1,
tag: 'font',
text: item.content,
textStyles: {
size: '14px',
color: '#666',
align: descriAlign
},
position: {
top: this.descrTop + 'px',
left: this.viewPadding + 'px',
width: this.viewWidth + 'px',
height: this.textHeight + 'px'
}
})
if (item.type == 'break') {
this.descrTop += this.textSpace;
this.popupHeight += this.textSpace;
}
})
}
// 按钮
drawBtn(id, width, text, left = this.viewPadding) {
let boxColor = confirmBgColor,
textColor = '#ffffff';
if (id == 'cancel') {
boxColor = '#f0f0f0';
textColor = '#666666';
}
this.popupContent.push({
id: id + 'Box',
tag: 'rect',
rectStyles: {
radius: '6px',
color: boxColor
},
position: {
bottom: this.viewPadding + 'px',
left: left + 'px',
width: width + 'px',
height: '40px'
}
})
this.popupContent.push({
id: id + 'Text',
tag: 'font',
text: text,
textStyles: {
size: '14px',
color: textColor
},
position: {
bottom: this.viewPadding + 'px',
left: left + 'px',
width: width + 'px',
height: '40px'
}
})
}
// 内容框
drawBox(showCancel) {
this.maskEl = new plus.nativeObj.View('maskEl', {
top: '0px',
left: '0px',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)'
});
this.popupEl = new plus.nativeObj.View('popupEl', {
tag: 'rect',
top: (this.screenHeight - this.popupHeight) / 2 + 'px',
left: '10%',
height: this.popupHeight + 'px',
width: '80%'
});
// 白色背景
this.popupEl.drawRect({
color: '#ffffff',
radius: '8px'
}, {
top: this.iconSize / 2 + 'px',
height: this.popupHeight - this.iconSize / 2 + 'px'
});
this.popupEl.draw(this.popupContent);
this.popupEl.addEventListener('click', e => {
const maxTop = this.popupHeight - this.viewPadding;
const maxLeft = this.popupWidth - this.viewPadding;
const buttonWidth = (this.viewWidth - this.viewPadding) / 2;
if (e.clientY > maxTop - 40 && e.clientY < maxTop) {
if (this.showCancel) {
if (e.clientX > maxLeft - buttonWidth && e.clientX < maxLeft) {
// 点击确认
upgrade.checkOs(this.apkUrl);
}
} else {
if (e.clientX > this.viewPadding && e.clientX < maxLeft) {
upgrade.checkOs(this.apkUrl);
}
}
this.hide();
}
});
}
}
export default new AppDialog()
upgrade.js
js
/**
* @Descripttion: app下载更新
* @Version: 1.0.0
* @Author: leefine
*/
import config from './upgrade-config.js'
const { upType=0 }=config.upgrade;
class Upgrade{
// 检测平台
checkOs(apkUrl){
uni.getSystemInfo({
success:(res) => {
if(res.osName=="android"){
if(upType==1 && packageName){
plus.runtime.openURL('market://details?id='+packageName)
}else{
this.downloadInstallApp(apkUrl)
}
}else if(res.osName=='ios' && appleId){
// apple id 在 app conection 上传的位置可以看到 https://appstoreconnect.apple.com
plus.runtime.launchApplication({
action: `itms-apps://itunes.apple.com/cn/app/id${appleId}?mt=8`
}, function(err) {
uni.showToast({
title:err.message,
icon:'none'
})
})
}
}
})
}
// 下载更新
downloadInstallApp(apkUrl){
const dtask = plus.downloader.createDownload(apkUrl, {}, function (d,status){
// 下载完成
if (status == 200){
plus.runtime.install(plus.io.convertLocalFileSystemURL(d.filename),{},{},function(error){
uni.showToast({
title: '安装失败',
icon:'none'
});
})
}else{
uni.showToast({
title: '更新失败',
icon:'none'
});
}
});
this.downloadProgress(dtask);
}
// 下载进度
downloadProgress(dtask){
try{
dtask.start(); //开启下载任务
let prg=0;
let showLoading=plus.nativeUI.showWaiting('正在下载');
dtask.addEventListener('statechanged',function(task,status){
// 给下载任务设置监听
switch(task.state){
case 1:
showLoading.setTitle('正在下载');
break;
case 2:
showLoading.setTitle('已连接到服务器');
break;
case 3:
prg=parseInt((parseFloat(task.downloadedSize)/parseFloat(task.totalSize))*100);
showLoading.setTitle('正在下载'+prg+'%');
break;
case 4:
// 下载完成
plus.nativeUI.closeWaiting();
break;
}
})
}catch(e){
plus.nativeUI.closeWaiting();
uni.showToast({
title: '更新失败',
icon:'none'
})
}
}
}
export default new Upgrade()
upgrade-config.js
js
export default {
upgrade:{
packageName:'',
appleId:'',
upType:0,
timer:24,
icon:'/static/logo.png',
title:'发现新版本',
confirmText:'立即更新',
cancelTtext:'稍后再说',
confirmBgColor:'#409eff',
showCancel:true,
titleAlign:'left',
descriAlign:'left'
}
}
首页发起调用:
js
import appDialog from '../../node_modules/js_sdk/dialog';
onLoad: function() {
this.checkForUpdate() // 检查版本更新
},
checkForUpdate() {
const that = this;
console.log("检查版本更新")
// 获取当前版本号是异步的
that.getCurrentVersionCode().then(currentVersionCode => {
console.log("当前版本号:", currentVersionCode);
that.$u.get(`https://${Config.baseUrl}/api/Redpacket/getLatestAppVersion`)
.then(res => {
if (res.status === 'success' && res.data) {
const latestVersion = res.data;
// 假设 version 是字符串如 "2.0.0",可以用某种方式转成整数版本号,也可以让后端返回 version_code
const latestVersionCode = parseInt(latestVersion.version.replace(/\D/g, '')) ||
0;
console.log("服务端返回版本号:", latestVersionCode);
if (latestVersionCode > currentVersionCode) {
// 根据 force_update 字段决定是否允许取消按钮
const isForceUpdate = latestVersion.force_update === 1;
appDialog.show(latestVersion.download_url, latestVersion.update_message, isForceUpdate);
} else {
uni.showToast({
title: '当前已是最新版',
icon: 'none'
});
}
} else {
uni.showToast({
title: '版本检查失败',
icon: 'none'
});
}
})
.catch(err => {
console.error('版本检查请求失败:', err);
uni.showToast({
title: '请求失败,请稍后重试',
icon: 'none'
});
});
});
},
getCurrentVersionCode() {
return new Promise((resolve) => {
plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
resolve(parseInt(wgtinfo.versionCode));
});
});
},
需要注意js文件导入路径,该方法只能在app内调用(包含plus),所以需要运行到安卓基座测试
服务器需要:
我是以表的形式存在数据库中的:
表:
sql
CREATE TABLE `fa_app_version` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`version` VARCHAR(50) NOT NULL COMMENT '版本号',
`force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新,0 = 否,1 = 是',
`update_message` TEXT NOT NULL COMMENT '更新说明',
`download_url` VARCHAR(255) NOT NULL COMMENT '下载链接',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP版本表';
php代码:
php
/**
* 获取最新的APP版本信息
*/
public function getLatestAppVersion()
{
// 查询id最大的那条APP版本记录
$latestVersion = Db::name('app_version')
->order('id', 'desc')
->limit(1)
->field('id, version, force_update, update_message, download_url, created_at, updated_at')
->find();
return json([
'status' => 'success',
'data' => $latestVersion
]);
}
前端调用:
js
that.$u.get(`https://${Config.baseUrl}/api/Redpacket/getLatestAppVersion`)
.then(res => {
if (res.status === 'success' && res.data) {
const latestVersion = res.data;
// 假设 version 是字符串如 "2.0.0",可以用某种方式转成整数版本号,也可以让后端返回 version_code
const latestVersionCode = parseInt(latestVersion.version.replace(/\D/g, '')) ||
0;
console.log("服务端返回版本号:", latestVersionCode);
if (latestVersionCode > currentVersionCode) {
// 根据 force_update 字段决定是否允许取消按钮
const isForceUpdate = latestVersion.force_update === 1;
appDialog.show(latestVersion.download_url, latestVersion.update_message, isForceUpdate);
} else {
uni.showToast({
title: '当前已是最新版',
icon: 'none'
});
}
} else {
uni.showToast({
title: '版本检查失败',
icon: 'none'
});
}
})
.catch(err => {
console.error('版本检查请求失败:', err);
uni.showToast({
title: '请求失败,请稍后重试',
icon: 'none'
});
});
manifest.json文件中需要配置:

这个,每次打包的时候需要更新这里的值,一般都是向上更新,然后后台数据库里存version的版本号一定要和你打包的时候这里设置的版本号一样。然后需要将你打包的apk上传到服务器public的某个目录下面,保证这个地址能够浏览器打开下载就行。
基本流程就是这样,就是用户进入首页后,会自动取加载是否需要更新的方法,然后根据后台返回的版本号,判断是否需要更新和强制更新,如果需要,那么就点击更新,用户会进入下载页面,我这里有一个下载进度提示,下载完成用户点击安装即可,这样就不需要用户卸载重新安装apk了,管理员通过后台上传新版本的apk,就可以让用户的app实现更新功能