uniapp APP自动更新组件

在uniapp中实现APP自动更新功能,主要涉及到客户端在功能不断迭代过程中,需要进行自动更新。uniapp一个详细的实现步骤,包括客户端和服务器端的配置:

服务器端配置

版本信息管理

  1. 服务器端需要维护一个数据库或配置文件,用于存储APP的最新版本信息,包括版本号、更新说明、下载链接等。
  2. 提供一个API接口,客户端可以通过该接口获取最新版本信息。

客户端实现

  1. 版本信息获取:
    • 在uniapp的客户端,通过uni.request()方法调用服务器端的API接口,获取最新版本信息。
  2. 版本比对:
    • 客户端获取到最新版本信息后,与当前APP的版本号进行比对。
    • 版本号比对逻辑可以根据实际情况设计,常见的做法是比较字符串或将其转换为数字进行比较。
  3. 更新提示:
    • 如果发现新版本,则弹出更新提示框,引导用户进行更新。
    • 可以通过uni.showModal()方法显示更新提示框,并提供更新和取消的选项。
  4. 下载并安装:
    • 用户确认更新后,客户端开始下载新版本APK文件。
    • 下载完成后,使用plus.runtime.install()方法安装APK文件。
    • 注意:安装APK文件需要用户授权,并且可能需要在Android的"设置"中开启"允许安装未知来源的应用"。
  5. 静默更新(可选):
    • 对于一些不需要用户干预的更新,可以考虑实现静默更新。
    • 静默更新通常涉及到wgt(widget)包的更新,而不是整个APK的替换。
    • uniapp提供了相关的插件和API支持wgt包的更新,如uni-upgrade-center等。

组件扩展简化调用

我们只需要在我们的首页引入版本自动更新组件即可。

<template>
	<view class="container container329152">
		<!-- #ifdef APP -->
		<diy-upgrade style="z-index: 999999999" image="/static/upgrade.png" upgradeUrl=""> </diy-upgrade>
		<!--  #endif -->
		<view class="clearfix"></view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				//用户全局信息
				userInfo: {},
				//页面传参
				globalOption: {},
				//自定义全局变量
				globalData: {}
			};
		},
		onShow() {
			this.setCurrentPage(this);
		},
		onLoad(option) {
			this.setCurrentPage(this);
			if (option) {
				this.setData({
					globalOption: this.getOption(option)
				});
			}

			this.init();
		},
		methods: {
			async init() {}
		}
	};
</script>

<style lang="scss" scoped>
	.container329152 {
	}
</style>

组件库代码实现

diy-upgrade组件代码实现,大家如果对此组件库可按需进行二次开发扩展。

<template>
	<view class="mask flex-center" v-if="showUpdate">
		<view class="content botton-radius" :class="[image?'':'no-imgae']">
			<view class="content-top" >
				<view class="content-top-text">
					<text>{{title}}</text>
					<text class="content-top-text-version">v.{{version}}</text>
				</view>
				<image v-if="image" class="content-top" style="top: 0;" width="100%" height="100%"
					:src='image'>
				</image>
				<view v-else  class="content-top" style="top: 0;" width="100%" height="100%"></view>
			</view>
			<view v-if="image"  class="content-header"></view>
			<view class="content-body">
				<slot></slot>
				<view class="body" v-if="contents">
					<scroll-view class="box-des-scroll" scroll-y="true">
						<rich-text :nodes="contents"></rich-text>
					</scroll-view>
				</view>
				<view class="footer flex-center">
					<template v-if="isAppStore">
						<button class="content-button" :style="btnStyle"  style="border: none;color: #fff;" plain @click="jumpToAppStore">
							{{downLoadBtnTextiOS}}
						</button>
					</template>
					<template v-else>
						<template v-if="!downloadSuccess">
							<view class="progress-box flex-column" v-if="downloading">
								<progress class="progress" border-radius="35" :percent="downLoadPercent"
									:activeColor="btnBgColor" show-info stroke-width="10" />
								<view class="flex flex-center" style="width:100%;font-size: 28rpx;display: flex;justify-content: space-around;">
									<text>{{downLoadingText}}</text>
									<text>({{downloadedSize}}/{{packageFileSize}}M)</text>
								</view>
							</view>

							<button v-else class="content-button"  :style="btnStyle" style="border: none;color: #fff;" plain
								@click="updateApp">
								{{downLoadBtnText}}
							</button>
						</template>
						<button v-else-if="downloadSuccess && !installed"  :style="btnStyle" class="content-button"
							style="border: none;color: #fff;" plain :loading="installing" :disabled="installing"
							@click="installPackage">
							{{installing ? '正在安装......' : '下载完成,立即安装'}}
						</button>
						<button v-if="installed && isWGT" :style="btnStyle" class="content-button" style="border: none;color: #fff;" plain
							@click="restart">
							安装完毕,点击重启
						</button>
					</template>
				</view>
			</view>
			<text v-if="!is_mandatory" class="close-img diy-icon-close" @click.stop="closeUpdate"></text>
		</view>
	</view>
