Sivo 悉见

uni-app 实现资源在线升级/热更新

匿名 · 更新于 2024/7/28

一、前言

使用 uni-app 开发跨终端应用,可将代码编译到iOSAndroid、微信小程序等多个平台,升级时也需考虑多平台同步升级。其中,uni-app发布为小程序的升级模式较简单,只需将开发完的代码提交到小程序后台,待审核通过后用户将自动升级。

1.1 Android升级检测机制

android系统使用包名(package name)来判定应用程序的同一性,但是由于包名可以由开发者自由设置,为了保护应用程序不被其他开发者开发的同包名应用覆盖,用于发布的Android应用程序需要加上开发者签名。签名是保证app不被第三方恶意替换。

在应用程序被升级的时候,Android系统将会验证被升级的应用程序包与升级后的应用程序包是否使用了同样的开发者签名,如果一致,该应用程序可以被升级;如果不一致,那么将被视为非同一开发者开发的应用程序,用户需要先卸载已经安装的应用,然后再安装新应用。在卸载的过程中,应用在android系统中所保存的设置信息(SavedPreferences)将被删除,以保护应用本地保存的资料不被盗取。

根据现有应用市场的升级规则,包名一致,才会提醒升级,升级时,签名不一致,无法升级

检测到包名一致且版本不一致 ——> 提示升级 ——> 签名不一致 ——> 升级失败。

包名一致签名一致,就升级成功了。

HBuilderX 1.6.5 起,uni-app 支持生成 App 资源升级包wgt

二、wgt 资源升级包升级

wgt文件称为使用浏览器作为一个小型的Web应用程序部件,这些文件包含有关部件配置,图像,索引,样式表,JavaScript等信息,通常是在一个压缩归档格式。解压后的文件目录结构如下:

在这里插入图片描述

全量apk包目录结构如下:

在这里插入图片描述

2.1 修改版本号

首先,更新 manifest.json 中的版本号。比如之前是 1.0.0,那么新版本应该是 1.0.1 或 1.1.0 。

在这里插入图片描述

2.2 发行

然后,在 HBuilderX 中生成升级包(wgt)。

菜单 -> 发行 -> 原生App-制作移动App资源升级包

在这里插入图片描述

生成结束会在控制台告知升级包的输出位置。

在这里插入图片描述

2.3 安装资源升级包

应用升级需要服务端与客户端配合完成,下面以本地测试过程中的操作举例说明:

存放资源
将 %appid%.wgt 文件存放在服务器的 static 目录下,即http://www.example.com/static/UNI832D722.wgt

服务端接口
约定检测升级的接口,地址为:http://www.example.com/update/

传入参数

返回参数

2.3.1 代码示例

下面是一个简单的服务端判定的示例,仅做参考,实际开发中根据自身业务需求处理。

var express = require('express');  
var router = express.Router();  
var db = require('./db');  

// TODO 查询配置文件或者数据库信息来确认是否有更新  
function checkUpdate(params, callback) {  
    db.query('一段SQL', function(error, result) {  
        // 这里简单判定下,不相等就是有更新。  
        var currentVersions = params.appVersion.split('.');  
        var resultVersions = result.appVersion.split('.');  

        if (currentVersions[0] < resultVersions[0]) {  
            // 说明有大版本更新  
            callback({  
                update: true,  
                wgtUrl: '',  
                pkgUrl: result.pkgUrl  
            })  
        } else {  
            // 其它情况均认为是小版本更新  
            callback({  
                update: true,  
                wgtUrl: result.wgtUrl,  
                pkgUrl: ''  
            })  
        }  
    });  
}  

router.get('/update/', function(req, res) {  
    var appName = req.query.name;  
    var appVersion = req.query.version;  
    checkUpdate({  
        appName: appName,  
        appVersion: appVersion  
    }, function(error, result) {  
        if (error) {  
            throw error;  
        }  
        res.json(result);  
    });  
});

注意事项⚠️:

客户端检测升级
在 App.vue 的 onLaunch 中检测升级,代码如下:

// #ifdef APP-PLUS  
plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {  
    uni.request({  
        url: 'http://www.example.com/update/',  
        data: {  
            version: widgetInfo.version,  
            name: widgetInfo.name  
        },  
        success: (result) => {  
            var data = result.data;  
            if (data.update && data.wgtUrl) {  
                uni.downloadFile({  
                    url: data.wgtUrl,  
                    success: (downloadResult) => {  
                        if (downloadResult.statusCode === 200) {  
                            plus.runtime.install(downloadResult.tempFilePath, {  
                                force: false  
                            }, function() {  
                                console.log('install success...');  
                                plus.runtime.restart();  
                            }, function(e) {  
                                console.error('install fail...');  
                            });  
                        }  
                    }  
                });  
            }  
        }  
    });  
});  
// #endif

不支持资源升级包情况如下:

注意事项⚠️

但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成黑客可篡改其他App的数据。

