背景:最近需要给app加一个可以检测到新版本并更新的功能,
之前没有考虑过这个问题,第一次尝试,特此记录一下。
我在这里使用到了uniapp上的更新插件,并在此插件基础上进行更改以适应我的项目。
插件链接:https://ext.dcloud.net.cn/plugin?id=2144,感谢大哥!
一、思考
想法:我这里的思路是app发送请求将app版本号发往后端服务器,与服务器上的apk版本号进行比对(也可以从服务器获取到最新版本的版本号返回到前端进行比对,这里就是仁者见仁了),如果有新版本会返回最新版本的url下载地址,在ios中则是跳到app store上进行下载。
二、操作
所需的操作:
要实现这个检测更新的功能,我们肯定是需要在应用启动的时候就运行,
所以我们就需要在App.vue中设置内容,然后把我们更新相关的操作可以
放在一个单独的js文件中;后端方面可以写一个接口文件里面写版本更新
相关的逻辑,还需要有一个/apk目录存放新版本安装包,以供安卓用户直
接从这里下载,实现应用内的下载安装。所以说,前端有两个文件,后端
有两个文件,我们下面就是围绕这几个文件描述。
2.1、App.vue
这个文件中需要新增的代码就在下面
import checkappupdate from 'js_sdk/wonyes-checkappupdate/wonyes/checkappupdate.js'
checkappupdate.check({
title:"检测到有新版本!",
content:"请升级app到最新版本!",
canceltext:"暂不升级",
oktext:"立即升级",
api:'https://xxx.xxxx.xxx/api/Update/renew',
barbackground:"rgba(50,50,50,0.8)",//进度条背景色,默认灰色,可自定义rgba形式色值
barbackgroundactive:"rgba(32,165,58,1)"//进度条前景色色,默认绿色,可自定义rgba形式色值
})
注意:上面的导入时我在使用插件时的目录,如果这个js文件你要单独新建的话,记得把这里的路径修改一下,能访问到js文件就行。下面的这个check方法的使用需要放在app.vue的onLaunch里,在api那里xxx.xxxx.xxx是你的域名,这个URL实际上就是访问后端更新相关逻辑接口的URL,为了保持一致,你可以根据自己的项目接口的访问规则自行修改即可。
2.2、checkappupdate.js
这个文件就是前端我们发送请求,接受返回数据,判断是否更新,更新提示框,下载apk等存放相关内容的js文件,使用插件的话的也是直接就有的,自己创建的话也可以,我在下方放上我的代码以供参考,但是无论是使用插件还是使用我的,记得修改一些内容已适配自己的逻辑需求。
function check(param = {}) {
console.log("开始检查版本更新!");
// 合并默认参数
param = Object.assign({
title: "检测到有新版本!",
content: "请升级app到最新版本!",
canceltext: "暂不升级",
oktext: "立即升级",
barbackground:"rgba(50,50,50,0.8)",
barbackgroundactive:"rgba(32,165,58,1)"
}, param)
if (!param.api) {
console.log("api有误!");
return false;
}
plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
// console.log(widgetInfo)
let platform = plus.os.name.toLocaleLowerCase()
uni.request({
url: param['api'],
data: {
platform: platform,
version: widgetInfo.version
},
header: {
'content-type': 'application/x-www-form-urlencoded'
},
method: 'POST',
dataType: 'json',
success: (result) => {
console.log("检测更新,打印后端返回结果:");
console.log(result);
let data = result.data ? result.data : null
if (data && data.code === 0 && data.url) {
if(/\.wgt$/i.test(data.url) || (platform == 'android' && /\.apk$/i.test(data.url))){
// 如果是热更新 wgt 或 android平台下apk
startdownload(param,data);
return
}
if (platform == 'ios') {
// 如果是ios,则跳转到appstore
uni.showModal({
title: param.title,
content: data.log ? data.log : param.content,
showCancel: data.force ? false : true,
confirmText: param.oktext,
cancelText: param.canceltext,
success: res => {
if (!res.confirm) {
console.log('取消了升级');
return
}else{
plus.runtime.openURL(result.data.url)
}
}
});
}
}
},
fail: (res) => {
console.log("请求发送失败!");
console.log(res);
}
})
});
}
function startdownload(param,data){
uni.showModal({
title: param.title,
content: data.log ? data.log : param.content,
showCancel: data.force ? false : true,
confirmText: param.oktext,
cancelText: param.canceltext,
success: res => {
if (!res.confirm) {
console.log('取消了升级');
return
}
if (data.shichang === 1 && /\.apk$/i.test(data.url)) {
//去应用市场更新
plus.runtime.openURL(data.shichangurl);
plus.runtime.restart();
} else {
// 开始下载
// 创建下载任务
var dtask = plus.downloader.createDownload(data.url, {
filename: "_downloads/"
},
function(d, status) {
console.log('d',d)
// 下载完成
if (status == 200) {
plus.runtime.install(d.filename, {
force: false
}, function() {
//进行重新启动;
plus.runtime.restart();
}, (e) => {
uni.showToast({
title: '安装升级包失败:' + JSON.stringify(e),
icon: 'none'
})
});
} else {
plus.nativeUI.alert("下载升级包失败,请手动去站点下载安装,错误码: " + status);
}
});
let wrapwidth=parseInt(plus.screen.resolutionWidth / 2);
let view = new plus.nativeObj.View("maskView", {
backgroundColor: param.barbackground,
left: (wrapwidth/2) + "px",
bottom: "80px",
width: wrapwidth+"px",
height: "10px"
});
view.show()
let viewinner = new plus.nativeObj.View("maskViewinner", {
backgroundColor: param.barbackgroundactive,
left: (wrapwidth/2)+"px",
bottom: "80px",
width: "1px",
height: "10px"
});
viewinner.show();
dtask.addEventListener("statechanged", (e) => {
if (e && e.downloadedSize > 0) {
let jindu = parseInt((e.downloadedSize / e.totalSize)*wrapwidth)
viewinner.setStyle({width:jindu+'px'});
}
}, false);
dtask.start();
}
}
});
}
export default {
check
}
注意:在这个代码的操作下更新的进度条是在页面下方,不太明显,我试着不太好调也就没有改动,这里可根据自己情况自行更改,代码是let wrapwidth这里往下。
2.3、后端接口Update.php
这里就是后端接口了,还是那句话,插件和我的不一定合适,只供参考作用,需要根据自己实际的项目逻辑自行修改适配。
<?php
namespace app\api\controller;
use app\common\controller\Api;
class Update extends Api
{
protected $noNeedLogin = ['renew'];
protected $noNeedRight = ['*'];
// 判断2个版本号的大小,并返回较大的那个,比如 1.0.9 1.1.0 ,则返回 1.1.0
// 传入由2个点分隔为3组的版本号,返回较大的那个
// 1.0.3 1.2.9 等
function getbig($one,$two){
$onearr=explode('.',$one);
$twoarr=explode('.',$two);
if(intval($onearr[0]) !== intval($twoarr[0])){
return intval($onearr[0])>intval($twoarr[0])?$one:$two;
}
if(intval($onearr[1]) !== intval($twoarr[1])){
return intval($onearr[1])>intval($twoarr[1])?$one:$two;
}
if(intval($onearr[2]) !== intval($twoarr[2])){
return intval($onearr[2])>intval($twoarr[2])?$one:$two;
}
return $one;
}
public function renew()
{
//获取传递的参数
$postParams = $_POST;
//我这里是使用的thinkPHP后台,在后台中可以直接设置ios版本和下载地址,这里获取就行
$ios=\think\Config::get("site.iosVersion");
$iosurl=\think\Config::get("site.iosUrl");
// 示例代码 update.php,此示例代码要求符合如下规则
// 1. manifest.json->基础配置->应用版本名称的值必须是以2个点号分隔开的3组数字,即 "数字.数字.数字" 比如 1.0.2 2.1.3 等,不可含有其他字符,且形式必须符合 数字.数字.数字 的形式
// 2. 该示例代码文件同目录下有 apk wgt等 文件夹,此文件夹下存放以版本号命名的apk包或wgt包,如果apk和wgt包版本号相同,则优先返回wgt,如 1.0.2.apk 1.2.4.apk 1.3.4.wgt等
// 3. 更新检测接口地址 http://a.com/update.php, apk下载url地址 http://a.com/apk/数字.数字.数字.apk或wgt
//0 不强制更新,1强制更新
$force=0;
//更新日志信息也是在后台直接设置,这里获取
$log=\think\Config::get("site.reNewInfo");
// $new 存放最大的版本号,初始为 app中传来的版本号
$new=$_POST['version'];
// 存放apk或wgt的路径地址,根据自己服务器的路径规则设置,我这里使用了从根目录开始的路径
//由于我项目服务器上只有public目录下的文件运行直接访问,所以apk放在这里,如果你与我的不同,根据自己情况更改;$path=__DIR__.'/apk/';这是插件中的,它使用了先获取到当前文件目录,然后把apk目录与接口目录放在同一目录下
// 首先判断是否有待更新的资源 wgt
$path='/xxx/xxx/xxx/xxx.xxxx.xxx/public/apk/';
// 此处使用了exec调用linux上系统命令返回目录下所有的apk文件名称,需要php中启用exec函数
exec("find {$path} -type f -regex '.*\(apk\|wgt\)'",$out);
foreach ($out as $k=>$v){
// 循环将最大的版本号赋给$new
$new = $this->getbig($new, substr(basename($v), 0, -4));
}
header('Content-Type:application/json;charset=utf-8');
// 如果版本号发生了变化,且 是热更新资源wgt,则无论ios还是android均返回资源wgt
if($new !=$_POST['version'] && file_exists($path.$new.'.wgt')){
// 如果是wgt,则无论ios还是android都返回
echo json_encode([
'code'=>0,
'msg'=>'ok',
'version'=>$new,
'url'=>'https://xxx.xxxx.xxx/apk/'.$new.'.wgt',
'log' => '新版本:v'.$new."\n".$log,
'force'=>$force
]);
exit;
}
// IOS 更新判断
// $_POST 请求数据为 [version=>'数字.数字.数字 形式版本号',platform=>'android或ios']
if($_POST['platform']=='ios'){
// 如果是ios,则直接填写最新版本号和商店下载地址
$new=$ios;//最新的版本号
$url=$iosurl;//苹果商店地址
// getbig($new,$_POST['version']) 函数会返回 所传入版本号参数中较大的那个
$big = $this->getbig($new,$_POST['version']);
if($_POST['version'] != $big){
// 如果返回值不等于 $_POST['version'],说明 $new 是新版本
// 返回地址
echo json_encode([
'code'=>0,
'msg'=>'ok',
'version'=>$new,
'url'=>$url,
'log'=>$log,
'force'=>$force
]);
}else{
echo json_encode([
'code'=>1,
'msg'=>'no',
]);
}
exit;
}
// android下判断
// 如果 $new 无变化,则无更新
if($new==$_POST['version']){
echo json_encode([
'code'=>1,
'msg'=>'no',
'postParams'=>$postParams,
]);
exit;
}else{
echo json_encode([
'code'=>0,
'msg'=>'ok',
'version'=>$new,
'url'=>'https://xxx.xxxx.xxx/apk/'.$new.'.apk',
'log'=>'新版本:v'.$new."\n".$log,
'force'=>$force
]);
}
}
}
注意:上面的代码有几个地方需要注意
1、由于安卓和ios检测更新最后下载的方式不一样,在ios中,我们会跳转app store下载更新,所以代码中就会获取到配置文件的site.iosVersion和site.iosUrl,这两个我采用的方式是都在thinkPHP的后台的系统配置的中直接设置而无需每次更新还得改动服务器上的这个文件,如果你使用的不是这个,那也想ios可以跳转,可以每次更新的时候修改一下这个文件的$ios和$iosurl参数这里,写死,只是有些麻烦。(另外更新日志那我也采用后台填写的方式,如下图),这个ios商店下载地址就是你在商店更新版本之后,点击分享这个app,就可以拷贝链接了,经百度说似乎每次更新这个链接都会发生改变,所以苹果商店一更新这里也得重新拷贝链接
2、如下图,服务器上我把存放新版本apk的目录放在了项目目录的public的apk文件夹下,因为我的项目不允许直接访问除了public下其他的文件夹,你也可以根据自己实际环境更改,反正最后把路径放在$path中使其下载时能够访问到即可;还有就是记得启用服务器上exec函数,不然找不到这个apk文件,启用方法自行百度(一般会写在配置文件里或者宝塔上单独设置)
差不多到这里完成了,经测试可以使用,有问题可以共同交流探讨!