</template>

<script>
	const localFilePathKey = 'UNI_ADMIN_UPGRADE_CENTER_LOCAL_FILE_PATH'
	const platform_iOS = 'iOS';
	let downloadTask = null;
	let openSchemePromise

	/**
	 * 对比版本号,如需要,请自行修改判断规则
	 * 支持比对	("3.0.0.0.0.1.0.1", "3.0.0.0.0.1")	("3.0.0.1", "3.0")	("3.1.1", "3.1.1.1") 之类的
	 * @param {Object} v1
	 * @param {Object} v2
	 * v1 > v2 return 1
	 * v1 < v2 return -1
	 * v1 == v2 return 0
	 */
	function compare(v1 = '0', v2 = '0') {
		v1 = String(v1).split('.')
		v2 = String(v2).split('.')
		const minVersionLens = Math.min(v1.length, v2.length);

		let result = 0;
		for (let i = 0; i < minVersionLens; i++) {
			const curV1 = Number(v1[i])
			const curV2 = Number(v2[i])

			if (curV1 > curV2) {
				result = 1
				break;
			} else if (curV1 < curV2) {
				result = -1
				break;
			}
		}

		if (result === 0 && (v1.length !== v2.length)) {
			const v1BiggerThenv2 = v1.length > v2.length;
			const maxLensVersion = v1BiggerThenv2 ? v1 : v2;
			for (let i = minVersionLens; i < maxLensVersion.length; i++) {
				const curVersion = Number(maxLensVersion[i])
				if (curVersion > 0) {
					v1BiggerThenv2 ? result = 1 : result = -1
					break;
				}
			}
		}

		return result;
	}

	export default {
		props: {
			//更新图片
			image: {
				type: String,
				default: ''
			},
			//版本更新较验地址
			upgradeUrl:{
				type: String,
				default: ''
			},
			// 进度条颜色
			btnBgColor:{
				default: '',
				type: String
			}
		},
		data() {
			return {
				showUpdate:false,
				// 更新的版本号
				version: '',
				// 系统环境
				platform: '',
				// 下载链接
				url: '',
				// 跳转的应用市场列表
				storeList: [],
				type:'',
				// 从之前下载安装
				installForBeforeFilePath: '',
				// 安装
				installed: false,
				installing: false,
				// 下载
				downloadSuccess: false,
				downloading: false,
				downLoadPercent: 50,
				downloadedSize: 0,
				packageFileSize: 0,
				tempFilePath: '', // 要安装的本地包地址
				// 默认安装包信息
				title: '版本更新',
				contents: '',
				is_mandatory: false,
				// 可自定义属性
				downLoadBtnTextiOS: '立即跳转更新',
				downLoadBtnText: '立即下载更新',
				downLoadingText: '安装包下载中,请稍后',
				pageLevelNum: 0
			}
		},
		onBackPress() {
			// 强制更新不允许返回
			if (this.is_mandatory) {
				return true
			}
			downloadTask && downloadTask.abort()
		},
		onHide() {
			openSchemePromise = null
		},
		mounted() {
			this.init()
		},
		computed: {
			isWGT() {
				return this.type === 'wgt'
			},
			isiOS() {
				return !this.isWGT ? this.platform.includes(platform_iOS) : false;
			},
			isAppStore() {
				return this.isiOS || (!this.isiOS && !this.isWGT && this.url.indexOf('.apk') === -1)
			},
			btnStyle(){
				return this.btnBgColor?{background:this.btnBgColor}:{}
			}
		},
		methods: {
			// 获取更新内容片段
			getContentHTML(content) {
				let contentArr = content.split('\n');
				return contentArr.map(item => `<p>${item}</p>`).join('\n')
			},
			async init(){
				// #ifdef APP-PLUS
				let thiz = this;
				if(!thiz.upgradeUrl){
					console.log('请配置版本较验地址')
					console.log("{url:'你的APK下越地址',version:'1.0.1',title:'版本更新',contents:'版本更新内容'}")
					uni.showToast({
						title:'请配置版本较验地址'
					})
					return;
				}
				uni.getSystemInfo({
					success: (res) => {
						let platform = res.platform;
						// 获取本机版本号
						plus.runtime.getProperty(plus.runtime.appid,async (wgtinfo) => {
							thiz.versionCode = wgtinfo.versionCode;
							let res = await getApp().globalData.currentPage.$http.post(thiz.upgradeUrl,{
								appid: plus.runtime.appid,
								platform,
								version: plus.runtime.version,
								wgtVersion: wgtinfo.version
							})
							res = res.data;
							console.log(res)
							//如果API返回新的地址,并判断版本是否相同
							if(res.url){
								thiz.url = res.url
								thiz.version = res.version
								//判断API返回的版本是不是大于系统版本
								if(compare(thiz.version,wgtinfo.version)){
									// 跳转的应用市场列表
									thiz.storeList = res.store_list || [];
									thiz.title = res.title || '发现新版本';
									thiz.type = res.type;
									if(res.contents){
										thiz.contents =  thiz.getContentHTML(res.contents)
									}
									thiz.is_mandatory = res.is_mandatory||false
									this.checkLocalStoragePackage()
								}
							}
						});
					},
					fail(e){
						console.log(e)
					}
				});
				// #endif
			},
			goBack() {
				this.showUpdate = false
			},
			checkLocalStoragePackage() {
				// 如果已经有下载好的包,则直接提示安装
				const localFilePathRecord = uni.getStorageSync(localFilePathKey)
				if (localFilePathRecord) {
					const {
						version,
						savedFilePath,
						installed
					} = localFilePathRecord

					// 比对版本
					if (!installed && compare(version, this.version) === 0) {
						this.downloadSuccess = true;
						this.installForBeforeFilePath = savedFilePath;
						this.tempFilePath = savedFilePath
					} else {
						// 如果保存的包版本小 或 已安装过,则直接删除
						this.deleteSavedFile(savedFilePath)
					}
				}
				this.showUpdate = true;
			},
			async closeUpdate() {
				if (this.downloading) {
					if (this.is_mandatory) {
						return uni.showToast({
							title: '下载中,请稍后......',
							icon: 'none',
							duration: 500
						})
					}
					uni.showModal({
						title: '是否取消下载?',
						cancelText: '否',
						confirmText: '是',
						success: res => {
							if (res.confirm) {
								downloadTask && downloadTask.abort()
								this.goBack()
							}
						}
					});
					return;
				}

				if (this.downloadSuccess && this.tempFilePath) {
					// 包已经下载完毕,稍后安装,将包保存在本地
					await this.saveFile(this.tempFilePath, this.version)
					this.goBack()
					return;
				}

				this.goBack()
			},
			updateApp() {
				this.checkStoreScheme().catch(() => {
					this.downloadPackage()
				})
			},
			// 跳转应用商店
			checkStoreScheme() {
				const storeList = (this.store_list || []).filter(item => item.enable)
				if (storeList && storeList.length) {
					storeList
						.sort((cur, next) => next.priority - cur.priority)
						.map(item => item.scheme)
						.reduce((promise, cur, curIndex) => {
							openSchemePromise = (promise || (promise = Promise.reject())).catch(() => {
								return new Promise((resolve, reject) => {
									plus.runtime.openURL(cur, (err) => {
										reject(err)
									})
								})
							})
							return openSchemePromise
						}, openSchemePromise)
					return openSchemePromise
				}

				return Promise.reject()
			},
			downloadPackage() {
				this.downloading = true;
				//下载包
				downloadTask = uni.downloadFile({
					url: this.url,
					success: res => {
						if (res.statusCode == 200) {
							this.downloadSuccess = true;
							this.tempFilePath = res.tempFilePath
							// 强制更新,直接安装
							if (this.is_mandatory) {
								this.installPackage();
							}
						}
					},
					complete: () => {
						this.downloading = false;

						this.downLoadPercent = 0
						this.downloadedSize = 0
						this.packageFileSize = 0

						downloadTask = null;
					}
				});

				downloadTask.onProgressUpdate(res => {
					this.downLoadPercent = res.progress;
					this.downloadedSize = (res.totalBytesWritten / Math.pow(1024, 2)).toFixed(2);
					this.packageFileSize = (res.totalBytesExpectedToWrite / Math.pow(1024, 2)).toFixed(2);
				});
			},
			installPackage() {
				// #ifdef APP-PLUS
				// wgt资源包安装
				if (this.isWGT) {
					this.installing = true;
				}
				plus.runtime.install(this.tempFilePath, {
					force: false
				}, async res => {
					this.installing = false;
					this.installed = true;

					// wgt包,安装后会提示 安装成功,是否重启
					if (this.isWGT) {
						// 强制更新安装完成重启
						if (this.is_mandatory) {
							uni.showLoading({
								icon: 'none',
								title: '安装成功,正在重启......'
							})

							setTimeout(() => {
								uni.hideLoading()
								this.restart();
							}, 1000)
						}
					} else {
						const localFilePathRecord = uni.getStorageSync(localFilePathKey)
						uni.setStorageSync(localFilePathKey, {
							...localFilePathRecord,
							installed: true
						})
					}
				}, async err => {
					// 如果是安装之前的包,安装失败后删除之前的包
					if (this.installForBeforeFilePath) {
						await this.deleteSavedFile(this.installForBeforeFilePath)
						this.installForBeforeFilePath = '';
					}

					// 安装失败需要重新下载安装包
					this.installing = false;
					this.installed = false;

					uni.showModal({
						title: '更新失败,请重新下载',
						content: err.message,
						showCancel: false
					});
				});

				// 非wgt包,安装跳出覆盖安装,此处直接返回上一页
				if (!this.isWGT && !this.is_mandatory) {
					this.goBack()
				}
				// #endif
			},
			restart() {
				this.installed = false;
				// #ifdef APP-PLUS
				//更新完重启app
				plus.runtime.restart();
				// #endif
			},
			saveFile(tempFilePath, version) {
				return new Promise((resolve, reject) => {
					uni.saveFile({
						tempFilePath,
						success({
							savedFilePath
						}) {
							uni.setStorageSync(localFilePathKey, {
								version,
								savedFilePath
							})
						},
						complete() {
							resolve()
						}
					})
				})
			},
			deleteSavedFile(filePath) {
				uni.removeStorageSync(localFilePathKey)
				return uni.removeSavedFile({
					filePath
				})
			},
			jumpToAppStore() {
				plus.runtime.openURL(this.url);
			}
		}
	}