使用热更新需要注意⚠️:

2.3.2 小结

用户对于热更新及整包更新的感知几乎是无差别的,同样需要主动触发用户下载安装,只不过包的大小不一致。

三、整包升级

接口约定
如下数据接口约定仅为示例,开发者可以自定义接口参数。

请求地址:https://www.example.com/update

请求方法:GET

请求数据:

{  
    "appid": plus.runtime.appid,  
    "platform": platformFlag,
    "version": plus.runtime.version  
}  

其中,platformFlag为手机系统标识,可通过如下方法获取:

async initSystemInfo() {
	uni.getSystemInfo({
		success(e) {
			console.log('The system info e is: ', e)
			// #ifndef MP
			getApp().globalData.deviceInfo = e
			Vue.prototype.StatusBar = e.statusBarHeight;
			if (e.platform === 'android') {
				getApp().globalData.deviceInfo.platformFlag = '0' // Android系统标识
				Vue.prototype.CustomBar = e.statusBarHeight + 50;
			} else {
				getApp().globalData.deviceInfo.platformFlag = '1' // IOS系统标识
				Vue.prototype.CustomBar = e.statusBarHeight + 43;
			}
			// #endif
			// #ifdef MP-WEIXIN
			Vue.prototype.StatusBar = e.statusBarHeight;
			const custom = wx.getMenuButtonBoundingClientRect();
			Vue.prototype.Custom = custom;
			Vue.prototype.CustomBar = custom.top - e.statusBarHeight;
			// #endif
			// #ifdef MP-ALIPAY
			Vue.prototype.StatusBar = e.statusBarHeight;
			Vue.prototype.CustomBar = e.statusBarHeight + e.titleBarHeight;
			// #endif
		}
	});
},

响应数据:

{  
    "status":1,//升级标志,1:需要升级;0:无需升级  `在这里插入代码片`
    "note": "修复bug1;\n修复bug2;",//release notes  
    "url": "http://www.example.com/uniapp.apk" //更新包下载地址  
} 

3.1 客户端实现

App启动时,向服务端上报当前版本号,服务端判断是否提示升级。

App.vueonLaunch中,发起升级检测请求,如下:

onLaunch: function () {  
    //#ifdef APP-PLUS  
    var server = "https://www.example.com/update"; //检查更新地址  
    var req = { //升级检测数据  
        "appid": plus.runtime.appid,  
        "version": plus.runtime.version  
    };  
    uni.request({  
        url: server,  
        data: req,  
        success: (res) =&gt; {  
            if (res.statusCode == 200 &amp;&amp; res.data.status === 1) {  
                uni.showModal({ //提醒用户更新  
                    title: "更新提示",  
                    content: res.data.note,  
                    success: (res) =&gt; {  
                        if (res.confirm) {  
							switch (uni.getSystemInfoSync().platform) {
								case 'android':
									// 开始下载任务
									const downloadTask = uni.downloadFile({
										url: data.url,
										// 接口调用成功
										success: (downloadResult) => {
											uni.hideLoading();
											console.log('----------------downloadResult----------------', downloadResult)
											if (downloadResult.statusCode === 200) {
												// uni.showLoading({
												// 	mask:true,
												// 	title: '安装中...'
												// });
												plus.runtime.install(downloadResult.tempFilePath, {
													force: false
												}, function() {
													// uni.hideLoading();
													console.log('install success...');
													plus.runtime.restart();
												}, function(e) {
													console.error('install fail...');
												});
											}
										},
										// 接口调用失败
										fail: (err) => {
											console.log('----------------Fail----------------', err);
											uni.showToast({
												icon:'none',
												mask:true,
												title: '安装失败,请重新下载',
											});
										},
										// 接口调用结束
										complete: () => {
											console.log('----------------Complete----------------:', downloadTask)
											downloadTask.offProgressUpdate(); //取消监听加载进度
										}
									});
		
									//监听下载进度
									downloadTask.onProgressUpdate(res => {
										// _self.percent = res.progress;
										state.percent = res.progress;
										// console.log('下载进度百分比:' + res.progress); // 下载进度百分比
										// console.log('已经下载的数据长度:' + res.totalBytesWritten); // 已经下载的数据长度,单位 Bytes
										// console.log('预期需要下载的数据总长度:' + res.totalBytesExpectedToWrite); // 预期需要下载的数据总长度,单位 Bytes
									});
									break;
								case 'ios':
									// 跳转至Apple Store
									let appleId=1549638327 //app的appleId
									plus.runtime.launchApplication({
									  action: `itms-apps://itunes.apple.com/cn/app/id${appleId}?mt=8`
									}, function(e) {
									  console.log('Open system default browser failed: ' + e.message);
									});
									break;
							}
                        }  
                    }  
                })  
            }  
        }  
    })  
    //#endif  
}  

注意⚠️:App的升级检测代码必须使用条件编译,否则在微信环境由于不存在plus相关API,将会报错。

