uniapp实现热更新/带进度条/附上源码

1,395 阅读4分钟

什么是热更新

上线后玩家下载的第一个版本(30M~200M),在运营过程中,如果需要更换UI显示,或者修改页面的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载(浪费流量和时间,体验不好)。 热更新可以在不重新下载客户端的情况下更新app的内容,下面是我实现的过程,希望对各位技术人员有参考价值!

需要服务器存放更新包资源,后端提供接口用于检测当前版本是否为最新版本

  • 服务器中存储着最新版本号,前端进行查询
  • 可以在首次进入应用时进行请求版本号进行一个匹对
  • 如果版本号一致则不提示,反之则提示进行更新执行更新操作

效果图

f1254295caa2d32972f42fa0687bd30.png

d15a34a8be8d9b6edd24596ef7f4634.png

image.png

属性注释


export default {
		name: "appUpdate",
		//@是否强制更新
		props: { 
			tabbar: {
				type: Boolean,
				default: false, //是否有原生tabbar组件
			},
			showTip: {
				type: Boolean,
				default: false, //是否有原生tabbar组件
			},
		},
		data() {
			return {
				popup_show: false, //弹窗是否显示
				platform: "", //ios or android
				version: "", //当前软件版本
				need_update: false, // 是否更新
				isforce: true, // 是否更新
				downing: false, //是否下载中
				downstatus: 0, //0未下载  1已开始 2已连接到资源  3已接收到数据  4下载完成
				update_info: {
					os: '', //设备系统
					version: '', //最新版本
					note: '', //升级说明
				},
				fileSize: 0, //文件大小
				downSize: 0, //已下载大小
				viewObj: null, //原生遮罩view
			};
		},

1.封装一个对比版本号的函数

// 对比版本号
			compareVersion(ov, nv) {
						// console.log('对比版本号compareVersion, ov: ', ov);
						// console.log('对比版本号compareVersion, nv: ', nv);
				if (!ov || !nv || ov == "" || nv == "") {
					return false;
				}
				let b = false,
					ova = ov.split(".", 4),
					nva = nv.split(".", 4);
						console.log('对比版本号compareVersion, nva: ', nva);
						console.log('对比版本号compareVersion, ova: ', ova);
				for (let i = 0; i < ova.length && i < nva.length; i++) {
					let so = ova[i],
						no = parseInt(so),
						sn = nva[i],
						nn = parseInt(sn);
					if (nn > no || sn.length > so.length) {
						return true;
					} else if (nn < no) {
						return false;
					}
				}
				if (nva.length > ova.length && 0 == nv.indexOf(ov)) {
					return true;
				} else {
					return false;
				}
			},

2.获取当前应用app版本

created() {
	vm = this;
	// #ifdef APP-PLUS
	// 获取手机系统信息
        // 保存 app 版本信息
	uni.getSystemInfo({ success: function(res) {
		vm.platform = res.platform //ios  or android
		}
	});
	plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { 
		vm.version = wgtinfo.version
	})
},

3.获取线上版本信息

// 获取线上版本信息
			getUpdateInfo() {
			var self = this;
			//向后台发起请求,获取最新版本号
				console.log('获取线上版本信息  res')
			vm.$api({
				url: self.$config.cardApiUrl+'version/hot',
				auth:false,
				method: 'POST',
				desc:'api获取后台app版本信息'
			}).then(res => {
					console.log('test_version.index  res',res)
					console.log('test_version.index  vm.platform',vm.platform)
				   if (res&&res.code == 200) {
					if( res.isshenhe && res.isshenhe >= 1 ){
						if( vm.systemType!=2 &&res.data[vm.platform] && res.data[vm.platform].isgoods==2){
						vm.setSystemType(2) //自己定义的方法可以不用管
						vm.setPageCur('creditHome')
					}
					vm.update_info = {
						...res.data[vm.platform],
						os: vm.platform
					}
					vm.isforce = res.data[vm.platform].isforce==1? true : false;

								console.log('后台返回的当前系统的升级参数', vm.update_info);
					if (!vm.update_info.os) {
						// 后台未配置当前系统的升级数据
					} else {
							vm.checkUpdate(); ///检查是否更新
						} 
						}
					}
				}).catch(err => {
					console.log('app_version err', err)
				})  
			},

4.检查是否更新