</script>

<style>
	page {
		background: transparent;
	}

	.flex-center {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		justify-content: center;
		align-items: center;
	}

	.mask {
		position: fixed;
		left: 0;
		top: 0;
		right: 0;
		bottom: 0;
		background-color: rgba(0, 0, 0, .65);
	}

	.botton-radius {
		border-radius: 30rpx;
	}

	.content {
		position: relative;
		top: 0;
		width: 600rpx;
		background-color: #fff;
		box-sizing: border-box;
		padding: 0 50rpx;
		font-family: Source Han Sans CN;
	}

	.text {
		/* #ifndef APP-NVUE */
		display: block;
		/* #endif */
		line-height: 200px;
		text-align: center;
		color: #FFFFFF;
	}

	.content-top {
		position: absolute;
		top: -195rpx;
		left: 0;
		width: 600rpx;
		height: 270rpx;
	}

	.content-top-text {
		font-size: 45rpx;
		font-weight: bold;
		color: #F8F8FA;
		position: absolute;
		top: 120rpx;
		left: 50rpx;
		z-index: 1;
		.content-top-text-version{
			font-size: 24rpx;
		}
	}
	.no-imgae .content-top{
		padding-top:30px;
		height: auto;
	}
	.no-imgae .content-top,.no-imgae .content-top .content-top-text{
		position: relative;
		top:0;
		left:0;
		color:#000;
		width:100%;
	}
	.content-header {
		height: 70rpx;
	}

	.title {
		font-size: 33rpx;
		font-weight: bold;
		color: #3DA7FF;
		line-height: 38px;
	}

	.footer {
		height: 150rpx;
		display: flex;
		align-items: center;
		justify-content: space-around;
	}

	.box-des-scroll {
		box-sizing: border-box;
		min-height: 100rpx;
		max-height: 400rpx;
		text-align: left;
	}

	.box-des {
		font-size: 26rpx;
		color: #000000;
		line-height: 50rpx;
	}

	.progress-box {
		width: 100%;
	}

	.progress {
		width: 90%;
		height: 40rpx;
		border-radius: 35px;
	}

	.close-img {
		width: 70rpx;
		height: 70rpx;
		z-index: 1000;
		position: absolute;
		bottom: -120rpx;
		left: calc(50% - 70rpx / 2);
		font-size: 60rpx;
		color:#fff;
	}

	.content-button {
		text-align: center;
		flex: 1;
		font-size: 30rpx;
		font-weight: 400;
		color: #FFFFFF;
		border-radius: 40rpx;
		margin: 0 18rpx;
		height: 80rpx;
		line-height: 80rpx;
		background: linear-gradient(to right, #1785ff, #3DA7FF);
	}
	.content-button.button-hover {
	  transform: translate(1rpx, 1rpx);
	}
	.flex-column {
		display: flex;
		flex-direction: column;
		align-items: center;
	}
</style>
相关推荐
耶啵奶膘2 小时前
uniapp+vue2全局监听退出小程序清除缓存
小程序·uni-app
我开心就好o9 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
Random_index9 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
初遇你时动了情16 小时前
uniapp 城市选择插件
开发语言·javascript·uni-app
小小黑00719 小时前
uniapp+vue3+ts H5端使用Quill富文本插件以及解决上传图片反显的问题
uni-app·vue
草字19 小时前
uniapp input限制输入负数,以及保留小数点两位.
java·前端·uni-app
前端小胡兔20 小时前
uniapp rpx兼容平板
uni-app
荔枝吖20 小时前
uniapp实现开发遇到过的问题(持续更新中....)
uni-app
艾小逗20 小时前
uniapp将图片url转换成base64支持app和h5
uni-app·base64·imagetobase64
halo14161 天前
uni-app 界面TabBar中间大图标设置的两种方法
开发语言·javascript·uni-app