3.2 数据表实现

需维护一张数据表,用于维护APP版本信息,主要字段信息如下:

字段名称 数据类型 数据说明
AppID varchar mobile AppID
version varchar 应用市场版本号
notes varchar 版本更新说明
url varchar 应用市场下载URL。 注意:⚠️根据谷歌、App Store应用市场审核规范,应用升级只能通过提交应用市场更新,不能通过下载apkIPA安装方式更新应用。

3.3 服务端实现

根据客户端接收的版本号,比对服务端最新版本号,决定是否需要升级,若需升级则返回升级信息(rlease notes更新包地址等)

开发者可以根据服务端开发语言,自己实现升级检测逻辑,如下是一个php示例代码:

header("Content-type:text/json");  
$appid = $_GET["appid"];  
$version = $_GET["version"]; //客户端版本号  
$rsp = array("status" =&gt; 0); //默认返回值,不需要升级  
if (isset($appid) &amp;&amp; isset($version)) {  
    if ($appid === "__UNI__123456") { //校验appid  
        if ($version !== "1.0.1") { //这里是示例代码,真实业务上,最新版本号及relase notes可以存储在数据库或文件中  
            $rsp["status"] = 1;  
            $rsp["note"] = "修复bug1;\n修复bug2;"; //release notes  
            $rsp["url"] = "http://www.example.com/uniapp.apk"; //应用升级包下载地址  
        }  
    }  
}   
echo json_encode($rsp);  
exit;  

注意事项⚠️:

3.4 发版配置

APP发版时,应注意维护manifest.jsonversionNameversionCode字段,以免影响版本升级。

在这里插入图片描述

3.5 注意事项

plus.runtime.version 为属性方式同步获取,拿到的是编译阶段manifest.json中设置的apk/ipa版本号,整包更新后的版本号,在manifest.json中配置时需要提交到App云端打包后才能生效。

plus.runtime.getProperty 为异步方法调用获取,拿到的当前应用的版本号,如果热更新过就是热更新后的版本号。

plus.runtime.getProperty(plus.runtime.appid, function(widgetInfo) {
	console.log(widgetInfo.version);
})

四、Uni-app 版本升级中心

uni-app提供了一整套版本维护框架,包含升级中心uni-upgrade-center - Admin、前台检测更新uni-upgrade-center-app

4.1 升级中心 uni-upgrade-center - Admin

uni-app提供了版本维护后台应用升级中心uni-upgrade-center - Admin,升级中心是一款uni-admin插件,负责App版本更新业务。包含后台管理界面、更新检查逻辑,App内只要调用弹出提示即可。

在这里插入图片描述

在上传安装包界面填写此次发版信息,其中包地址可以选择手动上传一个文件到云存储,会自动将地址填入该项。

也可以手动填写一个地址(例如:https://appgallery.huawei.com/app/C10764638),就可以不用再上传文件。

⚠️如果是发布苹果版本,包地址则为应用在AppStore的链接。

在这里插入图片描述

升级中心有以下功能点:

4.2 前台检测更新 uni-upgrade-center-app

uni-upgrade-center-app 负责前台检查升级更新。

项目结构如下图所示:
在这里插入图片描述

检测更新视图如下图所示:

在这里插入图片描述 在这里插入图片描述

该插件提供如下功能:

注意:⚠️在手机基座上运行时获取到的版本号appidhbuilderhbuilder的版本需要在文件里面手动设置。

4.3 工作原理

升级中心uni-upgrade-center - Admin负责维护版本信息,并将版本信息维护至数据库中。
前台检测更新插件uni-upgrade-center-app负责提供调用云函数读取数据库维护的版本信息一键式检查更新。

五、特别注意事项


1、uni-admin需要独立域名部署
2、uni-admin需要绑定uniCloud
3、uni-app项目也要绑定和uni-admin相同的uniCloud
4、uni-app项目要安装uni-upgrade-center
5、uni-app项目pages.json要配置如下路由

 
{
		  "path": "uni_modules/uni-upgrade-center-app/pages/upgrade-popup",
		  "style": {
			  "disableScroll": true,
			  "app-plus": {
				  "backgroundColorTop": "transparent",
				  "background": "transparent",
				  "titleNView": false,
				  "scrollIndicator": false,
				  "popGesture": "none",
				  "animationType": "fade-in",
				  "animationDuration": 200
			  }
		  }
}
 
6、uni-app的app.vue要加入以下代码
// #ifdef APP-PLUS
import checkUpdate from '@/uni_modules/uni-upgrade-center-app/utils/check-update'
import {nextTick} from "vue";
// #endif
onLaunch(() => {
  console.log("App Launch");
  // #ifdef APP-PLUS
  // console.log("检测新版本")
  nextTick(async () => {
    setTimeout(async () => {
      const res = await checkUpdate()
      console.log("检测结果", res)
    }, 3000)
  })
  // #endif
});