checkUpdate() {
// uni.showModal({
// 	content:'测试热更新'+vm.version+'--'+vm.update_info.version
// })
vm.need_update = vm.compareVersion(vm.version, vm.update_info.version); // 检查是否需要升级
	if (vm.need_update) {
		vm.popup_show = true; //线上版本号大于当前安装的版本号  显示升级框
		/* if (vm.tabbar) {
			//页面是否有原生tabbar组件
			// 创建原生view用来遮罩tabbar的点击事件 (如果是没有用原生的tabbar这一步可以取			vm.viewObj = new plus.nativeObj.View('viewObj', {
				bottom: '0px',
				left: '0px',
				height: '50px',
				width: '100%',
				backgroundColor: "rgba(0,0,0,.6)"
			});
			vm.viewObj.show() //显示原生遮罩
				} */
			}else{
				if(vm.showTip){
					uni.showToast({
						title:'当前为最新版本!',icon:'none'
					})
				}
			}
			},

5.封装函数

取消更新函数

closeUpdate() {
				if (vm.isforce) {
					// 强制更新,取消退出app
					plus.os.name == "Android" ? plus.runtime.quit() : plus.ios.import("UIApplication").sharedApplication().performSelector("exit");
				} else {
					vm.popup_show = false; //关闭升级弹窗
					// if (vm.viewObj) vm.viewObj.hide() //隐藏原生遮罩
				}
			},

立即更新函数

// 立即更新
	nowUpdate() {
		if (vm.downing) return false; //如果正在下载就停止操作
		vm.downing = true; //状态改变 正在下载中
		if(vm.platform=='ios' && vm.update_info.iswgt==2){ //苹果 整包更新
		plus.runtime.launchApplication({
			action: `itms-apps://itunes.apple.com/cn/app/id${vm.update_info.appid}?mt=8`
			}, function(e) {
				console.log('打开Apple Store错误: ', e);
				plus.nativeUI.toast("打开Apple Store错误,请手动前往搜索!");
			});
			}else if (/\.apk$/.test(vm.update_info.download_url)) { //安卓 整包更新
				// 如果是apk地址
				vm.download_wgt();
			} else if (/\.wgt$/.test(vm.update_info.download_url)) { //热更新
				// 如果是更新包
				vm.download_wgt();
			} else { //打开外部浏览器 更新地址
			plus.runtime.openURL(vm.update_info.download_url, function(e) { 
					console.log('打开外部浏览器错误: ', e);
					plus.nativeUI.toast("打开外部浏览器错误");
				});
			}
			},

下载资源包函数

// 下载升级资源包
download_wgt() {
			plus.nativeUI.showWaiting(""); //下载更新文件...
			let options = {
				method: "get"
			};
			/* if(vm.update_info.download_url.indexOf('web/')==-1){
				var _newUrl = vm.update_info.download_url.split('Uploads/');
				vm.update_info.download_url = _newUrl[0]+'web/Uploads/'+_newUrl[1];
	                 } */
          let dtask = plus.downloader.createDownload(vm.update_info.download_url, options, function(d, status) {});
		dtask.addEventListener("statechanged", function(task, status) {
			if (status === null) {} else if (status == 200) {
			//在这里打印会不停的执行,请注意,正式上线切记不要在这里打印东西///////////////////////////////////////////////////
			      vm.downstatus = task.state;
			      switch (task.state) {
				case 3: // 已接收到数据  
					vm.downSize = task.downloadedSize;
					if (task.totalSize) {
					vm.fileSize = task.totalSize; //服务器须返回正确的content-length才会有长度
					}
					break;
				        case 4:
					vm.installWgt(task.filename); // 安装wgt包  
					break;
					}
					} else {
						plus.nativeUI.closeWaiting();
						plus.nativeUI.toast("下载出错");
						vm.downing = false;
						vm.downstatus = 0;
					}
				});
				dtask.start();
			},

安装文件函数

installWgt(path) {
				plus.nativeUI.showWaiting(""); //安装更新文件...
				plus.runtime.install(path, {force:true}, function() {
					plus.nativeUI.closeWaiting();
					// 应用资源下载完成!
					plus.nativeUI.alert("资源下载完成!", function() {
						plus.runtime.restart();
					});
				}, function(e) {
					plus.nativeUI.closeWaiting();
					// 安装更新文件失败
					plus.nativeUI.alert("安装更新文件失败[" + e.code + "]:" + e.message);
				});
			},

