uniapp实现app自动更新

uniapp实现app自动更新:

实现步骤:

  1. 需要从后端读取最新版本的相关信息
  2. 前端用户进入首页的时候,需要判断当前版本与后端返回来的版本是否一致,不一致且后端版本大于当前版本的话,就需要提示用户是否需要更新,如果后端传了强制更新,那么就强制用户点击更新

相关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实现更新功能

相关推荐
Jackson@ML1 小时前
如何快速高效学习Python?
开发语言·python
西瓜本瓜@2 小时前
在Android中如何使用Protobuf上传协议
android·java·开发语言·git·学习·android-studio
UFIT2 小时前
Python函数与模块笔记
开发语言·python
机智的人猿泰山2 小时前
java kafka
java·开发语言·kafka
一夜枫林2 小时前
uniapp自定义拖拽排列
前端·javascript·uni-app
Y1nhl3 小时前
搜广推校招面经八十一
开发语言·人工智能·pytorch·深度学习·机器学习·推荐算法·搜索算法
Algorithm15763 小时前
谈谈接口和抽象类有什么区别?
java·开发语言
yu4106213 小时前
Rust 语言使用场景分析
开发语言·后端·rust
264玫瑰资源库5 小时前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