Uniapp 实现app自动检测更新/自动更新功能

实现步骤

  1. 配置 manifest.json
    • manifest.json 中设置应用的基本信息,包括 versionNameversionCode

一般默认0.0.1,1.

  1. 服务器端接口开发
    • 提供一个 API 接口,返回应用的最新版本信息,版本号、下载链接。
  2. 客户端检测更新
    • 使用 uni.request 发送请求到服务器端接口,获取最新版本信息。
    • 对比本地版本与服务器版本,判断是否需要更新。
  3. 展示更新提示
    • 如果需要更新,使用 uni.showModal 方法展示更新提示。
  4. 处理用户选择
    • 用户选择更新后,调用plus.downloader.createDownload 方法下载新版本。
    • 监听下载进度,并在下载完成后调用 plus.runtime.install 安装新版本。
  5. 异常处理
    • 对可能出现的错误进行捕获和处理,确保良好的用户体验。

我是参考的一个插件把过程简化了一些

插件地址: https://ext.dcloud.net.cn/plugin?id=9660

我简化了作者的index.js文件。其他的没变,以下是我的完整方法。

一共三个JS文件,注意引入路径。

index.vue

javascript 复制代码
import appDialog from '@/uni_modules/app-upgrade/js_sdk/dialog';
onLoad(){
    // 检查更新
    this.checkForUpdate()
},
methods: {
    async checkForUpdate() {
        //模拟接口返回数据
        let Response = {
            status: 1,// 0 无新版本 | 1 有新版本
            latestVersionCode: 200,//接口返回的最新版本号,用于对比
            changelog: "1. 优化了界面显示\n2. 修复了已知问题",//更新内容
            path: "xxx.apk"//下载地址
        };
        //获取当前安装包版本号
        const currentVersionCode = await this.getCurrentVersionCode();
        console.log("当前版本号:", currentVersionCode);
        console.log("最新版本号:", Response);
        // 对比版本号
        if (Response.latestVersionCode > currentVersionCode) {
            // 显示更新对话框
            appDialog.show(Response.path, Response.changelog);
        } else {
            uni.showToast({
                title: '当前已是最新版',
                icon: 'none'
            });
        }
    },
    getCurrentVersionCode() {
        return new Promise((resolve) => {
            //获取当前安装包版本号
            plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
                resolve(parseInt(wgtinfo.versionCode));
            });
        });
    }
},

js_sdk/dialog.js

javascript 复制代码
/**
 * @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 = '';
	}

	// 显示
	show(apkUrl, changelog) {
		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 (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 (showCancel) {
					// 取消
					// if(e.clientX>this.viewPadding && e.clientX<maxLeft-buttonWidth-this.viewPadding){}
					// 确定
					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()

js_sdk/upgrade.js

javascript 复制代码
/**
 * @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

javascript 复制代码
export default {
    upgrade:{
        packageName:'',
        appleId:'',
        upType:0,
        timer:24,
        icon:'/static/logo.png',
        title:'发现新版本',
        confirmText:'立即更新',
        cancelTtext:'稍后再说',
        confirmBgColor:'#409eff',
        showCancel:true,
        titleAlign:'left',
        descriAlign:'left'
    }
}

效果图:

upgrade.js 中downloadInstallApp函数下载更新代码解析,来自AI:

代码解析

1. plus.downloader.createDownload

这个方法用于创建一个下载任务。它接受三个参数:

  • url: 要下载的文件的 URL 地址。
  • headers : 下载请求的头部信息,通常是一个对象,这里传入的是一个空对象 {}
  • callback : 下载完成后的回调函数,它有两个参数:
    • d: 下载任务对象。
    • status: 下载的状态码,200 表示成功。
2. 回调函数

在下载完成后,回调函数会被调用。根据 status 的值来判断下载是否成功:

  • status == 200 : 下载成功,调用 plus.runtime.install 方法安装 APK 文件。
  • status != 200: 下载失败,显示一个更新失败的提示。
3. plus.runtime.install

这个方法用于安装下载好的 APK 文件。它接受四个参数:

  • path : 安装包的路径,这里使用 plus.io.convertLocalFileSystemURL(d.filename) 将下载任务的文件路径转换为本地文件系统路径。
  • options : 安装选项,这里传入的是一个空对象 {}
  • successCallback: 安装成功的回调函数,这里没有具体实现。
  • errorCallback: 安装失败的回调函数,显示一个安装失败的提示。
4. this.downloadProgress(dtask)

这是一个自定义的方法,用于监听下载进度。dtask 是下载任务对象,可以通过这个对象来获取下载的进度信息。

相关推荐
2401_857610033 分钟前
解析基于 SSM 框架 Vue 电脑测评系统:把握电脑测评精髓
前端·javascript·vue.js
烂不烂问厨房4 分钟前
前端项目发布后打开报错Uncaught SyntaxError: Unexpected token ‘<‘ (at chunk-vendors)
前端·javascript·vue.js·前端打包发布
总爱写点小BUG4 分钟前
可搜索的下拉选择框:filterable属性详解
前端·javascript·vue.js
毛毛三由6 分钟前
表单校验记录
前端·vue.js·elementui
程序媛_MISS_zhang_01106 分钟前
el-table中合并垂直方向的单元格
前端·javascript·vue.js
console.log('D')16 分钟前
预览和下载 (pc和微信小程序)
前端
web1478621072319 分钟前
前端XMLHttpRequest get请求能不能在body中传参数?
前端
何极光19 分钟前
uniapp小程序样式穿透
前端·小程序·uni-app
Yvemil720 分钟前
《开启微服务之旅:Spring Boot Web开发》(三)
前端·spring boot·微服务
子燕若水1 小时前
简要解释JSON Schema
前端·html·json