最后index页面使用

image.png

image.png

最后贴上完整代码

<template>
	<view class="wrap" v-if="popup_show">
		<view class="popup-bg" :style="getHeight">
			<view class="popup-content" :class="{'popup-content-show' : popup_show}">
				<view class="update-wrap">
					<image src="./images/img.png" class="top-img"></image>
					<view class="content">
						<text class="title">发现新版本:v{{update_info.version}}</text>
						<!-- 升级描述 -->
						<view class="title-sub" v-html="update_info.note"></view>
						<!-- 升级按钮 -->
						<button class="btn" v-if="downstatus < 1" @click="nowUpdate()">立即升级</button>
						<!-- 下载进度 -->
						<view class="sche-wrap" v-else>
							<!-- 更新包下载中 -->
							<view class="sche-bg">
								<view class="sche-bg-jindu" :style="lengthWidth"></view>
							</view>
							<text class="down-text">下载进度:{{(downSize/1024/1024 ).toFixed(2)}}M/{{(fileSize/1024/1024).toFixed(2)}}M</text>
						</view>
						<view class="tip">
							手动更新:"我的"界面,点击右上角进入
						</view>
					</view>
				</view>
				<image src="./images/close.png" class="close-ioc" @click="closeUpdate()" v-if="!force"></image>
			</view>

		</view>
	</view>
</template>

