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

ops/2024/11/2 6:17:55/

实现步骤

  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.complus.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.ppmy.cn/ops/130348.html

相关文章

C++入门——“C++11-右值引用和移动语义”

C11相比于C98增加以许多新特性&#xff0c;让C语言更加灵活好用&#xff0c;但是貌似也增加了许多学习的难度&#xff0c;现在先看第一部分。 一、右值引用和移动语义 1.右值引用和左值引用 在C中&#xff0c;值可以大致分为右值和左值&#xff0c;左值大概是哪些已经被定义的变…

DirectShow过滤器开发-写MP3音频文件过滤器(再写 写MP3)

下载本过滤器DLL 本过滤器将MP3音频流写到MP3音频文件。 过滤器信息 过滤器名称&#xff1a;写MP3_2 过滤器GUID&#xff1a;{AE46BC15-71E5-471C-8540-3B73094111EC} DLL注册函数名&#xff1a;DllRegisterServer 删除注册函数名&#xff1a;DllUnregisterServer 过滤器有1个…

C语言中函数的实参和形参

本文主要叙述C语言中函数的实参和形参的概念和区别。 实参&#xff08;实际参数&#xff09; 实参是在函数调用时提供的具体值或变量&#xff0c;它们被传递给函数以供函数内部使用。实参可以是常量、变量、表达式或其他函数的返回值。实参在函数调用时被传递给形参&#xff…

嵌入式常用功能之通讯协议1--IIC

嵌入式常用功能之通讯协议1--串口 嵌入式常用功能之通讯协议1--IIC&#xff08;本文&#xff09; 嵌入式常用功能之通讯协议1--SPI 一、IIC总线协议介绍 Inter-Integrated Circuit(集成电路总线&#xff09;&#xff0c;是由 Philips 半导体公司&#xff08;现在的 NXP 半导体…

第七章 selinux

1、selinux的说明 SELinux是Security-Enhanced Linux的缩写&#xff0c;意思是安全强化的linux。 SELinux 主要由美国国家安全局&#xff08;NSA&#xff09;开发&#xff0c;当初开发的目的是为了避免资源的误用。 系统资源都是通过程序进行访问的&#xff0c;如果将 /var/ww…

SpringBoot【实用篇】- 测试

文章目录 目标&#xff1a;1.加载测试专用属性3.Web环境模拟测试2.加载测试专用配置4.数据层测试回滚5.测试用例数据设定 目标&#xff1a; 加载测试专用属性加载测试专用配置Web环境模拟测试数据层测试回滚测试用例数据设定 1.加载测试专用属性 我们在前面讲配置高级的时候…

Reactor模型

Reactor模型 引言 ​ ​ 概念&#xff1a; Reactor模型&#xff1a;又称为反应堆模型&#xff0c;它是一种基于事件驱动和I/O多路复用的设计模式&#xff0c;常用于处理大量并发I/O事件&#xff0c;是一个高性能模型&#xff0c;它通过事件驱动的方式&#xff0c;高效地管…

parted 磁盘分区

目录 磁盘格式磁盘分区文件系统挂载使用扩展 - parted、fdisk、gdisk 区别 磁盘格式 parted /dev/vdcmklabel gpt # 设置磁盘格式为GPT p # 打印磁盘信息此时磁盘格式设置完成&#xff01; 磁盘分区 开始分区&#xff1a; mkpart data_mysql # 分区名&…