HBuilderX中配置JS/NVUE文件原生混淆加密
此文档不再维护,请参考新文档地址:https://uniapp.dcloud.io/tutorial/app-sec-confusion
App的安装包都可以解压。前端资源,一般都是明文存放在安装包中,为防止解压后泄露敏感信息,需要进行安全处理。
由此DCloud提供了App端的js/nvue文件的原生混淆。5+ App/Wap2App支持对指定的js进行原生混淆。uni-app支持对指定的nvue文件原生混淆。
原生混淆后的安装包,解压后看到的都是乱码。
但需要注意:
- 没有绝对的安全,非常重要的信息,应该保存在服务器而不是前端
- 运行期对资源代码解密是影响执行性能的。不建议全包混淆,仅挑选需要保护的个别文件处理即可
- uni-app项目制作wgt包不支持原生混淆加密(即使配置也不会生效),HBuilderX3.1.0+版本后支持
- 为了保证加密数据的安全性,加密算法和key不对外公开,因此离线打包无法支持原生混淆加密,标准基座或自定义基座真机运行也不支持原生混淆加密(只有正式云打包才支持)
具体使用方式如下:
第一步、在manifest.json文件中配置要混淆的文件列表
打开manifest.json文件,切换到“源码视图”,按不同项目类型进行配置。
uni-app项目
uni-app的js运行在独立的jscore中,而不是webview中,所以不受iOS平台WKWebview不支持原生混淆的限制。
uni-app的vue页面中的js,是整体编译到一个大js文件中的,它经过编译,已经不再是vue源码了,但还不是乱码。对这个统一的大文件进行混淆会有影响性能。
所以uni-app只支持独立混淆nvue/js文件。
- vue页面
HBuilderX2.6.3+版本v3编译器支持对独立的js文件进行原生混淆,开发者可以将要保护的js代码写到独立的js文件中,在vue页面中使用import引用;如果此js同时被nvue页面import引用,则nvue页面也需要配置原生混淆才有效。另外main.js也可以原生混淆。
老版本不支持vue页面的原生混淆,开发者只能将要保护的js代码写到nvue文件中进行保护。 - nvue页面
HBuilderX2.3.4+版本支持nvue文件的原生混淆。
如果nvue页面引入了外部的js文件,会被一起原生混淆。但如果这个js还被其他不加密的文件引用,则该js仍然会暴露在安装包中。 - vue页面和nvue页面同时使用加密js里的数据或方法(HBuilderX2.6.3+版本v3编译器)
配置该js加密,并在App.vue中引用该js,把该js中的数据或方法赋值给全局对象,如globalData,vue和nvue中通过访问getApp访问共享数据或方法即可,无需配置nvue页面加密。
如果要发布多端的话,要保护的js最好写在app-plus的条件编译中,否则发布到其他端,还是无法原生混淆。
HBuilderX2.3.4版本开始,uni-app项目支持对nvue文件进行原生混淆
在"app-plus" -> "confusion" -> "resources"节点下添加要混淆的nvue文件列表:
"app-plus": {
"confusion": {
"description": "NVUE原生混淆",
"resources": {
"pages/barcode/barcode.nvue": {
},
"pages/map/map.nvue": {
}
}
},
// ...
}
resource下的键名为nvue文件路径(相对于应用根目录),值为空JSON对象(大括号)。
<a id="vuejs"></a>
HBuilderX2.6.3+版本开始,uni-app项目使用v3编译器支持对vue页面中引用的js文件进行原生混淆
在manifest.json文件中添加要混淆的js文件列表:
"app-plus": {
"confusion": {
"description": "原生混淆",
"resources": {
"common/test.js" : {}
}
},
// ...
}
在vue文件中引用混淆的js文件:
import test from '../common/test.js';
//test.join(); //调用引用js中的方法
注意:uni-app中vue页面的webview组件支持加载使用加密混淆hybrid、static目录中的js文件,nvue页面的webview组件不支持。
5+ App/Wap2App项目
应用运行期间在页面打开时需要消耗更多时间进行混淆文件还原,为减少对运行速度的影响,5+App/wap2app仅支持对js文件进行原生混淆。
在"plus" -> "confusion" -> "resources"节点下添加要混淆的js文件列表:
"plus": {
"confusion": {
"description": "JS原生混淆",
"resources": {
"js/common.js": {
},
"js/immersed.js": {
}
}
},
// ...
}
resource下的键名为js文件路径(相对于应用根目录),值为空JSON对象(大括号)。
<a id="wkwebview"></a>
HBuilderX2.6.11+版本开始,在iOS11+设备上使用WKWebview也可以支持JS原生混淆
WKWebview使用了更加严格的安全机制,使用原生混淆的js文件在html页面中必须使用自定义协议头plus-confusion://来引用:
<script type="text/javascript" src="plus-confusion://../js/common.js"></script>
<!-- plus-confusion://后面为js文件路径,相对于当前html页面的路径 -->
在manifest.json的"plus" -> "confusion" -> "resources"节点下添加要混淆的js文件列表。
在"confusion"节点下添加 "supportWKWebview": true 支持WKWebview。
由于自定义协议仅在iOS11及以上设备才支持,建议配置应用支持的最低版本deploymentTarget为11.0:
"plus": {
"confusion": {
"description": "JS原生混淆",
"supportWKWebview": true,
"resources": {
"js/common.js": {
}
}
},
"distribute": {
"apple": {
"deploymentTarget": "11.0" //设置应用仅支持iOS11及以上设备
//...
}
}
// ...
}
注意:iOS平台WKWebview需iOS11+系统才支持原生混淆。5+App/wap2app项目,如果要兼容iOS11以下设备只能强制使用UIWebview内核,但苹果将要废弃UIWebview(详情)。如对原生混淆很重视,从长远考虑,建议改造升级uni-app
第二步、提交云端打包
配置好原生混淆的文件列表后,需要提交云端打包,注意在App云端打包对话框中需要勾选“对配置的js文件进行原生混淆”
再次强调:为了保证加密数据的安全性,加密算法和key不对外公开,因此离线打包无法支持原生混淆。
熟悉原生的开发者可将敏感信息存放于原生代码中,再与js进行交互。
对安全性要求较高的开发者,除了对前端js进行加密外,还应该对整个apk再进行一次加固。市面上很多加固服务可以选择,比如360加固、爱加密等。
此文档不再维护,请参考新文档地址:https://uniapp.dcloud.io/tutorial/app-sec-confusion
App的安装包都可以解压。前端资源,一般都是明文存放在安装包中,为防止解压后泄露敏感信息,需要进行安全处理。
由此DCloud提供了App端的js/nvue文件的原生混淆。5+ App/Wap2App支持对指定的js进行原生混淆。uni-app支持对指定的nvue文件原生混淆。
原生混淆后的安装包,解压后看到的都是乱码。
但需要注意:
- 没有绝对的安全,非常重要的信息,应该保存在服务器而不是前端
- 运行期对资源代码解密是影响执行性能的。不建议全包混淆,仅挑选需要保护的个别文件处理即可
- uni-app项目制作wgt包不支持原生混淆加密(即使配置也不会生效),HBuilderX3.1.0+版本后支持
- 为了保证加密数据的安全性,加密算法和key不对外公开,因此离线打包无法支持原生混淆加密,标准基座或自定义基座真机运行也不支持原生混淆加密(只有正式云打包才支持)
具体使用方式如下:
第一步、在manifest.json文件中配置要混淆的文件列表
打开manifest.json文件,切换到“源码视图”,按不同项目类型进行配置。
uni-app项目
uni-app的js运行在独立的jscore中,而不是webview中,所以不受iOS平台WKWebview不支持原生混淆的限制。
uni-app的vue页面中的js,是整体编译到一个大js文件中的,它经过编译,已经不再是vue源码了,但还不是乱码。对这个统一的大文件进行混淆会有影响性能。
所以uni-app只支持独立混淆nvue/js文件。
- vue页面
HBuilderX2.6.3+版本v3编译器支持对独立的js文件进行原生混淆,开发者可以将要保护的js代码写到独立的js文件中,在vue页面中使用import引用;如果此js同时被nvue页面import引用,则nvue页面也需要配置原生混淆才有效。另外main.js也可以原生混淆。
老版本不支持vue页面的原生混淆,开发者只能将要保护的js代码写到nvue文件中进行保护。 - nvue页面
HBuilderX2.3.4+版本支持nvue文件的原生混淆。
如果nvue页面引入了外部的js文件,会被一起原生混淆。但如果这个js还被其他不加密的文件引用,则该js仍然会暴露在安装包中。 - vue页面和nvue页面同时使用加密js里的数据或方法(HBuilderX2.6.3+版本v3编译器)
配置该js加密,并在App.vue中引用该js,把该js中的数据或方法赋值给全局对象,如globalData,vue和nvue中通过访问getApp访问共享数据或方法即可,无需配置nvue页面加密。
如果要发布多端的话,要保护的js最好写在app-plus的条件编译中,否则发布到其他端,还是无法原生混淆。
HBuilderX2.3.4版本开始,uni-app项目支持对nvue文件进行原生混淆
在"app-plus" -> "confusion" -> "resources"节点下添加要混淆的nvue文件列表:
"app-plus": {
"confusion": {
"description": "NVUE原生混淆",
"resources": {
"pages/barcode/barcode.nvue": {
},
"pages/map/map.nvue": {
}
}
},
// ...
}
resource下的键名为nvue文件路径(相对于应用根目录),值为空JSON对象(大括号)。
<a id="vuejs"></a>
HBuilderX2.6.3+版本开始,uni-app项目使用v3编译器支持对vue页面中引用的js文件进行原生混淆
在manifest.json文件中添加要混淆的js文件列表:
"app-plus": {
"confusion": {
"description": "原生混淆",
"resources": {
"common/test.js" : {}
}
},
// ...
}
在vue文件中引用混淆的js文件:
import test from '../common/test.js';
//test.join(); //调用引用js中的方法
注意:uni-app中vue页面的webview组件支持加载使用加密混淆hybrid、static目录中的js文件,nvue页面的webview组件不支持。
5+ App/Wap2App项目
应用运行期间在页面打开时需要消耗更多时间进行混淆文件还原,为减少对运行速度的影响,5+App/wap2app仅支持对js文件进行原生混淆。
在"plus" -> "confusion" -> "resources"节点下添加要混淆的js文件列表:
"plus": {
"confusion": {
"description": "JS原生混淆",
"resources": {
"js/common.js": {
},
"js/immersed.js": {
}
}
},
// ...
}
resource下的键名为js文件路径(相对于应用根目录),值为空JSON对象(大括号)。
<a id="wkwebview"></a>
HBuilderX2.6.11+版本开始,在iOS11+设备上使用WKWebview也可以支持JS原生混淆
WKWebview使用了更加严格的安全机制,使用原生混淆的js文件在html页面中必须使用自定义协议头plus-confusion://来引用:
<script type="text/javascript" src="plus-confusion://../js/common.js"></script>
<!-- plus-confusion://后面为js文件路径,相对于当前html页面的路径 -->
在manifest.json的"plus" -> "confusion" -> "resources"节点下添加要混淆的js文件列表。
在"confusion"节点下添加 "supportWKWebview": true 支持WKWebview。
由于自定义协议仅在iOS11及以上设备才支持,建议配置应用支持的最低版本deploymentTarget为11.0:
"plus": {
"confusion": {
"description": "JS原生混淆",
"supportWKWebview": true,
"resources": {
"js/common.js": {
}
}
},
"distribute": {
"apple": {
"deploymentTarget": "11.0" //设置应用仅支持iOS11及以上设备
//...
}
}
// ...
}
注意:iOS平台WKWebview需iOS11+系统才支持原生混淆。5+App/wap2app项目,如果要兼容iOS11以下设备只能强制使用UIWebview内核,但苹果将要废弃UIWebview(详情)。如对原生混淆很重视,从长远考虑,建议改造升级uni-app
第二步、提交云端打包
配置好原生混淆的文件列表后,需要提交云端打包,注意在App云端打包对话框中需要勾选“对配置的js文件进行原生混淆”
再次强调:为了保证加密数据的安全性,加密算法和key不对外公开,因此离线打包无法支持原生混淆。
熟悉原生的开发者可将敏感信息存放于原生代码中,再与js进行交互。
对安全性要求较高的开发者,除了对前端js进行加密外,还应该对整个apk再进行一次加固。市面上很多加固服务可以选择,比如360加固、爱加密等。
收起阅读 »mui上传图片案例
本案例是上传图片案例,可以进行图片选择和拍照操作。
使用时主要修改有以下几个:
1、var head_pho = document.getElementById('head_pho');这里修改成获取事件id
2、task.addData('token', token); 在此可以设置参数
3、task.addFile(filepath, { key: "personImage" }); filepath为图片路径,key是服务端接收图片的参数
4、plus.uploader.createUpload 在这里设置成服务端上传图片接口
以下为代码
var head_pho = document.getElementById('head_pho');
mui.plusReady(function() {
// 文件路径
var filepath;
head_pho.addEventListener('tap', function() {
if (mui.os.plus) {
var a = [{
title: '拍照'
}, {
title: '从手机相册选择'
}];
plus.nativeUI.actionSheet({
title: '修改头像',
cancel: '取消',
buttons: a
}, function(b) {
switch (b.index) {
case 0:
break;
case 1:
//拍照
getImages();
break;
case 2:
//打开相册
galleryImages();
break;
default:
break;
}
}, false);
}
});
//拍照
function getImages() {
var mobileCamera = plus.camera.getCamera();
mobileCamera.captureImage(function(e) {
plus.io.resolveLocalFileSystemURL(e, function(entry) {
var path = entry.toLocalURL() + '?version=' + new Date().getTime();
var dstname = "_downloads/" + getUid() + ".jpg"; //设置压缩后图片的路径
compressImage(path, dstname, 0);
}, function(err) {
console.log("读取拍照文件错误");
});
}, function(e) {
console.log("er", err);
}, function() {
filename: '_doc/head.png';
});
}
//从本地相册选择
function galleryImages() {
plus.gallery.pick(function(a) {
plus.io.resolveLocalFileSystemURL(a, function(entry) {
plus.io.resolveLocalFileSystemURL('_doc/', function(root) {
root.getFile('head.png', {}, function(file) {
//文件已经存在
file.remove(function() {
console.log("文件移除成功");
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + '?version=' + new Date().getTime();
var dstname = "_downloads/" + getUid() + ".jpg"; //设置压缩后图片的路径
compressImage(path, dstname, 270);
}, function(err) {
console.log("copy image fail: ", err);
});
}, function(err) {
console.log("删除图片失败:(" + JSON.stringify(err) + ")");
});
}, function(err) {
//打开文件失败
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + '?version=' + new Date().getTime();
uploadHeadImg(path);
}, function(err) {
console.log("上传图片失败:(" + JSON.stringify(err) + ")");
});
});
}, function(e) {
console.log("读取文件夹失败:(" + JSON.stringify(err) + ")");
});
});
}, function(err) {
console.log("读取拍照文件失败: ", err);
}, {
filter: 'image'
});
}
// 产生一个随机数
function getUid() {
return Math.floor(Math.random() * 100000000 + 10000000).toString();
}
// 上传操作
function upload() {
var task = plus.uploader.createUpload(getUrl() + 'com_uploadPersonImage.action', {
method: "POST"
},
function(t, status) { //上传完成
if (status == 200) {
console.log(t.responseText);
// imgdiv.innerHTML = '<img id="userImg" src="' + t.responseText + '"/>';
} else {
console.log("上传失败:" + status);
}
});
task.addData('token', token);
task.addFile(filepath, {
key: "personImage"
});
task.start();
}
// 进行图片压缩
function compressImage(src, dstname, rotate) {
plus.zip.compressImage({
src: src,
dst: dstname,
overwrite: true,
quality: 20,
rotate: rotate
},
function(event) {
console.log("Compress success:" + event.target);
filepath = event.target;
upload();
},
function(error) {
console.log(error);
});
}
})
本案例是上传图片案例,可以进行图片选择和拍照操作。
使用时主要修改有以下几个:
1、var head_pho = document.getElementById('head_pho');这里修改成获取事件id
2、task.addData('token', token); 在此可以设置参数
3、task.addFile(filepath, { key: "personImage" }); filepath为图片路径,key是服务端接收图片的参数
4、plus.uploader.createUpload 在这里设置成服务端上传图片接口
以下为代码
var head_pho = document.getElementById('head_pho');
mui.plusReady(function() {
// 文件路径
var filepath;
head_pho.addEventListener('tap', function() {
if (mui.os.plus) {
var a = [{
title: '拍照'
}, {
title: '从手机相册选择'
}];
plus.nativeUI.actionSheet({
title: '修改头像',
cancel: '取消',
buttons: a
}, function(b) {
switch (b.index) {
case 0:
break;
case 1:
//拍照
getImages();
break;
case 2:
//打开相册
galleryImages();
break;
default:
break;
}
}, false);
}
});
//拍照
function getImages() {
var mobileCamera = plus.camera.getCamera();
mobileCamera.captureImage(function(e) {
plus.io.resolveLocalFileSystemURL(e, function(entry) {
var path = entry.toLocalURL() + '?version=' + new Date().getTime();
var dstname = "_downloads/" + getUid() + ".jpg"; //设置压缩后图片的路径
compressImage(path, dstname, 0);
}, function(err) {
console.log("读取拍照文件错误");
});
}, function(e) {
console.log("er", err);
}, function() {
filename: '_doc/head.png';
});
}
//从本地相册选择
function galleryImages() {
plus.gallery.pick(function(a) {
plus.io.resolveLocalFileSystemURL(a, function(entry) {
plus.io.resolveLocalFileSystemURL('_doc/', function(root) {
root.getFile('head.png', {}, function(file) {
//文件已经存在
file.remove(function() {
console.log("文件移除成功");
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + '?version=' + new Date().getTime();
var dstname = "_downloads/" + getUid() + ".jpg"; //设置压缩后图片的路径
compressImage(path, dstname, 270);
}, function(err) {
console.log("copy image fail: ", err);
});
}, function(err) {
console.log("删除图片失败:(" + JSON.stringify(err) + ")");
});
}, function(err) {
//打开文件失败
entry.copyTo(root, 'head.png', function(e) {
var path = e.fullPath + '?version=' + new Date().getTime();
uploadHeadImg(path);
}, function(err) {
console.log("上传图片失败:(" + JSON.stringify(err) + ")");
});
});
}, function(e) {
console.log("读取文件夹失败:(" + JSON.stringify(err) + ")");
});
});
}, function(err) {
console.log("读取拍照文件失败: ", err);
}, {
filter: 'image'
});
}
// 产生一个随机数
function getUid() {
return Math.floor(Math.random() * 100000000 + 10000000).toString();
}
// 上传操作
function upload() {
var task = plus.uploader.createUpload(getUrl() + 'com_uploadPersonImage.action', {
method: "POST"
},
function(t, status) { //上传完成
if (status == 200) {
console.log(t.responseText);
// imgdiv.innerHTML = '<img id="userImg" src="' + t.responseText + '"/>';
} else {
console.log("上传失败:" + status);
}
});
task.addData('token', token);
task.addFile(filepath, {
key: "personImage"
});
task.start();
}
// 进行图片压缩
function compressImage(src, dstname, rotate) {
plus.zip.compressImage({
src: src,
dst: dstname,
overwrite: true,
quality: 20,
rotate: rotate
},
function(event) {
console.log("Compress success:" + event.target);
filepath = event.target;
upload();
},
function(error) {
console.log(error);
});
}
})
收起阅读 »
H5 端长按无法被页面滚动或滑动阻止
详细问题描述
目前我们要实现的需求为:
1、长按触发 showActionSheet
2、长按的元素在长按期间有页面滚动或者页面滑动时不触发长按
在小程序端没问题,在 H5 端滑动或者滚动的时候仍然会触发长按
[内容]
重现步骤
[步骤] H5 端列表元素长按时滑动
[结果] 滑动没有阻止长按事件的触发
[期望] 在长按触发前如果有页面滑动或者滚动时阻止长按触发
[如果语言难以表述清晰,拍一个视频或截图,有图有真相]
IDE运行环境说明
[HBuilder 或 HBuilderX。如果你用其他工具开发uni-app,也需要在此说明]
[IDE版本号] 2.3.3.20190923
[windows版本号]
[mac版本号]
uni-app运行环境说明
[运行端是h5或app或某个小程序?] h5
[项目是cli创建的还是HBuilderX创建的?如果是cli创建的,请更新到最新版cli再试] HBuilderX创建
[编译模式是老模板模式还是新的自定义组件模式?] 自定义组件模式
详细问题描述
目前我们要实现的需求为:
1、长按触发 showActionSheet
2、长按的元素在长按期间有页面滚动或者页面滑动时不触发长按
在小程序端没问题,在 H5 端滑动或者滚动的时候仍然会触发长按
[内容]
重现步骤
[步骤] H5 端列表元素长按时滑动
[结果] 滑动没有阻止长按事件的触发
[期望] 在长按触发前如果有页面滑动或者滚动时阻止长按触发
[如果语言难以表述清晰,拍一个视频或截图,有图有真相]
IDE运行环境说明
[HBuilder 或 HBuilderX。如果你用其他工具开发uni-app,也需要在此说明]
[IDE版本号] 2.3.3.20190923
[windows版本号]
[mac版本号]
uni-app运行环境说明
[运行端是h5或app或某个小程序?] h5
[项目是cli创建的还是HBuilderX创建的?如果是cli创建的,请更新到最新版cli再试] HBuilderX创建
[编译模式是老模板模式还是新的自定义组件模式?] 自定义组件模式
收起阅读 »1.8升级2.3 安卓无法运行。 能解决红包奖励
1:44:58.250 44:57.623 20949 21138 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:44:58.440 Error: [JS Framework] Using invalid instance id "1" when calling destroyInstance.
11:44:59.879 App Launch at App.vue:48
11:45:00.745 45:00.127 20949 21154 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:45:00.918 Error: [JS Framework] Using invalid instance id "2" when calling destroyInstance.
11:45:02.355 App Launch at App.vue:48
11:45:03.150 45:02.533 20949 21170 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:45:03.441 Error: [JS Framework] Using invalid instance id "3" when calling destroyInstance.
1:44:58.250 44:57.623 20949 21138 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:44:58.440 Error: [JS Framework] Using invalid instance id "1" when calling destroyInstance.
11:44:59.879 App Launch at App.vue:48
11:45:00.745 45:00.127 20949 21154 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:45:00.918 Error: [JS Framework] Using invalid instance id "2" when calling destroyInstance.
11:45:02.355 App Launch at App.vue:48
11:45:03.150 45:02.533 20949 21170 E console : [ERROR] reportJSException >>>> exception function:callReportCrash, exception:weex core process crash and restart exception
11:45:03.441 Error: [JS Framework] Using invalid instance id "3" when calling destroyInstance.
Hbuilder真机运行调试之模拟器无法获得token
首先,我成功的完成HBuilder真机运行连接逍遥模拟器或mumu模拟器。
而,我的项目中大量用到了plus.storage.getItem('_token');来获取token;
但是在模拟器上成功运行项目后,测试发现所有的token获取均为null。
所以,关于在模拟器上开发应用,目前道路难行。
首先,我成功的完成HBuilder真机运行连接逍遥模拟器或mumu模拟器。
而,我的项目中大量用到了plus.storage.getItem('_token');来获取token;
但是在模拟器上成功运行项目后,测试发现所有的token获取均为null。
所以,关于在模拟器上开发应用,目前道路难行。
收起阅读 »ios ajax的http请求无法响应
最新版本的hbuildx推送ios真机调试无法HTTP通信。没有响应。应该是又被启用了ats验证了吧。
最新版本的hbuildx推送ios真机调试无法HTTP通信。没有响应。应该是又被启用了ats验证了吧。
uni.request传递复杂对象
js版serialize 实现
function serialize (obj, prefix) {
const str = []
let p
if (obj.length === 0) {
// str.push(encodeURIComponent(prefix) + '=1')
} else {
for (p in obj) {
if (obj.hasOwnProperty(p)) {
const k = prefix ? prefix + '[' + p + ']' : p
let v = obj[p]
if (v instanceof Date) {
v = parseTime(v)
}
str.push((v !== null && typeof v === 'object') ? serialize(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(
v))
}
}
}
return str.join('&')
}
uni.request.data改造
options.data = serialize(options.data)
这样就可以了
js版serialize 实现
function serialize (obj, prefix) {
const str = []
let p
if (obj.length === 0) {
// str.push(encodeURIComponent(prefix) + '=1')
} else {
for (p in obj) {
if (obj.hasOwnProperty(p)) {
const k = prefix ? prefix + '[' + p + ']' : p
let v = obj[p]
if (v instanceof Date) {
v = parseTime(v)
}
str.push((v !== null && typeof v === 'object') ? serialize(v, k) : encodeURIComponent(k) + '=' + encodeURIComponent(
v))
}
}
}
return str.join('&')
}
uni.request.data改造
options.data = serialize(options.data)
这样就可以了
收起阅读 »iOS 云打包后台权限配置教程
iOS App 在使用一些功能时需要申请后台权限,比如后台音频播放、后台定位等,在提交云打包时,需要在 manifest.json 的 "app-plus" -> "distribute" -> "ios" -> "UIBackgroundModes"
节点添加对应权限的 描述key 即可;
注意:后台权限用不到千万不要填写,以免审核被拒
{
"app-plus" : {
/* 应用发布信息 */
"distribute" : {
/* ios打包配置 */
"ios" : {
"UIBackgroundModes" : [ "audio"] // 数组,支持多个
},
...
}
}
}
权限描述key获取方式
在 xcode 中勾选权限
然后查看 info.plist 右键勾选 Raw Keys & Values
UIBackgroundModes 对应的就是需要填写在 manifest.json 中的 值
所有 key
audio // Audio, AirPlay, and Picture in Picture
bluetooth-central // Uses Bluetooth LE accessories
bluetooth-peripheral // Acts as a Bluetooth LE accessory
external-accessory // External accessory communication
fetch // Background fetch
location // Location updates
processing // Background processing
remote-notification // Remote notifications
voip // Voice over IP
iOS App 在使用一些功能时需要申请后台权限,比如后台音频播放、后台定位等,在提交云打包时,需要在 manifest.json 的 "app-plus" -> "distribute" -> "ios" -> "UIBackgroundModes"
节点添加对应权限的 描述key 即可;
注意:后台权限用不到千万不要填写,以免审核被拒
{
"app-plus" : {
/* 应用发布信息 */
"distribute" : {
/* ios打包配置 */
"ios" : {
"UIBackgroundModes" : [ "audio"] // 数组,支持多个
},
...
}
}
}
权限描述key获取方式
在 xcode 中勾选权限
然后查看 info.plist 右键勾选 Raw Keys & Values
UIBackgroundModes 对应的就是需要填写在 manifest.json 中的 值
所有 key
audio // Audio, AirPlay, and Picture in Picture
bluetooth-central // Uses Bluetooth LE accessories
bluetooth-peripheral // Acts as a Bluetooth LE accessory
external-accessory // External accessory communication
fetch // Background fetch
location // Location updates
processing // Background processing
remote-notification // Remote notifications
voip // Voice over IP
收起阅读 »
vue(uni-app)中对数值进行保留小数点后两位的处理(两种情况)
View部分代码:
<view class="com_middle1 com_middle_bg">
<view>投资金额</view>
<view>{{ numFilter(object.InvestmentAmount) + object.MoneyUnit }}</view>
</view>
JS部分:(注意toFixed()只能用在数值部分)
第一保留小数点数值后两位,尾数四舍五入
numFilter (value) {
// 截取当前数据到小数点后两位
let realVal = parseFloat(value).toFixed(2)
return realVal
}
第二保留小数点后两位的过滤器,不会四舍五入
numFilter (value) {
// 截取当前数据到小数点后三位
let tempVal = parseFloat(value).toFixed(3)
let realVal = tempVal.substring(0, tempVal.length - 1)
return realVal
}
View部分代码:
<view class="com_middle1 com_middle_bg">
<view>投资金额</view>
<view>{{ numFilter(object.InvestmentAmount) + object.MoneyUnit }}</view>
</view>
JS部分:(注意toFixed()只能用在数值部分)
第一保留小数点数值后两位,尾数四舍五入
numFilter (value) {
// 截取当前数据到小数点后两位
let realVal = parseFloat(value).toFixed(2)
return realVal
}
第二保留小数点后两位的过滤器,不会四舍五入
numFilter (value) {
// 截取当前数据到小数点后三位
let tempVal = parseFloat(value).toFixed(3)
let realVal = tempVal.substring(0, tempVal.length - 1)
return realVal
}
HBuilderX官方QQ交流群
为了方便大家技术交流,因此建立了QQ群
HX官方群15:793046085
HX官方群14:750929504
(2000人群)
HX官方群13:1029243934
(2000人群,已满)
HX官方群12: 1051711389
(1000人群,已满)
HX官方群11: 1051710376
(2000人群,已满)
HX官方群9:824581834
(500人群)
HX官方群8:759481147
(2000人群)
HX官方群7:768137673
(500人群,已满)
HX官方群6:843092525
(500人群,已满)
HX官方群5:178140648
(1000人群,还有几十个空名额)
HX官方群4:905643840
(500人群,已满)
HX官方群3:335122268
(500人群,已满)
HX官方群2:363040810
(500人群,已满)
Mac交流群
仅限mac用户加入
- Mac群1:
148229211
- Mac群2:
339884851
为了方便大家技术交流,因此建立了QQ群
HX官方群15:793046085
HX官方群14:750929504
(2000人群)
HX官方群13:1029243934
(2000人群,已满)
HX官方群12: 1051711389
(1000人群,已满)
HX官方群11: 1051710376
(2000人群,已满)
HX官方群9:824581834
(500人群)
HX官方群8:759481147
(2000人群)
HX官方群7:768137673
(500人群,已满)
HX官方群6:843092525
(500人群,已满)
HX官方群5:178140648
(1000人群,还有几十个空名额)
HX官方群4:905643840
(500人群,已满)
HX官方群3:335122268
(500人群,已满)
HX官方群2:363040810
(500人群,已满)
Mac交流群
仅限mac用户加入
- Mac群1:
148229211
- Mac群2:
339884851
消息推送一个好功能,90%的开发者都不知道
1.个推报表简介
推送数据报表主要用于统计某一条消息的具体下发情况。单条推送消息下发用户总量有多少,其中成功推送到手机的数量有多少,又有多少用户看到了弹窗通知、点击了弹窗通知并打开了应用。通过消息推送报表可以很直观地看到推送消息流转情况、消息下发到达成功率、用户对消息的点击情况等。
当然推送报表不单单只有上述这一种维度,个推的推送统计主要分了三个维度:消息推送维度、APP推送维度、APP用户维度。
- 消息推送维度:针对单个消息推送,提供单条消息推送的下发成功数,手机到达数,弹窗展示数等
- APP推送维度:针对应用单日所有消息推送,单日所有任务的下发成功数,手机到达数,弹窗展示数等
- APP用户维度:针对应用单日用户数据,应用单日新增用户数、在线用户数等
上述三个维度的推送数据统计则是通过个推开发者中心和服务端SDK集成接口来进行输出。个推开发者中心提供详细的可视化图表,服务端SDK集成接口则提供服务端的数据记录和统计。两者的数据都是一致的,但功能和数据类型上有些许不同,使用者可以根据自己需求选用不同的方式。下面将分别从个推开发者中心和服务端SDK集成接口两方面对个推报表进行阐述。
2.开发者中心
登录个推开发者中心,在产品与服务界面选择个推消息推送消息推送后进入了应用列表页面,选择相应的应用,点击数据报表即可在数据统计栏目中看到各个维度的推送统计数据。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190920160039840.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FuZHJvaWxseQ==,size_16,color_FFFFFF,t_70 =200x300)
2.1 推送记录
推送记录中保存了所有的历史推送,通过点击 "推送数据" 可以查询该推送的任务报表情况。
2.2 推送数据(消息推送维度)
针对单个消息推送,提供报表数据。
- 百日联网用户数:最近百日所有打开过APP的用户,可以理解为预计可以下发的用户总量。
- 成功下发数:消息下发的实际用户量。APP在手机后台中存活才会进行下发,否则不会进行消息下发。
- 到达数:成功下发到手机的消息数。到达手机后因为网络等原因,无法回执到服务端,所以成功下发数会略大于到达数。
- 展示数:手机弹窗展示消息的数量。部分用户可能对应用设置了禁止弹窗,所以到达数会大于展示数。
- 点击数:手机用户点击弹窗通知的数量
上图中展示的报表数据是个推+其他渠道的累加数据,点击详情即可看到各个渠道下发统计。其中的 "-" 表示不支持该字段报表
2.3 推送统计(APP推送维度)
推送统计从APP推送维度出发,对APP当天所有任务的下发数据进行统计。举个例子,APP开发者15号下发的消息,可能用户16号打开手机才会收到,但是此次数据仍会统计到15号的推送统计中。针对这种情况,个推会对APP统计数据在三天内予以持续更新。所以APP推送统计数据会在之后的三天持续更新。日联网用户数:当天打开应用的用户数
- 成功下发数、到达数、展示数、点击数:和上述一致
点击展开也能看到各个渠道的App维度统计。
2.5 实时性和准确性
创建一个新的推送任务,能够实时地看到推送的总体趋势,趋势数据会进行实时更新。
已完成推送的任务报表数据,为什么到了第二天数据会发生变化?
个推内部推送数据报表分成两种:实时报表和统计报表。第二天统计报表会对前一天的实时报表进行修正。
实时报表以实时性为主,具有实时更新的功能。APP开发者如果想查看当天下发的任务数据,可以选择实时报表。。相比起实时报表,统计报表更加注重准确性,旨在通过统计分析,对数据进行筛选、去重,得到最终的准确报表。APP开发者一般隔天就能查询到准确的报表数据。
3.服务端SDK集成接口
在官网提供的SDK Demo中就有示例程序,做了很好的封装,返回结果也做了预处理。。APP开发者只需要传入简单的参数即可完成报表查询, SDK接口提供了一些开发者平台中没有的数据统计服务,如用户数据中的最近24小时在线趋势统计、根据用户条件查询用户数等。
详细可参考 文档中心和Java服务端集成文档
/**
* 推送结果相关demo
*/
public class PushResultDemo {
public static IGtPush gtPush = new IGtPush(APPKEY, MASTERSECRET);
/**
* 获取推送结果,可查询消息有效可下发总数,消息回执总数,用户点击数等结果。
* @param taskId
*/
private static void getPushResultByTaskId(String taskId) {
Map<String, Object> ret = gtPush.getPushResult(taskId).getResponse();
System.out.println(JSON.toJSONString(ret));
}
/**
* 获取应用单日的推送数据(推送数据包括:发送总数,在线发送数,接收数,展示数,点击数)(目前只支持查询1天前的数据)
*/
private static void queryAppPushDataByDate(String date) {
IQueryResult ret = gtPush.queryAppPushDataByDate(APPID, date);
Map<String, Object> res = ret.getResponse();
System.out.println(JSON.toJSONString(ret));
for(Map.Entry<String,Object> entry: res.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
/**
* 通过接口查询当前时间一天内的在线数(十分钟一个点,一小时六个点)
*/
private static void getLast24HoursOnlineUserStatistics () {
IQueryResult queryResult = push.getLast24HoursOnlineUserStatistics(APPID);
System.out.println(queryResult.getResponse().get("onlineStatics"));
}
/**
* 获取某个应用单日的用户数据(用户数据包括:新增用户数,累计注册用户总数,在线峰值,日联网用户数)
注:目前可查询1天前的数据
*/
private static void queryAppUserDataByDate(String date) {
IQueryResult ret = push.queryAppUserDataByDate(APPID, date);
System.out.println(ret.getResponse().toString());
Map<String, Object> data = (Map<String, Object>) ret.getResponse().get("data");
System.out.println("新用户注册总数:"+data.get("newRegistCount"));
System.out.println("用户注册总数:"+data.get("registTotalCount"));
System.out.println("活跃用户数:"+data.get("activeCount"));
System.out.println("在线用户数:"+data.get("onlineCount"));
}
}
小结
推送数据报表是个推消息推送服务中的重要组成成分,通过推送报表可以直观、准确地了解消息触达用户的详细情况,对推送数据一目了然。个推将针对报表服务不断优化,为APP开发者们提供更加完善的推送数据统计支持。
1.个推报表简介
推送数据报表主要用于统计某一条消息的具体下发情况。单条推送消息下发用户总量有多少,其中成功推送到手机的数量有多少,又有多少用户看到了弹窗通知、点击了弹窗通知并打开了应用。通过消息推送报表可以很直观地看到推送消息流转情况、消息下发到达成功率、用户对消息的点击情况等。
当然推送报表不单单只有上述这一种维度,个推的推送统计主要分了三个维度:消息推送维度、APP推送维度、APP用户维度。
- 消息推送维度:针对单个消息推送,提供单条消息推送的下发成功数,手机到达数,弹窗展示数等
- APP推送维度:针对应用单日所有消息推送,单日所有任务的下发成功数,手机到达数,弹窗展示数等
- APP用户维度:针对应用单日用户数据,应用单日新增用户数、在线用户数等
上述三个维度的推送数据统计则是通过个推开发者中心和服务端SDK集成接口来进行输出。个推开发者中心提供详细的可视化图表,服务端SDK集成接口则提供服务端的数据记录和统计。两者的数据都是一致的,但功能和数据类型上有些许不同,使用者可以根据自己需求选用不同的方式。下面将分别从个推开发者中心和服务端SDK集成接口两方面对个推报表进行阐述。
2.开发者中心
登录个推开发者中心,在产品与服务界面选择个推消息推送消息推送后进入了应用列表页面,选择相应的应用,点击数据报表即可在数据统计栏目中看到各个维度的推送统计数据。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190920160039840.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0FuZHJvaWxseQ==,size_16,color_FFFFFF,t_70 =200x300)
2.1 推送记录
推送记录中保存了所有的历史推送,通过点击 "推送数据" 可以查询该推送的任务报表情况。
2.2 推送数据(消息推送维度)
针对单个消息推送,提供报表数据。
- 百日联网用户数:最近百日所有打开过APP的用户,可以理解为预计可以下发的用户总量。
- 成功下发数:消息下发的实际用户量。APP在手机后台中存活才会进行下发,否则不会进行消息下发。
- 到达数:成功下发到手机的消息数。到达手机后因为网络等原因,无法回执到服务端,所以成功下发数会略大于到达数。
- 展示数:手机弹窗展示消息的数量。部分用户可能对应用设置了禁止弹窗,所以到达数会大于展示数。
- 点击数:手机用户点击弹窗通知的数量
上图中展示的报表数据是个推+其他渠道的累加数据,点击详情即可看到各个渠道下发统计。其中的 "-" 表示不支持该字段报表
2.3 推送统计(APP推送维度)
推送统计从APP推送维度出发,对APP当天所有任务的下发数据进行统计。举个例子,APP开发者15号下发的消息,可能用户16号打开手机才会收到,但是此次数据仍会统计到15号的推送统计中。针对这种情况,个推会对APP统计数据在三天内予以持续更新。所以APP推送统计数据会在之后的三天持续更新。日联网用户数:当天打开应用的用户数
- 成功下发数、到达数、展示数、点击数:和上述一致
点击展开也能看到各个渠道的App维度统计。
2.5 实时性和准确性
创建一个新的推送任务,能够实时地看到推送的总体趋势,趋势数据会进行实时更新。
已完成推送的任务报表数据,为什么到了第二天数据会发生变化?
个推内部推送数据报表分成两种:实时报表和统计报表。第二天统计报表会对前一天的实时报表进行修正。
实时报表以实时性为主,具有实时更新的功能。APP开发者如果想查看当天下发的任务数据,可以选择实时报表。。相比起实时报表,统计报表更加注重准确性,旨在通过统计分析,对数据进行筛选、去重,得到最终的准确报表。APP开发者一般隔天就能查询到准确的报表数据。
3.服务端SDK集成接口
在官网提供的SDK Demo中就有示例程序,做了很好的封装,返回结果也做了预处理。。APP开发者只需要传入简单的参数即可完成报表查询, SDK接口提供了一些开发者平台中没有的数据统计服务,如用户数据中的最近24小时在线趋势统计、根据用户条件查询用户数等。
详细可参考 文档中心和Java服务端集成文档
/**
* 推送结果相关demo
*/
public class PushResultDemo {
public static IGtPush gtPush = new IGtPush(APPKEY, MASTERSECRET);
/**
* 获取推送结果,可查询消息有效可下发总数,消息回执总数,用户点击数等结果。
* @param taskId
*/
private static void getPushResultByTaskId(String taskId) {
Map<String, Object> ret = gtPush.getPushResult(taskId).getResponse();
System.out.println(JSON.toJSONString(ret));
}
/**
* 获取应用单日的推送数据(推送数据包括:发送总数,在线发送数,接收数,展示数,点击数)(目前只支持查询1天前的数据)
*/
private static void queryAppPushDataByDate(String date) {
IQueryResult ret = gtPush.queryAppPushDataByDate(APPID, date);
Map<String, Object> res = ret.getResponse();
System.out.println(JSON.toJSONString(ret));
for(Map.Entry<String,Object> entry: res.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
/**
* 通过接口查询当前时间一天内的在线数(十分钟一个点,一小时六个点)
*/
private static void getLast24HoursOnlineUserStatistics () {
IQueryResult queryResult = push.getLast24HoursOnlineUserStatistics(APPID);
System.out.println(queryResult.getResponse().get("onlineStatics"));
}
/**
* 获取某个应用单日的用户数据(用户数据包括:新增用户数,累计注册用户总数,在线峰值,日联网用户数)
注:目前可查询1天前的数据
*/
private static void queryAppUserDataByDate(String date) {
IQueryResult ret = push.queryAppUserDataByDate(APPID, date);
System.out.println(ret.getResponse().toString());
Map<String, Object> data = (Map<String, Object>) ret.getResponse().get("data");
System.out.println("新用户注册总数:"+data.get("newRegistCount"));
System.out.println("用户注册总数:"+data.get("registTotalCount"));
System.out.println("活跃用户数:"+data.get("activeCount"));
System.out.println("在线用户数:"+data.get("onlineCount"));
}
}
小结
推送数据报表是个推消息推送服务中的重要组成成分,通过推送报表可以直观、准确地了解消息触达用户的详细情况,对推送数据一目了然。个推将针对报表服务不断优化,为APP开发者们提供更加完善的推送数据统计支持。
收起阅读 »uni-app展示富文本内容,图片的宽度溢出屏幕,解决方案!
这个问题虽然已经过去有一段时间了,但是我还是要写出来和大家分享!毕竟一个人或者是两个人写的也不一定会符合自己想要的结果,我也不知道会帮到多少人,还是要写一下,供大家参考。。。
现在的项目已经进入尾声,之前遇到这个问题解决了一段时间,在网上找了各种各种,说的不符合我。刚开始我是使用v-html这个api是可以加载富文本的(开始不了解rich-text),加载的富文本因为有图片,图片一直就是溢出屏幕的状态,在网上看了别人说的html.replace()做替换,根据标签替换的html.p.img{width:100% }试过了一遍都不好使,最后一个朋友说是用这个方法比较靠谱。确实很实用的一种方案,下边我写出来供参考:
html: <rich-text :nodes="content"></rich-text>
js: var richtext = res.data.list.Content;
const regex = new RegExp('<img', 'gi');
this.content = richtext.replace(regex, <img style="max-width: 100%;"
);
但是也有弊端:
这个问题虽然已经过去有一段时间了,但是我还是要写出来和大家分享!毕竟一个人或者是两个人写的也不一定会符合自己想要的结果,我也不知道会帮到多少人,还是要写一下,供大家参考。。。
现在的项目已经进入尾声,之前遇到这个问题解决了一段时间,在网上找了各种各种,说的不符合我。刚开始我是使用v-html这个api是可以加载富文本的(开始不了解rich-text),加载的富文本因为有图片,图片一直就是溢出屏幕的状态,在网上看了别人说的html.replace()做替换,根据标签替换的html.p.img{width:100% }试过了一遍都不好使,最后一个朋友说是用这个方法比较靠谱。确实很实用的一种方案,下边我写出来供参考:
html: <rich-text :nodes="content"></rich-text>
js: var richtext = res.data.list.Content;
const regex = new RegExp('<img', 'gi');
this.content = richtext.replace(regex, <img style="max-width: 100%;"
);
但是也有弊端:
收起阅读 »