<script>
	let vm;
	import { mapActions, mapState } from 'vuex';
	export default {
		name: "appUpdate",
		//@是否强制更新
		props: { 
			tabbar: {
				type: Boolean,
				default: false, //是否有原生tabbar组件
			},
			showTip: {
				type: Boolean,
				default: false, //是否有原生tabbar组件
			},
		},
		data() {
			return {
				popup_show: false, //弹窗是否显示
				platform: "", //ios or android
				version: "", //当前软件版本
				need_update: false, // 是否更新
				isforce: true, // 是否更新
				downing: false, //是否下载中
				downstatus: 0, //0未下载  1已开始 2已连接到资源  3已接收到数据  4下载完成
				update_info: {
					os: '', //设备系统
					version: '', //最新版本
					note: '', //升级说明
				},
				fileSize: 0, //文件大小
				downSize: 0, //已下载大小
				viewObj: null, //原生遮罩view
			};
		},
		created() {
			vm = this;
			// #ifdef APP-PLUS
			// 获取手机系统信息
			uni.getSystemInfo({ success: function(res) {
					vm.platform = res.platform //ios  or android
				}
			});
			plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => { 
				vm.version = wgtinfo.version
			})
			// #endif
		},
		computed: {
			...mapState({
				systemType: state => state.init.systemType,
			}),
			// 下载进度计算
			lengthWidth: function() {
				let w = this.downSize / this.fileSize * 100;
				if (!w) {
					w = 0
				} else {
					w = w.toFixed(2)
				}
				return {
					width: w + "%" //return 宽度半分比
				}
			},
			getHeight: function() {
				let bottom = 0;
				if (this.tabbar) {
					bottom = 50;
				}
				return {
					"bottom": bottom + 'px',
					"height": "auto"
				}
			}
		},
		methods: {
			...mapActions(['setPageCur','setSystemType']),
			// 检查更新
			update() {
				console.log('父组件触发方法');
				// #ifdef APP-PLUS
				console.log('当前版本', vm.version);
				vm.getUpdateInfo(); //获取更新信息
				// #endif
			}, 
			// 获取线上版本信息
			getUpdateInfo() {
				var self = this;
				//向后台发起请求,获取最新版本号
					console.log('获取线上版本信息  res')
				vm.$api({
					url: self.$config.cardApiUrl+'version/hot',
					auth:false,
					method: 'POST',
					desc:'api获取后台app版本信息'
				}).then(res => {
					console.log('test_version.index  res',res)
					console.log('test_version.index  vm.platform',vm.platform)
					if (res&&res.code == 200) {
						if( res.isshenhe && res.isshenhe >= 1 ){
							if( vm.systemType!=2 &&res.data[vm.platform] && res.data[vm.platform].isgoods==2){
								vm.setSystemType(2)
								vm.setPageCur('creditHome')
							}
							vm.update_info = {
								...res.data[vm.platform],
								os: vm.platform
							}
							vm.isforce = res.data[vm.platform].isforce==1? true : false;

								console.log('后台返回的当前系统的升级参数', vm.update_info);
							if (!vm.update_info.os) {
								// 后台未配置当前系统的升级数据
							} else {
								vm.checkUpdate(); ///检查是否更新
							} 
						}
					}
				}).catch(err => {
					console.log('app_version err', err)
				})  
			},
			// 检查是否更新
			checkUpdate() {
				// uni.showModal({
				// 	content:'测试热更新'+vm.version+'--'+vm.update_info.version
				// })
				vm.need_update = vm.compareVersion(vm.version, vm.update_info.version); // 检查是否需要升级
				if (vm.need_update) {
					vm.popup_show = true; //线上版本号大于当前安装的版本号  显示升级框
					/* if (vm.tabbar) {
						//页面是否有原生tabbar组件
						// 创建原生view用来遮罩tabbar的点击事件 (如果是没有用原生的tabbar这一步可以取消)
						vm.viewObj = new plus.nativeObj.View('viewObj', {
							bottom: '0px',
							left: '0px',
							height: '50px',
							width: '100%',
							backgroundColor: "rgba(0,0,0,.6)"
						});
						vm.viewObj.show() //显示原生遮罩
					} */
				}else{
					if(vm.showTip){
						uni.showToast({
							title:'当前为最新版本!',icon:'none'
						})
					}
				}
			},

			// 取消更新
			closeUpdate() {
				if (vm.isforce) {
					// 强制更新,取消退出app
					plus.os.name == "Android" ? plus.runtime.quit() : plus.ios.import("UIApplication").sharedApplication().performSelector("exit");
				} else {
					vm.popup_show = false; //关闭升级弹窗
					// if (vm.viewObj) vm.viewObj.hide() //隐藏原生遮罩
				}
			},
			// 立即更新
			nowUpdate() {
				if (vm.downing) return false; //如果正在下载就停止操作
				vm.downing = true; //状态改变 正在下载中
				if(vm.platform=='ios' && vm.update_info.iswgt==2){ //苹果 整包更新
					plus.runtime.launchApplication({
						action: `itms-apps://itunes.apple.com/cn/app/id${vm.update_info.appid}?mt=8`
					}, function(e) {
						console.log('打开Apple Store错误: ', e);
						plus.nativeUI.toast("打开Apple Store错误,请手动前往搜索!");
					});
				}else if (/\.apk$/.test(vm.update_info.download_url)) { //安卓 整包更新
					// 如果是apk地址
					vm.download_wgt();
				} else if (/\.wgt$/.test(vm.update_info.download_url)) { //热更新
					// 如果是更新包
					vm.download_wgt();
				} else { //打开外部浏览器 更新地址
					plus.runtime.openURL(vm.update_info.download_url, function(e) { 
						console.log('打开外部浏览器错误: ', e);
						plus.nativeUI.toast("打开外部浏览器错误");
					});
				}
			},
			// 下载升级资源包
			download_wgt() {
				plus.nativeUI.showWaiting(""); //下载更新文件...
				let options = {
					method: "get"
				};
				/* if(vm.update_info.download_url.indexOf('web/')==-1){
					var _newUrl = vm.update_info.download_url.split('Uploads/');
					vm.update_info.download_url = _newUrl[0]+'web/Uploads/'+_newUrl[1];
				} */
				let dtask = plus.downloader.createDownload(vm.update_info.download_url, options, function(d, status) {

				});

				dtask.addEventListener("statechanged", function(task, status) {
					if (status === null) {} else if (status == 200) {
						//在这里打印会不停的执行,请注意,正式上线切记不要在这里打印东西///////////////////////////////////////////////////
						vm.downstatus = task.state;
						switch (task.state) {
							case 3: // 已接收到数据  
								vm.downSize = task.downloadedSize;
								if (task.totalSize) {
									vm.fileSize = task.totalSize; //服务器须返回正确的content-length才会有长度
								}
								break;
							case 4:
								vm.installWgt(task.filename); // 安装wgt包  
								break;
						}
					} else {
						plus.nativeUI.closeWaiting();
						plus.nativeUI.toast("下载出错");
						vm.downing = false;
						vm.downstatus = 0;
					}
				});
				dtask.start();
			},

			// 安装文件
			installWgt(path) {
				plus.nativeUI.showWaiting(""); //安装更新文件...
				plus.runtime.install(path, {force:true}, function() {
					plus.nativeUI.closeWaiting();
					// 应用资源下载完成!
					plus.nativeUI.alert("资源下载完成!", function() {
						plus.runtime.restart();
					});
				}, function(e) {
					plus.nativeUI.closeWaiting();
					// 安装更新文件失败
					plus.nativeUI.alert("安装更新文件失败[" + e.code + "]:" + e.message);
				});
			},
			// 对比版本号
			compareVersion(ov, nv) {
						// console.log('对比版本号compareVersion, ov: ', ov);
						// console.log('对比版本号compareVersion, nv: ', nv);
				if (!ov || !nv || ov == "" || nv == "") {
					return false;
				}
				let b = false,
					ova = ov.split(".", 4),
					nva = nv.split(".", 4);
						console.log('对比版本号compareVersion, nva: ', nva);
						console.log('对比版本号compareVersion, ova: ', ova);
				for (let i = 0; i < ova.length && i < nva.length; i++) {
					let so = ova[i],
						no = parseInt(so),
						sn = nva[i],
						nn = parseInt(sn);
					if (nn > no || sn.length > so.length) {
						return true;
					} else if (nn < no) {
						return false;
					}
				}
				if (nva.length > ova.length && 0 == nv.indexOf(ov)) {
					return true;
				} else {
					return false;
				}
			},
		}
	}
