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

news2024/12/25 13:49:38

实现步骤

  1. 配置 manifest.json
    • 在 manifest.json 中设置应用的基本信息,包括 versionName 和 versionCode

           一般默认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

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

/**
 * @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

/**
 * @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

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 是下载任务对象,可以通过这个对象来获取下载的进度信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2234670.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基础算法——排序算法(冒泡排序,选择排序,堆排序,插入排序,希尔排序,归并排序,快速排序,计数排序,桶排序,基数排序,Java排序)

1.概述 比较排序算法 算法最好最坏平均空间稳定思想注意事项冒泡O(n)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)Y比较最好情况需要额外判断选择O( n 2 n^2 n2)O( n 2 n^2 n2)O( n 2 n^2 n2)O(1)N比较交换次数一般少于冒泡堆O( n l o g n nlogn nlogn)O( n l o g n nlogn nlogn)O( n l…

多元数据库时代,云和恩墨携手鲲鹏引领数据库一体机新变革

近年来&#xff0c;随着企业数据存储结构日益多元化&#xff0c;传统架构数据库面临发展瓶颈&#xff0c;越来越多企业倾向于采用不同类型的数据库满足多样化的数据需求。这一趋势下&#xff0c;国内数据库市场呈现百花齐放的态势&#xff0c;产业加速迈入多元数据库时代。 作为…

[SWPUCTF 2022 新生赛]Cycle Again -拒绝脚本小子,成为工具糕手

1.题目 打开&#xff0c;一张图片&#xff0c;一个压缩包 2.分析 图片丢进随波逐流中 发现第一部分的flag NSSCTF{41d769db- 丢进b神的工具中 爆出第二段flag 9f5d-455e-a458-8012ba3660f3} 两段进行拼接 NSSCTF{41d769db-9f5d-455e-a458-8012ba3660f3} 直接拿下 遥遥领…

机场电子采购信息系统

摘 要 互联网的发展&#xff0c;改变了人类原来繁琐的生活和消费习惯&#xff0c;人们的时间观念也在不断加强&#xff0c;所以各种信息系统的数量越来越多&#xff0c;方便了用户&#xff0c;用户习惯也发生了改变。对于传统的企业采购模式来说由于费用高、速度慢、不透明化…

RabbitMQ设置消息过期时间

RabbitMQ设置消息过期时间 1、过期消息&#xff08;死信&#xff09;2、设置消息过期的两种方式2.1、设置单条消息的过期时间2.1.1、配置文件application.yml2.1.2、配置类RabbitConfig2.1.3、发送消息业务类service&#xff08;核心代码&#xff09;2.1.4、启动类2.1.5、依赖文…

android数组控件Textview

说明&#xff1a;android循环控件&#xff0c;注册和显示内容 效果图&#xff1a; step1: E:\projectgood\resget\demozz\IosDialogDemo-main\app\src\main\java\com\example\iosdialogdemo\TimerActivity.java package com.example.iosdialogdemo;import android.os.Bundl…

【AI日记】24.11.01 LangChain、openai api和github copilot

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 工作1 内容&#xff1a;学习deeplearning.ai的免费课程地址&#xff1a;LangChain Chat with Your DataB站地址&#xff1a;https://www.bilibili.com/video/BV148411D7d2github代码&#xff1a;https:…

指标+AI+BI:构建数据分析新范式丨2024袋鼠云秋季发布会回顾

10月30日&#xff0c;袋鼠云成功举办了以“AI驱动&#xff0c;数智未来”为主题的2024年秋季发布会。大会深度探讨了如何凭借 AI 实现新的飞跃&#xff0c;重塑企业的经营管理方式&#xff0c;加速数智化进程。 作为大会的重要环节之一&#xff0c;袋鼠云数栈产品经理潮汐带来了…

goframe开发一个企业网站 前端界面 拆分界面7

将页面拆出几个公用部分 在resource/template/front创建meta.html header.html footer.html meta.html <head><meta charset"utf-8"><meta content"widthdevice-width, initial-scale1.0" name"viewport"><title>{{.…

SpringBoot3集成Junit5

目录 1. 确保项目中包含相关依赖2. 配置JUnit 53. 编写测试类4、Junit5 新增特性4.1 注解4.2 断言4.3 嵌套测试4.4 总结 在Spring Boot 3中集成JUnit 5的步骤相对简单。以下是你可以按照的步骤&#xff1a; 1. 确保项目中包含相关依赖 首先&#xff0c;确保你的pom.xml文件中…

[数据结构从小白到大牛]第五篇:3分钟带你吃透双链表并用C语言模拟实现

目录 1->前言 2->链表的概念和结构 2.1链表概念 2.2->带头双向循环链表结构 3->模拟实现带头双向循环链表 3.1定义链表结点 struct ListNode 3.2创建链表结点 CreateLTNode 函数 3.3链表初始化函数 ListInit函数 3.4链表打印函数 ListPrint函数 3.5链表…

前端通过nginx部署一个本地服务的方法

前端通过nginx部署一个本地服务的方法&#xff1a; 1.下载ngnix nginx 下载完成后解压缩后运行nginx.exe文件 2.打包你的前端项目文件 yarn build 把生成的dist文件复制出来&#xff0c;替换到nginx的html文件下 3.配置conf目录的nginx.conf文件 主要配置server监听 ser…

不同的浮点数类型

不同的浮点数类型 尽管4字节的浮点数可表达相当大的数值&#xff0c;但对于人类而言&#xff0c;总不够用。一般而言&#xff0c;浮点数有3种类型&#xff0c;单精度的float和双精度的double以及更长的long double, 可参考&#xff1a;数据类型大小 不同语言的浮点数类型 C/Obj…

蓝桥杯第21场小白入门赛补题

5.蓝桥派对 思路 &#xff1a;一个区间与多少个其他区间有关联&#xff0c;先对所有区间左端点和右端点从小到大排序&#xff0c;对于每个询问&#xff0c;我们先算出[1,r]这个区间里有多少个区间的起点即区间总数&#xff0c;使用upper_bound函数&#xff0c;然后使用lower_bo…

推荐一款功能强大的数据库开发管理工具:SQLite Expert Pro

SQLite Expert Professional是一个功能强大的工具&#xff0c;旨在简化SQLite3数据库的开发。 它是SQLite的一个功能丰富的管理和开发工具&#xff0c;旨在满足所有用户从编写简单SQL查询到开发复杂数据库的需求。 图形界面支持所有SQLite功能。 它包括一个可视化查询构建器&a…

sql专题 之 常用命令

文章目录 查询基础语法查询全表查询选择查询&#xff1a;常量和运算&#xff1a; 条件查询where运算符&#xff1a;、 !、<、>空值&#xff1a;null模糊查询&#xff1a;like逻辑运算&#xff1a;and or not 去重&#xff1a;distinct排序&#xff1a;order by截断和偏移…

Unity的gRPC使用之实现客户端

应用背景&#xff1a;本想Unity调用C的dll库获取一些数据资源&#xff0c;但是由于自己调用的C库模块化处理的不太理想&#xff0c;众多dll之间相互依赖&#xff0c;使得在调用dll的时候&#xff0c;会忽略一些dll的缺失&#xff0c;使Unity项目报错&#xff0c;故想到了使用gR…

Linux基础-常用操作命令详讲

Linux基础-常用操作命令详讲 一、openssl加密简单介绍 1. 生成加密的密码散列&#xff08;password hash&#xff09;​编辑 1.1 常见的选项总结表 1.2 加密参数详解 2. 自签名证书 3. 证书转换 二、文件管理 1. 创建空文件 ​编辑 2. 删除文件 4. 新建目录 ​编辑…

[大模型]视频生成-Sora简析

参考资料&#xff1a; Sora技术报告https://openai.com/index/video-generation-models-as-world-simulators/4分钟详细揭密&#xff01;Sora视频生成模型原理https://www.bilibili.com/video/BV1AW421K7Ut 一、概述 相较于Gen-2、Stable Diffusion、Pika等生成模型的前辈&am…

STM32学习笔记-外部中断和外部时钟

文章目录 EXTI基本结构AFIO 定时器1. STM32 定时器的种类2. 定时器的主要功能3. 定时器的配置4. 定时器 PWM 输出模式5. 定时器中断配置输出比较1. 输出比较模式概述2. 输出比较模式的配置今天实在有点疲惫了&#xff0c;明天继续学吧。 EXTI基本结构 AFIO 中断引脚选择&#…