</script>

<style lang="scss" scoped>
	.wrap{
		position: relative;
		z-index: 999;
	}
	.popup-bg {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		position: fixed;
		top: 0;
		left: 0rpx;
		right: 0;
		bottom: 0;
		width: 750rpx;
		background-color: rgba(0, 0, 0, .6);
	}

	.popup-content {
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.popup-content-show {
		animation: mymove 500ms;
		transform: scale(1);
	}

	@keyframes mymove {
		0% {
			transform: scale(0);
			/*开始为原始大小*/
		}

		100% {
			transform: scale(1);
		}

	}



	.update-wrap {
		width: 580rpx;
		border-radius: 18rpx;
		position: relative;
		display: flex;
		flex-direction: column;
		background-color: #ffffff;
		padding: 170rpx 30rpx 0;

		.top-img {
			position: absolute;
			left: 0;
			width: 100%;
			height: 256rpx;
			top: -128rpx;
		}

		.content {
			display: flex;
			flex-direction: column;
			align-items: center;
			padding-bottom: 40rpx;

			.title {
				font-size: 32rpx;
				font-weight: bold;
				color: #BE0608;
			}

			.title-sub {
				text-align: center;
				font-size: 24rpx;
				color: #666666;
				padding: 30rpx 0;
			}

			.btn {
				width: 460rpx;
				display: flex;
				align-items: center;
				justify-content: center;
				color: #ffffff;
				font-size: 30rpx;
				height: 80rpx;
				line-height: 80rpx;
				border-radius: 100px;
				background-color: #BE0608;
				margin-top: 20rpx;
			}
			.tip{
				padding:10rpx 0 0;
				font-size: 24rpx;color: #BE0608;text-align: center;
			}
		}
	}


	.close-ioc {
		width: 70rpx;
		height: 70rpx;
		margin-top: 30rpx;
	}

	.sche-wrap {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: flex-end;
		padding: 10rpx 50rpx 0;

		.sche-wrap-text {
			font-size: 24rpx;
			color: #666;
			margin-bottom: 20rpx;
		}

		.sche-bg {
			position: relative;
			background-color: #cccccc;
			height: 30rpx;
			border-radius: 100px;
			width: 480rpx;
			display: flex;
			align-items: center;

			.sche-bg-jindu {
				position: absolute;
				left: 0;
				top: 0;
				height: 30rpx;
				min-width: 40rpx;
				border-radius: 100px;
				background: url(images/round.png) #BE0608 center right 4rpx no-repeat;
				background-size: 26rpx 26rpx;
			}
		}

		.down-text {
			font-size: 24rpx;
			color: #be2b2d;
			margin-top: 16rpx;
		}
	}
</style>

最后在附上如何打包更新包

image.png

选择制作wgt包,然后点击生成就可以了,然后就等待打包,更新包一般比安装包快且没有次数限制

image.png

需要注意的是记得每次打包记得更改版本号跟版本名称这两个地方哦,不然的话会一直重复更新 一直跳出弹窗

image.png