
iOS上架被调查相关问题解答
今年以来因为审核的加严,很多iOS开发者账号提交ipa上架审核,直接反馈账号被调查了,也没有具体APP原因。
反馈邮件翻译
Hello,
We are unable to continue this app’s review because your Apple Developer Program account is currently under investigation for not following the App Store Review Guidelines’ Developer Code of Conduct.
Common practices that may lead to an investigation include, but are not limited to:
你好
我们无法继续此应用的审核,因为您的Apple开发者计划帐户目前正在接受调查,因为其未遵循App Store审核指南的“开发人员行为准则”。
可能导致调查的常见做法包括但不限于:
1、 不准确地描述应用程序或服务
2、误导性应用内容
3、参与不真实的评级和评论操作
4、提供误导性的客户支持响应
5、在解决方案中心提供误导性回复
6、从事误导性采购或诱饵开关计划
7、在应用内部或外部参与其他不诚实或欺诈活动
在我们的调查过程中,我们不会审核您提交的任何应用。 完成调查后,我们将通过解决方案中心通知您。
我们目前不要求您提供任何其他信息,也不会分享任何其他详细信息。 我们感您在调查期间继续保持耐心。
最好的祝福
下面列举一些解答,了解具体怎么回事。
1、为什么会账号会调查?
答:可能提交的APP是代码相似,市场上有很多类似App,认为马甲包,或者APP本身有违规,也是因为苹果机器审核越来越严格的原因,比较容易触发!
2、账号调查了会持续多久?
答:时间不确定,被调查多数在一个月内出结果,也有几个月的。如果调查没问题,也可能直接给通过APP审核。
3、账号被调查了怎么办?
答:调查期间,不要操作,申诉和加急都无效,只能等待出结果。在被调查阶段,无论如何提交都是统一回复的,回复你账号被调查,一直到出现2个结果,账号调查才结束。
1)直接过审;
2)被拒绝,有明确拒绝条款;
4.一个App被调查了,会影响同账号下的其他App吗?
答:被调查邮件里虽然说不会审核,但是其实是针对这一个App的,其他 App 项目还是可以提交正常审核了。同一个APP就不要换账号,或者删除再重新提交审核了,等调查结束在操作,调查完可能直接给通过。
5.把被调查的App转移到另一个开发者账号重新提交,会有问题吗?
答:因为是针对App的调查,所以转移App大概率无效,并且可能会被判定重复提交。另外,不建议把正在审核的App删除,很有可能被苹果认为是恶意隐瞒,直接导致封号。
6.此次调查是机审还是人审?
答:基本确定是机审,且是查重审核。因为从整个事件来看,是在清理灰色地带的应用。可能你的各个App的功能完全不同,但很多基础功能都是复用统一代码模块,这样确实会节省很多成本,但现在看很大概率会被误判为重复应用。
- 正常通过调查之后,App会有啥影响,会被列为重点审核对象吗?
答:目前来看没啥影响,甚至可能已被标记为安全对象,审核异常顺利。
iOS上架交流社区
http://www.applicationloader.net/
今年以来因为审核的加严,很多iOS开发者账号提交ipa上架审核,直接反馈账号被调查了,也没有具体APP原因。
反馈邮件翻译
Hello,
We are unable to continue this app’s review because your Apple Developer Program account is currently under investigation for not following the App Store Review Guidelines’ Developer Code of Conduct.
Common practices that may lead to an investigation include, but are not limited to:
你好
我们无法继续此应用的审核,因为您的Apple开发者计划帐户目前正在接受调查,因为其未遵循App Store审核指南的“开发人员行为准则”。
可能导致调查的常见做法包括但不限于:
1、 不准确地描述应用程序或服务
2、误导性应用内容
3、参与不真实的评级和评论操作
4、提供误导性的客户支持响应
5、在解决方案中心提供误导性回复
6、从事误导性采购或诱饵开关计划
7、在应用内部或外部参与其他不诚实或欺诈活动
在我们的调查过程中,我们不会审核您提交的任何应用。 完成调查后,我们将通过解决方案中心通知您。
我们目前不要求您提供任何其他信息,也不会分享任何其他详细信息。 我们感您在调查期间继续保持耐心。
最好的祝福
下面列举一些解答,了解具体怎么回事。
1、为什么会账号会调查?
答:可能提交的APP是代码相似,市场上有很多类似App,认为马甲包,或者APP本身有违规,也是因为苹果机器审核越来越严格的原因,比较容易触发!
2、账号调查了会持续多久?
答:时间不确定,被调查多数在一个月内出结果,也有几个月的。如果调查没问题,也可能直接给通过APP审核。
3、账号被调查了怎么办?
答:调查期间,不要操作,申诉和加急都无效,只能等待出结果。在被调查阶段,无论如何提交都是统一回复的,回复你账号被调查,一直到出现2个结果,账号调查才结束。
1)直接过审;
2)被拒绝,有明确拒绝条款;
4.一个App被调查了,会影响同账号下的其他App吗?
答:被调查邮件里虽然说不会审核,但是其实是针对这一个App的,其他 App 项目还是可以提交正常审核了。同一个APP就不要换账号,或者删除再重新提交审核了,等调查结束在操作,调查完可能直接给通过。
5.把被调查的App转移到另一个开发者账号重新提交,会有问题吗?
答:因为是针对App的调查,所以转移App大概率无效,并且可能会被判定重复提交。另外,不建议把正在审核的App删除,很有可能被苹果认为是恶意隐瞒,直接导致封号。
6.此次调查是机审还是人审?
答:基本确定是机审,且是查重审核。因为从整个事件来看,是在清理灰色地带的应用。可能你的各个App的功能完全不同,但很多基础功能都是复用统一代码模块,这样确实会节省很多成本,但现在看很大概率会被误判为重复应用。
- 正常通过调查之后,App会有啥影响,会被列为重点审核对象吗?
答:目前来看没啥影响,甚至可能已被标记为安全对象,审核异常顺利。
iOS上架交流社区
http://www.applicationloader.net/
收起阅读 »
uni-app友盟和官方离线多渠道打包
公司要求用友盟但是官方不支持友盟多渠道打包,所以必须一个一个打,但是官方又有打包限制而且速度很慢,所以想到了利用反编译重新打包
实现原理:apkTool反编译资源>修改AndroidManifest.xml>重新打包>应用签名
适用平台:Window、依赖JDK
操作步骤:解压压缩包script.vbs、apktool_2.4.1.jar、apktool.bat三个文件
1、将你生成的apk文件放入解呀目录与上述三个文件同目录
2、用记事本打开script.vbs
channelList = array("oppo","baidu","huawei","xiaomi") //渠道列表可以自己按需求添加或者移除
apkName = "app" //修改app为你apk对应的文件名(不带.apk后缀),如app.apk则填写app
keystore = "D:\\test.keystore" //修改为你签名文件路径
keystorePwd = "12345678" //签名文件密码
keystoreAlias = "testalias" //签名文件别名
3、保存,双击script.vbs
4、等待生成app_oppo.apk等文件则结束
注意事项:在执行后会产生apk对应的文件夹,如果apk发生变化要删除此文件夹不然打包是之前的
链接: https://pan.baidu.com/s/1s6Z4TNlW0vhn1PXq8xk5kw提取码: kf2f
公司要求用友盟但是官方不支持友盟多渠道打包,所以必须一个一个打,但是官方又有打包限制而且速度很慢,所以想到了利用反编译重新打包
实现原理:apkTool反编译资源>修改AndroidManifest.xml>重新打包>应用签名
适用平台:Window、依赖JDK
操作步骤:解压压缩包script.vbs、apktool_2.4.1.jar、apktool.bat三个文件
1、将你生成的apk文件放入解呀目录与上述三个文件同目录
2、用记事本打开script.vbs
channelList = array("oppo","baidu","huawei","xiaomi") //渠道列表可以自己按需求添加或者移除
apkName = "app" //修改app为你apk对应的文件名(不带.apk后缀),如app.apk则填写app
keystore = "D:\\test.keystore" //修改为你签名文件路径
keystorePwd = "12345678" //签名文件密码
keystoreAlias = "testalias" //签名文件别名
3、保存,双击script.vbs
4、等待生成app_oppo.apk等文件则结束
注意事项:在执行后会产生apk对应的文件夹,如果apk发生变化要删除此文件夹不然打包是之前的
链接: https://pan.baidu.com/s/1s6Z4TNlW0vhn1PXq8xk5kw提取码: kf2f

图片压缩及转换base64
本教程适合APP,利用5+进行图片压缩,然后转换成base64格式文件。
uni.chooseImage({
sizeType: ['original'],
sourceType: ['camera'],
count: 1,
success: async (res) => {
// 加载框
uni.showLoading({
title: '图片压缩中。。。'
});
let path = res.tempFilePaths[0];
// 压缩图片
plus.zip.compressImage({
src:path,
dst:path,
overwrite:true,
quality:20,
width: '780px',
height:'1040px',
format: 'jpg'
},
function(res) {
console.log("图片压缩完成");
let imgPathUrl = res.target;
let imgPathSize = res.size;
console.log("图片链接:"+imgPathUrl)
console.log("图片尺寸:"+imgPathSize)
// 文件系统中的读取文件对象,用于获取文件的内容
uni.showLoading({
title: '图片转换中。。。'
});
let reader = new plus.io.FileReader();
// 文件读取操作完成时的回调函数
reader.onloadend = (fileData)=> {
uni.hideLoading();
console.log('文件读取完成!');
let speech = fileData.target.result;//base64图片
// 去除base64文件头
let imgData = speech.replace(/^data:image\/\w+;base64,/, "");
};
reader.readAsDataURL(res.target);
},
function(error) {
console.log("Compress error!",error);
return;
});
},
fail:(err) => {
// 加载框
console.log(err)
uni.showToast({
icon: 'none',
duration:3000,
title: '拍照失败'
});
}
});
本教程适合APP,利用5+进行图片压缩,然后转换成base64格式文件。
uni.chooseImage({
sizeType: ['original'],
sourceType: ['camera'],
count: 1,
success: async (res) => {
// 加载框
uni.showLoading({
title: '图片压缩中。。。'
});
let path = res.tempFilePaths[0];
// 压缩图片
plus.zip.compressImage({
src:path,
dst:path,
overwrite:true,
quality:20,
width: '780px',
height:'1040px',
format: 'jpg'
},
function(res) {
console.log("图片压缩完成");
let imgPathUrl = res.target;
let imgPathSize = res.size;
console.log("图片链接:"+imgPathUrl)
console.log("图片尺寸:"+imgPathSize)
// 文件系统中的读取文件对象,用于获取文件的内容
uni.showLoading({
title: '图片转换中。。。'
});
let reader = new plus.io.FileReader();
// 文件读取操作完成时的回调函数
reader.onloadend = (fileData)=> {
uni.hideLoading();
console.log('文件读取完成!');
let speech = fileData.target.result;//base64图片
// 去除base64文件头
let imgData = speech.replace(/^data:image\/\w+;base64,/, "");
};
reader.readAsDataURL(res.target);
},
function(error) {
console.log("Compress error!",error);
return;
});
},
fail:(err) => {
// 加载框
console.log(err)
uni.showToast({
icon: 'none',
duration:3000,
title: '拍照失败'
});
}
});
收起阅读 »

uniapp是准备走向哪里
吐槽一下,最近打算用uniapp开发一个Android pad + window(NWjs)的项目。
在开发过程中,遇到的坑不要太多了,感觉官方仅考虑支持手机+小程序,发现问题如下:
- 官方组件:input、button等多端不一致:mobile、pad、h5
- 编译环境问题频繁:没有提示任何错误,但Android端调试模式下,pages中配置的页面加载不了,没做任何修改,突然又可以了。
- dev模式下,访问 局域网地址(编译好后,会提供本地+局域网两个地址),该地址下的sockjs-node 访问的地址确是 localhost,意味着如果你是一台开发,一台预览,那么恩,热更新无效了
我的建议
- 丰富生态:插件市场中需要有人去跟进维护
- 插件质量:优化现有组件,提高多端一致性(毕竟也会有人开发pad版本的),这部分甚至可以外包给第三方团队。
- 专注于底层环境,放弃hbuilderX或若降低hbuilderX在公司的比重,加强cli模式下的开发体验,丰富cli模式下的更多的配置指导、说明文档,以便提供更多的灵活性。
吐槽一下,最近打算用uniapp开发一个Android pad + window(NWjs)的项目。
在开发过程中,遇到的坑不要太多了,感觉官方仅考虑支持手机+小程序,发现问题如下:
- 官方组件:input、button等多端不一致:mobile、pad、h5
- 编译环境问题频繁:没有提示任何错误,但Android端调试模式下,pages中配置的页面加载不了,没做任何修改,突然又可以了。
- dev模式下,访问 局域网地址(编译好后,会提供本地+局域网两个地址),该地址下的sockjs-node 访问的地址确是 localhost,意味着如果你是一台开发,一台预览,那么恩,热更新无效了
我的建议
- 丰富生态:插件市场中需要有人去跟进维护
- 插件质量:优化现有组件,提高多端一致性(毕竟也会有人开发pad版本的),这部分甚至可以外包给第三方团队。
- 专注于底层环境,放弃hbuilderX或若降低hbuilderX在公司的比重,加强cli模式下的开发体验,丰富cli模式下的更多的配置指导、说明文档,以便提供更多的灵活性。

承接uni-app开发、后端PHP开发
PHP开发经验:6年
uni-app经验:2年
uni-app项目包括:搞笑社区、资讯、工具、商城、教育
说明:全职开发,长期接外包,速度与质量有保证
联系方式
微信:shalibaji_ge
PHP开发经验:6年
uni-app经验:2年
uni-app项目包括:搞笑社区、资讯、工具、商城、教育
说明:全职开发,长期接外包,速度与质量有保证
联系方式
微信:shalibaji_ge

2.6.11 V 3
更新到2.6.11, 会暴露很多以前没有的问题, 是不是规范语法要求了!!!! 不知道是新版本和v3编译器哪个的问题,降了版本2.6.9还是会有错 报错: [-1]$r[4][t0-0] is undefined at view.umd.min.js:1; [-1]$r[8][t0-0] is undefined at view.umd.min.js:1; 之前可以是undefined和null的地方 现在不可以; TypeError: undefined is not an object (evaluating ; 项目升级后用2.6.11的版本打包后, 运行时好多样式都错乱了, 晚上检查到凌晨呀,各种改; 用老版本打包一点问题没有;
更新到2.6.11, 会暴露很多以前没有的问题, 是不是规范语法要求了!!!! 不知道是新版本和v3编译器哪个的问题,降了版本2.6.9还是会有错 报错: [-1]$r[4][t0-0] is undefined at view.umd.min.js:1; [-1]$r[8][t0-0] is undefined at view.umd.min.js:1; 之前可以是undefined和null的地方 现在不可以; TypeError: undefined is not an object (evaluating ; 项目升级后用2.6.11的版本打包后, 运行时好多样式都错乱了, 晚上检查到凌晨呀,各种改; 用老版本打包一点问题没有;
收起阅读 »
Websocket直播间聊天室教程 - GoEasy快速实现聊天室
最近两年直播那个火啊,真的是无法形容!经常有朋友问起,我想实现一个直播间聊天或者我想开发一个聊天室, 要如何开始呢?
今天小编就手把手的教你用GoEasy做一个聊天室,当然也可以用于直播间内的互动。全套源码已经开源。
uniapp websocket聊天室体验demo:https://ext.dcloud.net.cn/plugin?id=5212
本教程主要目的是为大家介绍实现思路,为了确保本教程能帮助到使用不同前端技术的朋友,采用了HTML + JQuery的方式,后续还会推出Uniapp(vue/nvue)和小程序版本,大家可以持续关注。
我们这次要实现的聊天室,有两个界面,分别是:
- 登录界面
- 聊天室界面
登录
对于登录界面,我们期望:
- 用户可以输入自己的昵称
- 用户可以选择自己喜欢的头像
- 用户可以选择进入不同的聊天室(直播间)
实现步骤
登录界面的实现,不用多说,因为真的是So Easy! 一个简单的界面,只包含三个简单的逻辑:
- 验证是否输入昵称
- 验证是否选择一个头像
- 根据选择进入相应的聊天室
下边重点讲一下聊天室的实现。
聊天室(直播间)
当我们进入一个聊天室后,我们期望:
- 用户能看到当前有多少用户在线,这个数字能够实时的更新
- 用户能看到当前在线用户们的头像,而且能够实时的更新
- 如果有用户进入或离开聊天室
a. 聊天室会有“XXX进来了"或"XXX离开了"的提示
b. 在线用户的数字和用户的头像列表会随之自动更新 - 用户可以在聊天里发言
- 用户可以发送道具:火箭或者比心
实现步骤
第一步:聊天室界面显示
1. 初始化:
当用户选择了一个聊天室,显示聊天室界面之前,我们首先要进行以下初始化工作:
- 初始化当前用户currentUser,用户id,昵称,头像
- 初始化当前聊天室ID: currentRoomId
- 初始化GoEasy对象,注意一定要加上userId参数(可以是该用户的uuid或id等唯一标识,只有设置了userId的客户端在上下线时,才会触发上下线提醒)。同时需要将头像和昵称放入userData,当我们收到一个用户上线提醒的时候,我们需要知道这个用户的头像和昵称。
- 初始化onlineUsers,onlineUsers是用来存放当前聊天室在线用户数和在线用户列表。 将当前聊天室Id (currentRoomId)作为channel,执行goEasy.hereNow查询此刻聊天室在线用户数和用户列表,赋值给onlineUsers。除了在进入聊天室的时候初始化onlineUsers,当有用户进入或离开时,也会动态的更新onlineUsers。
- 以当前聊天室的id(currentRoomId)作为channel,执行subscriber方法监听和接收聊天室新消息。
- 以当前聊天室的id(currentRoomId)作为channel,执行subscriberPresence监听用户进入和离开事件。
参考代码:service.js
//初始化聊天室
this.joinRoom = function(userId,nickName, avatar, roomID) {
//初始化当前用户
this.currentUser = new User(userId, nickName, avatar);
//初始化当前聊天室id
this.currentRoomId = roomID;
//初始化goeasy,建立长连接
this.goeasy = new GoEasy({
host: "hangzhou.goeasy.io",
appkey: "您的appkey",
userId: this.currentUser.id,
userData: '{"nickname":"' + this.currentUser.nickname + '","avatar":"' + this.currentUser.avatar + '"}',
onConnected: function () {
console.log( "GoEasy connect successfully.")
},
onDisconnected: function () {
console.log("GoEasy disconnected.")
}
});
//查询当前在线用户列表,初始化onlineUsers对象
this.initialOnlineUsers();
//监听用户上下线提醒,实时更新onlineUsers对象
this.subscriberPresence();
//监听和接收新消息
this.subscriberNewMessage();
};
2. 页面展示:
完成初始化之后,就跳转到直播间界面,在页面上显示以下数据:
- 当前聊天室的名称
- 聊天记录,并且显示聊天室界面
- 展示聊天室界面
参考代码:controller.js
//页面切换到聊天室界面
function showChatRoom() {
//更新房间名
$("#chatRoom-header").find(".current-chatRoom-name").text(loginCommand.roomName);
//加载聊天历史
var chatHistory = service.loadChatHistory();
chatHistory.forEach(function (item) {
//展示发送的消息
var otherPerson = createCurrentChatRoomPerson(item.senderNickname + ":", item.content)
$(".chatRoom-content-box").append($(otherPerson));
});
//隐藏登录界面
$(".chat-login-box").hide();
// //显示聊天界面
$(".chatRoom-box").show();
// //滑动到最后一行
scrollBottom();
}
至此,我们已经完成了goeasy长连接的初始化,和一个聊天室静态展示。接下来,我们一起来看看如何让这个聊天室能够动起来。
第二步:聊天室互动
1. 实时更新在线用户数和头像列表
之前在service.initialOnlineUsers方法已经初始化onlineUsers对象,但聊天室随时都有用户进进出出,所以我们接下来还需要能够在有用户上线或下线的时候能够实时的更新onlineUsers,并且实时显示在页面上。
当我们收到一个用户上线提醒,我们将新上线的用户的信息存入在线用户对象onlineUsers里,当有用户离开时,在本地在线用户列表里删除。
参考代码:service.js
//监听用户上下线时间,维护onlineUsers对象
this.subscriberPresence = function() {
var self = this;
this.goeasy.subscribePresence({
channel: this.currentRoomId,
onPresence: function(presenceEvents) {
presenceEvents.events.forEach(function(event) {
var userId = event.userId;
var count = presenceEvents.clientAmount;
//更新onlineUsers在线用户数
self.onlineUsers.count = count;
//如果有用户进入聊天室
if (event.action == "join" || event.action == "online") {
var userData = JSON.parse(event.userData);
var nickName = userData.nickname;
var avatar = userData.avatar;
var user = new User(userId, nickName, avatar);
//将新用户加入onlineUsers列表
self.onlineUsers.users.push(user);
//触发界面的更新
self.onJoinRoom(user.nickname, user.avatar);
} else {
for (var i = 0; i < self.onlineUsers.users.length; i++) {
var leavingUser = self.onlineUsers.users[i];
if (leavingUser.id == userId) {
var nickName = leavingUser.nickname;
var avatar = leavingUser.avatar;
//将离开的用户从onlineUsers中删掉
self.onlineUsers.users.splice(i, 1);
//触发界面的更新
self.onLeaveRoom(nickName, avatar);
}
}
}
});
},
onSuccess : function () {
console.log("监听成功")
},
onFailed : function () {
console.log("监听失败")
}
});
};
2. 发送消息
- 初始化一个chatMessage对象,包含发送方id,昵称,消息内容,消息类型为chat
- 将chatMessage转换为一个Json格式的字符串
- 调用GoEasy的Publish方法,完成消息的发送
参考代码(service.js)
this.sendMessage = function(content) {
var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.CHAT, content);
var self = this;
this.goeasy.publish({
channel: self.currentRoomId,
message: JSON.stringify(message),
onSuccess: function() {
console.log("消息发布成功。");
},
onFailed: function(error) {
console.log("消息发送失败,错误编码:" + error.code + " 错误信息:" + error.content);
}
});
};
3. 接收和显示新消息/道具
之前我们已经在初始化页面的时候执行了service.subscriberNewMessage(),当我们收到一条消息时:
- 根据消息类型判断是一条聊天消息,还是一个道具
- 如果收到的是一条聊天消息,直接显示到界面
- 如果是道具,就播放动画
参考代码(service.js)
//监听消息或道具
this.subscriberNewMessage = function() {
var self = this;
this.goeasy.subscribe({
channel: this.currentRoomId, //替换为您自己的channel
onMessage: function(message) {
var chatMessage = JSON.parse(message.content);
//todo:事实上不推荐在前端收到时保存, 一个用户开多个窗口,会导致重复保存, 建议所有消息都是都在发送时在服务器端保存,这里只是为了演示
self.restapi.saveChatMessage(self.currentRoomId, chatMessage);
//如果收到的是一个消息,就显示为消息
if (chatMessage.type == MessageType.CHAT) {
var selfSent = chatMessage.senderUserId == self.currentUser.id;
var content = JSON.parse(message.content);
self.onNewMessage(chatMessage.senderNickname, content, selfSent);
}
//如果收到的是一个道具,就播放道具动画
if (chatMessage.type == MessageType.PROP) {
if (chatMessage.content == Prop.ROCKET) {
self.onNewRocket(chatMessage.senderNickname);
}
if (chatMessage.content == Prop.HEART) {
self.onNewHeart(chatMessage.senderNickname);
}
}
}
});
};
4. 发送和接收并展示道具
其实和发送消息的实现几乎是一样的,具体代码请参考service.js的sendProp方法,controller.js的onNewHeart()方法。动画的播放,使用了TweenMax这个库,主要是为了展示一个实现思路,小编也不知道这个库是否有很好的兼容性,以及是否能够用在Uniapp和小程序下,知道的朋友可以留言分享给大家。
this.sendProp = function(prop) {
var self = this;
var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.PROP, prop);
this.goeasy.publish({
channel: self.currentRoomId,
message: JSON.stringify(message),
onSuccess: function() {
console.log("道具发布成功。");
},
onFailed: function(error) {
console.log("道具发送失败,错误编码:" + error.code + " 错误信息:" + error.content);
}
});
};
至此,一个聊天室就搞定了,是不是很简单?
如果阅读本文或开发中有任何问题,也欢迎在GoEasy官网(https://www.goeasy.io)添加GoEasy为好友,来获得更多技术支持。
GoEasy系列教程:
最近两年直播那个火啊,真的是无法形容!经常有朋友问起,我想实现一个直播间聊天或者我想开发一个聊天室, 要如何开始呢?
今天小编就手把手的教你用GoEasy做一个聊天室,当然也可以用于直播间内的互动。全套源码已经开源。
uniapp websocket聊天室体验demo:https://ext.dcloud.net.cn/plugin?id=5212
本教程主要目的是为大家介绍实现思路,为了确保本教程能帮助到使用不同前端技术的朋友,采用了HTML + JQuery的方式,后续还会推出Uniapp(vue/nvue)和小程序版本,大家可以持续关注。
我们这次要实现的聊天室,有两个界面,分别是:
- 登录界面
- 聊天室界面
登录
对于登录界面,我们期望:
- 用户可以输入自己的昵称
- 用户可以选择自己喜欢的头像
- 用户可以选择进入不同的聊天室(直播间)
实现步骤
登录界面的实现,不用多说,因为真的是So Easy! 一个简单的界面,只包含三个简单的逻辑:
- 验证是否输入昵称
- 验证是否选择一个头像
- 根据选择进入相应的聊天室
下边重点讲一下聊天室的实现。
聊天室(直播间)
当我们进入一个聊天室后,我们期望:
- 用户能看到当前有多少用户在线,这个数字能够实时的更新
- 用户能看到当前在线用户们的头像,而且能够实时的更新
- 如果有用户进入或离开聊天室
a. 聊天室会有“XXX进来了"或"XXX离开了"的提示
b. 在线用户的数字和用户的头像列表会随之自动更新 - 用户可以在聊天里发言
- 用户可以发送道具:火箭或者比心
实现步骤
第一步:聊天室界面显示
1. 初始化:
当用户选择了一个聊天室,显示聊天室界面之前,我们首先要进行以下初始化工作:
- 初始化当前用户currentUser,用户id,昵称,头像
- 初始化当前聊天室ID: currentRoomId
- 初始化GoEasy对象,注意一定要加上userId参数(可以是该用户的uuid或id等唯一标识,只有设置了userId的客户端在上下线时,才会触发上下线提醒)。同时需要将头像和昵称放入userData,当我们收到一个用户上线提醒的时候,我们需要知道这个用户的头像和昵称。
- 初始化onlineUsers,onlineUsers是用来存放当前聊天室在线用户数和在线用户列表。 将当前聊天室Id (currentRoomId)作为channel,执行goEasy.hereNow查询此刻聊天室在线用户数和用户列表,赋值给onlineUsers。除了在进入聊天室的时候初始化onlineUsers,当有用户进入或离开时,也会动态的更新onlineUsers。
- 以当前聊天室的id(currentRoomId)作为channel,执行subscriber方法监听和接收聊天室新消息。
- 以当前聊天室的id(currentRoomId)作为channel,执行subscriberPresence监听用户进入和离开事件。
参考代码:service.js
//初始化聊天室
this.joinRoom = function(userId,nickName, avatar, roomID) {
//初始化当前用户
this.currentUser = new User(userId, nickName, avatar);
//初始化当前聊天室id
this.currentRoomId = roomID;
//初始化goeasy,建立长连接
this.goeasy = new GoEasy({
host: "hangzhou.goeasy.io",
appkey: "您的appkey",
userId: this.currentUser.id,
userData: '{"nickname":"' + this.currentUser.nickname + '","avatar":"' + this.currentUser.avatar + '"}',
onConnected: function () {
console.log( "GoEasy connect successfully.")
},
onDisconnected: function () {
console.log("GoEasy disconnected.")
}
});
//查询当前在线用户列表,初始化onlineUsers对象
this.initialOnlineUsers();
//监听用户上下线提醒,实时更新onlineUsers对象
this.subscriberPresence();
//监听和接收新消息
this.subscriberNewMessage();
};
2. 页面展示:
完成初始化之后,就跳转到直播间界面,在页面上显示以下数据:
- 当前聊天室的名称
- 聊天记录,并且显示聊天室界面
- 展示聊天室界面
参考代码:controller.js
//页面切换到聊天室界面
function showChatRoom() {
//更新房间名
$("#chatRoom-header").find(".current-chatRoom-name").text(loginCommand.roomName);
//加载聊天历史
var chatHistory = service.loadChatHistory();
chatHistory.forEach(function (item) {
//展示发送的消息
var otherPerson = createCurrentChatRoomPerson(item.senderNickname + ":", item.content)
$(".chatRoom-content-box").append($(otherPerson));
});
//隐藏登录界面
$(".chat-login-box").hide();
// //显示聊天界面
$(".chatRoom-box").show();
// //滑动到最后一行
scrollBottom();
}
至此,我们已经完成了goeasy长连接的初始化,和一个聊天室静态展示。接下来,我们一起来看看如何让这个聊天室能够动起来。
第二步:聊天室互动
1. 实时更新在线用户数和头像列表
之前在service.initialOnlineUsers方法已经初始化onlineUsers对象,但聊天室随时都有用户进进出出,所以我们接下来还需要能够在有用户上线或下线的时候能够实时的更新onlineUsers,并且实时显示在页面上。
当我们收到一个用户上线提醒,我们将新上线的用户的信息存入在线用户对象onlineUsers里,当有用户离开时,在本地在线用户列表里删除。
参考代码:service.js
//监听用户上下线时间,维护onlineUsers对象
this.subscriberPresence = function() {
var self = this;
this.goeasy.subscribePresence({
channel: this.currentRoomId,
onPresence: function(presenceEvents) {
presenceEvents.events.forEach(function(event) {
var userId = event.userId;
var count = presenceEvents.clientAmount;
//更新onlineUsers在线用户数
self.onlineUsers.count = count;
//如果有用户进入聊天室
if (event.action == "join" || event.action == "online") {
var userData = JSON.parse(event.userData);
var nickName = userData.nickname;
var avatar = userData.avatar;
var user = new User(userId, nickName, avatar);
//将新用户加入onlineUsers列表
self.onlineUsers.users.push(user);
//触发界面的更新
self.onJoinRoom(user.nickname, user.avatar);
} else {
for (var i = 0; i < self.onlineUsers.users.length; i++) {
var leavingUser = self.onlineUsers.users[i];
if (leavingUser.id == userId) {
var nickName = leavingUser.nickname;
var avatar = leavingUser.avatar;
//将离开的用户从onlineUsers中删掉
self.onlineUsers.users.splice(i, 1);
//触发界面的更新
self.onLeaveRoom(nickName, avatar);
}
}
}
});
},
onSuccess : function () {
console.log("监听成功")
},
onFailed : function () {
console.log("监听失败")
}
});
};
2. 发送消息
- 初始化一个chatMessage对象,包含发送方id,昵称,消息内容,消息类型为chat
- 将chatMessage转换为一个Json格式的字符串
- 调用GoEasy的Publish方法,完成消息的发送
参考代码(service.js)
this.sendMessage = function(content) {
var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.CHAT, content);
var self = this;
this.goeasy.publish({
channel: self.currentRoomId,
message: JSON.stringify(message),
onSuccess: function() {
console.log("消息发布成功。");
},
onFailed: function(error) {
console.log("消息发送失败,错误编码:" + error.code + " 错误信息:" + error.content);
}
});
};
3. 接收和显示新消息/道具
之前我们已经在初始化页面的时候执行了service.subscriberNewMessage(),当我们收到一条消息时:
- 根据消息类型判断是一条聊天消息,还是一个道具
- 如果收到的是一条聊天消息,直接显示到界面
- 如果是道具,就播放动画
参考代码(service.js)
//监听消息或道具
this.subscriberNewMessage = function() {
var self = this;
this.goeasy.subscribe({
channel: this.currentRoomId, //替换为您自己的channel
onMessage: function(message) {
var chatMessage = JSON.parse(message.content);
//todo:事实上不推荐在前端收到时保存, 一个用户开多个窗口,会导致重复保存, 建议所有消息都是都在发送时在服务器端保存,这里只是为了演示
self.restapi.saveChatMessage(self.currentRoomId, chatMessage);
//如果收到的是一个消息,就显示为消息
if (chatMessage.type == MessageType.CHAT) {
var selfSent = chatMessage.senderUserId == self.currentUser.id;
var content = JSON.parse(message.content);
self.onNewMessage(chatMessage.senderNickname, content, selfSent);
}
//如果收到的是一个道具,就播放道具动画
if (chatMessage.type == MessageType.PROP) {
if (chatMessage.content == Prop.ROCKET) {
self.onNewRocket(chatMessage.senderNickname);
}
if (chatMessage.content == Prop.HEART) {
self.onNewHeart(chatMessage.senderNickname);
}
}
}
});
};
4. 发送和接收并展示道具
其实和发送消息的实现几乎是一样的,具体代码请参考service.js的sendProp方法,controller.js的onNewHeart()方法。动画的播放,使用了TweenMax这个库,主要是为了展示一个实现思路,小编也不知道这个库是否有很好的兼容性,以及是否能够用在Uniapp和小程序下,知道的朋友可以留言分享给大家。
this.sendProp = function(prop) {
var self = this;
var message = new ChatMessage(this.currentUser.id,this.currentUser.nickname, MessageType.PROP, prop);
this.goeasy.publish({
channel: self.currentRoomId,
message: JSON.stringify(message),
onSuccess: function() {
console.log("道具发布成功。");
},
onFailed: function(error) {
console.log("道具发送失败,错误编码:" + error.code + " 错误信息:" + error.content);
}
});
};
至此,一个聊天室就搞定了,是不是很简单?
如果阅读本文或开发中有任何问题,也欢迎在GoEasy官网(https://www.goeasy.io)添加GoEasy为好友,来获得更多技术支持。
GoEasy系列教程:

uni-app自定义长按事件
<view @touchmove="handletouchmove" @touchstart="handletouchstart" @touchend="handletouchend" >
</view>
handletouchstart(e) {
this.timeOutEvent = setTimeout(() => {
this.onLongPress(e)
}, 1000); //这里设置定时器,定义长按1000毫秒触发长按事件,时间可以自己改,
return false;
},
handletouchend() {
clearTimeout(this.time); //清除定时器
if (this.time != 0) {
//处理点击时间
}
return false;
},
handletouchmove() {
clearTimeout(this.time); //清除定时器
this.time = 0;
},
onLongPress(e) {
// 处理长按事件
}
<view @touchmove="handletouchmove" @touchstart="handletouchstart" @touchend="handletouchend" >
</view>
handletouchstart(e) {
this.timeOutEvent = setTimeout(() => {
this.onLongPress(e)
}, 1000); //这里设置定时器,定义长按1000毫秒触发长按事件,时间可以自己改,
return false;
},
handletouchend() {
clearTimeout(this.time); //清除定时器
if (this.time != 0) {
//处理点击时间
}
return false;
},
handletouchmove() {
clearTimeout(this.time); //清除定时器
this.time = 0;
},
onLongPress(e) {
// 处理长按事件
}
收起阅读 »

uniAPP开发、团队开发uniapp
需求明确
沟通、整理和明确客户需求,撰写文档,搭建功能脑图架构
交互体验
用户体验设计、用户场景模拟、原型设计
UI设计
界面、色彩视觉设计、图标设计及布局设计
功能开发
服务器端、苹果及安卓端、前端H5开发/接口开发
测试验收
BUG修改、功能调整和优化、验收文档完善,上线应用市场
售后服务
后续技术维护、持续跟进、项目运营支撑
一⑤8叁2一①伍0玖九
需求明确
沟通、整理和明确客户需求,撰写文档,搭建功能脑图架构
交互体验
用户体验设计、用户场景模拟、原型设计
UI设计
界面、色彩视觉设计、图标设计及布局设计
功能开发
服务器端、苹果及安卓端、前端H5开发/接口开发
测试验收
BUG修改、功能调整和优化、验收文档完善,上线应用市场
售后服务
后续技术维护、持续跟进、项目运营支撑
一⑤8叁2一①伍0玖九

Sliver Rest Wp api:全功能的WordPress api工具
Sliver Rest Wp api:全功能的WordPress api工具:无须会后台,轻松构建你自己的任何项目
关于Sliver Rest Wp api
Sliver Rest Wp api的前身是SliverRingApi。后来在某一天一个客户告诉我他网站出了很严重的安全性问题,还说他那边的人查出来问题就出在我的接口上,我当时一脸懵逼。我那接口全都是是get各种查,怎么会出问题?
当然,即便是不知道到底问题出在了谁身上(我自己的官网演示站三五个月都没闹动),我还是停止了继续发售SliverRingApi并且准备重构,也就是你现在看到的这个版本。
和SliverRingApi一样Sliver Rest Wp api包含了已有的所有功能,并且还将包含SliverRingApi所列出来的那些开发的所有功能,但这还远远不够,在未来的日子里我还将把本工具打造的更加强大,更加持久,所有第三方登录注册、所有支付、所有实用的功能,市面上有的,你能想得到的,我将全部给你!
Sliver Rest Wp api能做什么
在开始之前,我需要说明一下本工具采用的技术架构:WordPress+lumen
WordPress
如果你用过WordPress我相信你会知道它有多强大,如果你没有用过,那么我建议你去折腾一下,它绝对是开源建站程序中最优秀的一个,而且市场占有率极高,虽然在官方和大多数人看来,把WordPress定义成了一款博客程序或者cms但其实它远远不止于此,它能做的更多,所有的、全类型的都能做。
Lumen
最快的api开发框架。
传统的app或者前后端分离或者网站开发方式
试想一下,如果你现在要做一个app、一个前后端分离的网站你需要做什么?
一、开发一个后端管理程序?
那么为什么不能用WordPress呢,它免费开源,插件、主题数十万有余,开发者可文档满地都是,你用它来当你的后端,来管理你的内容难道不是相对于你重新开发来说更完美的嘛,尤其是对于不会开发后端程序的人来说。
二、再为你的app或者前后端分离的网站开发一套api?
那么为什么不用本套工具呢,本套工具的开发者也就是本人,折腾WordPress四年有余,对WordPress的底层以及特性都了如膝盖(离了如指掌还略远),而本接口则高度的利用了WordPress的特性并且结合了Lumen快速开发api的优越性,做到了快速、安全、方便、拓展于一体。
而且为了考虑不同类型的用途、不用类型程序的开发,我还做了特殊的设计,总而言之,只要你会前端,你可以利用它做任何事情!
所以,现在你的后端管理程序有了,api接口也有了,你只需要写你的前端(那正是我所不擅长的),其他所有事情就交给我来就好了!
与WP Rest Api相比
总所周知WordPress自己也有一套api叫做rest api,那么我为什么还要写这套工具呢:
与rest api相比更灵活,更安全
结合lumen(不知道这个的可以去百度一下)
结合了更多第三方功能:登录注册、支付等等
不需要安装插件(现在市面上大多数rest api拓展都推荐装一堆插件)
你可以禁用WordPress的rest api(据说这玩意是不安全的),不会影响本套工具的功能
能做什么?
最后在总结一下:
为了那些所有不会后端只会前端的开发者提供所有开发接口+详细文档
结合WordPress+lumen为你提供更快、更高效、更安全的api
帮助所有只会前端不会后端的开发者实现开发所有类型项目的梦想(接口即服务)
得了,不多说了:你自己来看吧,Sliver Rest Wp api:全功能的WordPress api工具):轻松构建你自己的任何项目
已有接口
文章类
sliver-rest-wp-api文章列表接口
sliver-rest-wp-api:文章阅读接口
分类类
Sliver Rest Wp api:全功能的WordPress api工具:无须会后台,轻松构建你自己的任何项目
关于Sliver Rest Wp api
Sliver Rest Wp api的前身是SliverRingApi。后来在某一天一个客户告诉我他网站出了很严重的安全性问题,还说他那边的人查出来问题就出在我的接口上,我当时一脸懵逼。我那接口全都是是get各种查,怎么会出问题?
当然,即便是不知道到底问题出在了谁身上(我自己的官网演示站三五个月都没闹动),我还是停止了继续发售SliverRingApi并且准备重构,也就是你现在看到的这个版本。
和SliverRingApi一样Sliver Rest Wp api包含了已有的所有功能,并且还将包含SliverRingApi所列出来的那些开发的所有功能,但这还远远不够,在未来的日子里我还将把本工具打造的更加强大,更加持久,所有第三方登录注册、所有支付、所有实用的功能,市面上有的,你能想得到的,我将全部给你!
Sliver Rest Wp api能做什么
在开始之前,我需要说明一下本工具采用的技术架构:WordPress+lumen
WordPress
如果你用过WordPress我相信你会知道它有多强大,如果你没有用过,那么我建议你去折腾一下,它绝对是开源建站程序中最优秀的一个,而且市场占有率极高,虽然在官方和大多数人看来,把WordPress定义成了一款博客程序或者cms但其实它远远不止于此,它能做的更多,所有的、全类型的都能做。
Lumen
最快的api开发框架。
传统的app或者前后端分离或者网站开发方式
试想一下,如果你现在要做一个app、一个前后端分离的网站你需要做什么?
一、开发一个后端管理程序?
那么为什么不能用WordPress呢,它免费开源,插件、主题数十万有余,开发者可文档满地都是,你用它来当你的后端,来管理你的内容难道不是相对于你重新开发来说更完美的嘛,尤其是对于不会开发后端程序的人来说。
二、再为你的app或者前后端分离的网站开发一套api?
那么为什么不用本套工具呢,本套工具的开发者也就是本人,折腾WordPress四年有余,对WordPress的底层以及特性都了如膝盖(离了如指掌还略远),而本接口则高度的利用了WordPress的特性并且结合了Lumen快速开发api的优越性,做到了快速、安全、方便、拓展于一体。
而且为了考虑不同类型的用途、不用类型程序的开发,我还做了特殊的设计,总而言之,只要你会前端,你可以利用它做任何事情!
所以,现在你的后端管理程序有了,api接口也有了,你只需要写你的前端(那正是我所不擅长的),其他所有事情就交给我来就好了!
与WP Rest Api相比
总所周知WordPress自己也有一套api叫做rest api,那么我为什么还要写这套工具呢:
与rest api相比更灵活,更安全
结合lumen(不知道这个的可以去百度一下)
结合了更多第三方功能:登录注册、支付等等
不需要安装插件(现在市面上大多数rest api拓展都推荐装一堆插件)
你可以禁用WordPress的rest api(据说这玩意是不安全的),不会影响本套工具的功能
能做什么?
最后在总结一下:
为了那些所有不会后端只会前端的开发者提供所有开发接口+详细文档
结合WordPress+lumen为你提供更快、更高效、更安全的api
帮助所有只会前端不会后端的开发者实现开发所有类型项目的梦想(接口即服务)
得了,不多说了:你自己来看吧,Sliver Rest Wp api:全功能的WordPress api工具):轻松构建你自己的任何项目
已有接口
文章类
sliver-rest-wp-api文章列表接口
sliver-rest-wp-api:文章阅读接口
分类类
sliver-rest-wp-api:获取网站分类列表
根据父级分类获取子分类
获取分类信息接口
获取分类下的标签列表

UniAPP这些天使用心得
用uniApp有一段时间了,给我的第一感受是,第一次对程序开发失去了信心。
不知道是什么鬼才发明了这个东西,搞出来折磨人。
难用不说,经常是出一个错,你根本定位不到错误的位置,更奇葩的是,在游览器能显示正常的东西,在移动端就显示一片空白。
官方出的调试工具,竟然也能有bug,用有bug的调试工具去调试bug,真奇怪。
不得不说,开发uniAPP,真是一件令人沮丧的事,可惜我人微言轻,不能说服领导用flutter。
最后,我只想为我这些天掉的头发说一句,UniAPP,我可去你m的吧
用uniApp有一段时间了,给我的第一感受是,第一次对程序开发失去了信心。
不知道是什么鬼才发明了这个东西,搞出来折磨人。
难用不说,经常是出一个错,你根本定位不到错误的位置,更奇葩的是,在游览器能显示正常的东西,在移动端就显示一片空白。
官方出的调试工具,竟然也能有bug,用有bug的调试工具去调试bug,真奇怪。
不得不说,开发uniAPP,真是一件令人沮丧的事,可惜我人微言轻,不能说服领导用flutter。
最后,我只想为我这些天掉的头发说一句,UniAPP,我可去你m的吧

uniapp中使用sqlite对本地缓存下数据进行处理
先说下我决定用sqlite的条件:
主要是流程处理,需要在无网络的情况下实现,数据量多的时候用h5的缓存完全不够,在看了文档之后选择使用SQLite ,早起在mui的时候使用的indexDB;
因为在社区也没收到具体的,所以写下记录下也和小伙伴分享下,有啥问题可以互相交流下。
该文档中用到的两个点 (SQLite 和vue中的mixin)
我这有两个环境,我先说一个简单;
- 主页菜单进去 到列表界面 从该步骤开始缓存本地数据;
- 从列表点击进入到详情,并在详情操作。有网络正常,无网络时存入表中,当切换到有网情况后,进行提交;
使用SQLite时需要先开权限,在配置文件中,如下图

SQLite官方demo ; 在hbuilderX 下新建demo pages/API/sqlite/sqlite 下官方提供的sqlite使用。为了使用方便,我把需要的单独提出来;
该出贴出function 代码,不做具体说明;
function openComDB(name, path, callback) {
plus.sqlite.openDatabase({
name: name,
path: path,
success: function(e) {
// plus.nativeUI.alert('打开数据库成功');
callback(e)
},
fail: function(e) {
// plus.nativeUI.alert("打开数据库失败");
callback(e);
}
})
}
function executeSQL(name, sql, callback) {
plus.sqlite.selectSql({
name: name,
sql: sql,
success: function(e) {
// console.log("查询数据库:" + name + ",表:" + sql + ";的");
// console.log(JSON.stringify(e));
callback(e);
},
fail: function(e) {
console.log("查询数据库失败:" + JSON.stringify(e));
callback(e);
}
})
}
export{
openComDB,
executeSQL
}
该出 进入正题;
在需要用到的vue文件下,引入上方function; 路径是自己的;
import {openComDB,executeSQL,dropSQL} from '../../common/env.js'
说下简单的思路:
1.进入lists界面,先判断是否有网络,有网络正常调用,无网络时需要判断本地数据库中是否有数据,无数据则第一次进入。有数据需要调用本地数据中的数据;
ps
在有些uni使用上可能会和现在的有点出入,因为这个是早期写的NFC写入的功能,用的nvue的,还是weex的模式下。
贴上部分相关代码
created() {
this.getNetworkType(); //初始化网络当前状态;
uni.onNavigationBarButtonTap((e) => {
//该处是因为的导航栏右边加了两个筛选条件。可以忽略。 不过如果做离线需要筛选的,筛选条件等数据同样需要缓存
if (e.index == 1) {
this.pickType();
} else if (e.index == 0) {
this.pickBuild()
}
})
},
methods: {
getNetworkType() {
//获取网络信息
uni.getNetworkType({
success: res => {
this.netWork = res.networkType;
this.isOpenDB();
}
})
},
isOpenDB() {
console.log('是否打开数据库');
var isOpen = plus.sqlite.isOpenDatabase({
name: 'nfc', //数据库的名字
path: '_doc/nfcList.db' //地址
});
console.log(!isOpen);
if (!isOpen) {
console.log('Unoepned:' + isOpen);
// plus.nativeUI.alert('Unopened!');
this.openDB(); //打開DB
} else {
// plus.nativeUI.alert('Opened!');
this.isNet();
// this.getLocalType();
}
},
openDB() {
//SQLite
openComDB('nfc', '_doc/nfcList.db', res => {
console.log('打开数据库');
this.isNet();
});
},
isNet(){
//网络问题;
if (this.netWork == 'wifi' || this.netWork == '4g') {
//在有网络情况下,会先情况之前的表,为了防止没有及时更新到数据。update我是嫌麻烦,没这样写。就这样暴力写了。
console.log('wifi || 4g ');
this.dropTable("pointLists");
this.dropTable("codeTypeTable");
this.dropTable("codeTable");
this.dropTable("statusTable")
//删了之后创建表
this.createCodeTable();
this.createCodeTypeTable();
this.createBuildTable();
this.createLists();
this.createUpateStatus(); // 离线时更新
//然后把数据插入进去
this.getTabType(); //初始化格式
this.getPonitType(); //获取code。默认值
this.getCodeType(); // 类型下的选择项;
this.getBuilds(); // 获取建筑物数据
} else {
//无网络时;
console.log('初始化无网络');
this.getTabType(); //初始化格式
this.locCodeTypeItem();
this.selBuildFun();
}
//上面贴的drop,create,insert 我会在下面贴出部分代码。不会全部贴.SQL语句不会的建议百度找文档多看下,无非就是增删改查
},
}
整个代码贴上太长了,我还是分段写吧;
本身就是个带tab类型的列表。延用nvue 的 weex的形式,未改成uniapp形式,如需参照此处tab,需要看之前的官方demo。 建议用官方新的uniapp模式,我是懒得改。
//初始化
getTabType() {
//初始化列表;
let ary = [];
for (let i = 0; i < this.tabBars.length; i++) {
let aryItem = {
loadingText: "",
data: [],
pageNum: 1
}
ary.push(aryItem);
}
this.newsitems = ary;
if(this.netWork == "wifi" || this.netWork == "4g"){
this.getPointList() //默认加载未处理; 有网络下,正常调用接口
}else{
this.selPointList() //无网络下调用数据库表中的数据;
}
},
再有网络调用接口时需要将 数据insert到创建的数据表中;
创建数据库中的表
我的思路是整个app为一个数据库,有各种不同的表。 目前只有nfc中用到了,所以数据库的name 就取名 nfc 了,未进行修改;
现在正儿八经的创建表;
用简单的建筑信息为例;根据自己需要的进行创建表;
createBuildTable() {
//创建建筑物类型表;
var sqlTable = 'create table if not exists buildTable("id" INT(10) NOT NULL UNIQUE,"name" CHAR,"gridCode" CHAR)'
executeSQL('nfc', sqlTable, res => {})
},
在有网调用接口时,insert 表;需要与上方创建的完全对应,最后一个不加 “,” 只有不对应就会报错
insertBuildCode() {
for (var i = 0; i < this.builds.length; i++) {
var sqlInsert = "insert into buildTable values('";
sqlInsert += this.builds[i].id + "','";
sqlInsert += this.builds[i].name + "','";
sqlInsert += this.builds[i].gridCode + "'";
sqlInsert += ')';
executeSQL('nfc', sqlInsert, res => {
})
}
this.getLocBuilds() //插入成功后,就可以查看结果。此处用到就直接赋值,没用到,或者别处用到,就在别处调用
},
getLocBuilds() {
executeSQL('nfc', 'select * from buildTable', res => {
console.log("建筑物查询结果");
this.builds = res;
this.selBuildFun()
})
},
查看就这样,就写个简单的例子。列表数据比较多。就不放上。如果详情的数据是通过detailById 接口调用,list表的需要将详情的数据加进去。
当整个数据存上后,就是点击list跳转到detail界面。
detail正常的vue文件。
有网络时正常查看,进行提交操作。
无网络时,查看本地数据库的表。
所有创建和插入都是在list进行的,detail.vue中 进行 查看,和更改,如果需要删除 也可。 (因为在list就很有可能是无网络的情况,所以所有的数据都在列表获取到了)
list ---> detail
可以带id ,也可以带json、建议带唯一的id就可以了,可以在detail select 出需要的数据;
我这里是觉着数据已经在list存上了,detail不如直接使用,不管用网络无网络,本身做提交,后台也不会不停的改数据。
比较严谨的可以有网络时候调用详情接口,无网络时在本地查。
查询代码上面贴了列子此处就不放上了。
简单说一下离线提交。会用到VUE的mixin。
离线提交的思路 。在list的时候需要创建 提交的 表 locSubTable(随便起的名字方便后面提到) ,并在最后 加上 flag 字段 (这个自己随意) 类型INT 。因为没有布尔。所以在插入的时候需要用flag 0,1 进行判断是否需要缓存提交。
0 为 false 1 为 true ;
在detail 内,若 是离线时进行提交,在 locSubTable 进行insert 。最后flag 插入 为 0 ;
在无网络切换至 wifi/4g下,查询 locSubTable 表 where flag = 0 的数据。 返回的 res.length = 0 的时候,无离线提交。
ren.length 有数据需要循环提交。接口提交成功之后需要。update locSubTable 中的 flag = 1 。表示缓存的数据已经被成功提交掉。
还需要更新下list 表的数据,将状态从 未处理 update 为 已处理 (此处更改是因为我的列表是tab类型。流程处理后的状态需要及时更新掉。)
写之前是打算贴代码的,写上之后发现不知道从哪里下手贴,只能说下处理的思路。
后面简单说下mixin的使用。是为了在全局监听网络变化,并能在app下进行离线提交。
在社区找到了uniapp是可以支持的。不过好像用的也不多。(此处该贴上官方链接,找到补上)
因为监听网络切换的时候,界面不会只停留在当前页。但是每个页面都写上也太多了,后来在vue的文档中找到了mixin 混入
《当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项 》
在写的时候也发现的一些坑,会一起记录下。(如果写完发现太多了那我就是被疫情在家憋惨了,憋成了话痨,尴尬)
再common中新建 mixin.js ; mixin的基础使用,可以在vue官方文档查找。
var isLoc ={
data() {
return {} // 有需要的data,也可以加上,我没有用到。
},
methoad:{
//因为需要在其他地方查数据库,所以依旧在最开始打开数据库
isOpenDB() {
console.log("mixin 中是否打开数据库");
var isOpen = plus.sqlite.isOpenDatabase({
name: 'nfc',
path: '_doc/nfcList.db'
});
console.log("数据库是否打开:" + !isOpen);
if (!isOpen) {
console.log('unopen:' + isOpen)
this.openDB()
}
},
openDB() {
openComDB('nfc', '_doc/nfcList.db', res => {
console.log("mixin:打开数据库");
this.getLocUser()
})
},
selStatusList() {
//查询是否有离线写入,未提交数据; flag == 0 false。
// console.log("切换至网络,查询是否有缓存未提交数据");
executeSQL('nfc', 'select * from statusTable where flag = 0', res => {
// console.log(res)
// console.log(res.length);
for (var i = 0; i < res.length; i++) {
this.submitPointFun(res[i].id); //返回的参数根据实际情况来的。
}
})
},
submitPointFun(curId) { // 提交巡检接口
//提交接口
getData({id: curId},data => { //填自己的接口、本身这个我封了下,按照自己的来。
urlFuc("xxxxxxxx", data, res => {
console.log("当前ID:" + curId + "提交成功");
//返回 成功后, 对列表的数据进行update 。
this.updatePointStatus(curId);
this.updatePointList(curId)
});
});
},
updatePointStatus(curId) {
// 切换至有网络后,提交成功后,更新已提交成功数据 更新巡检点状态
//修改flag = 1 - true ;
var updateSQL = 'update statusTable set flag = 1 where id = ' + curId;
executeSQL('nfc', updateSQL, res => {
console.log("更新数据");
})
},
updatePointList(curId) {
// 状态更改后,更改列表改ID的数据
var updateSQL = 'update pointLists set status = "已关联" where id = ' + curId;
console.log(updateSQL);
executeSQL('nfc', updateSQL, res => {
console.log("更新列表数据")
})
},
},
},
created() {
this.isOpenDB();
uni.onNetworkStatusChange((res) => {
//监听网络
console.log('MIXIN 下监听网络');
// console.log(res.isConnected);
// console.log(res.networkType);
if (res.networkType == 'none') {
console.log('无网络');
// that.seleceByType();
} else if (res.networkType == 'wifi' || res.networkType == '4g') {
console.log('wifi');
//切换到有网络时,需要查看是否有离线数据,并进行提交。
this.selStatusList(); //查询是否有离线数据
this.selLocTaskList();
// that.getNfCList();
}
})
}
}
export {
isLoc
}
引用
import {
isLocData
} from '../../common/mixin.js'
export default {
mixins: [isLocData],
data(){}
}
遇到的坑,
最开始想放在app.vue下的。 但是安装后会白屏。后来改到main.vue下。
这是一个比较简单的使用,后面还有一个有点复杂的使用,离线提交的时候上报多选择数据和 图片,备注等信息。 (离线图片的时候,需要注意进程关掉再开,之前的缓存图片地址就没了,会导致离线上传时图片的丢失。所以建议不要手动关掉进程,或者将图片存下来,成功后再删掉。)
之后因为图片临时缓存的问题,我在本地存了,提交成功后,删除本地存的文件。(离线中mixin.js一样)
imageList ,当前图片显示,因为会多张图上传。
var files = [];
console.log(data.length);
var that = this;
//将临时文件存为本地文件,不受进程关闭的影响
for (var i = 0; i < data.length; i++) {
uni.saveFile({
tempFilePath: data[i],
success: img => {
var savedFilePath = img.savedFilePath;
console.log(savedFilePath);
that.imageList.push(savedFilePath)
console.log("tupian ")
console.log(that.imageList)
// files.push(savedFilePath);
// console.log(files);
// console.log(JSON.stringify(files));
}
})
}
成功后记得删除本地
removeSaveFiles(filePath) {
//移除本地存储文件;
console.log("需要删除的地址")
console.log(filePath)
uni.removeSavedFile({
filePath: filePath,
complete: function(res) {
console.log(res);
}
});
},
状态也有 未处理,处理中,已结束。多个tabs。用法还是一样的。不做说明了。开始以为就一点点的。没想到会写这么多。
希望有帮助,这个sqlite 也是这次的时候使用过的,之前也没有。毕竟是个前端,搞的时候也问了后台一些思路。可能不是很完美,如果有什么好的建议也可以和我说。
补充说明
有小伙伴在指出
文档中 增删改 使用
void plus.sqlite.executeSql(options);
查询 使用
void plus.sqlite.selectSql(options);
最开始没发现,我封装的 plus.sqlite.selectSql(options); 增删改查都可以使用,也没碰到什么问题。
建议大家按照官方的来使用。
先说下我决定用sqlite的条件:
主要是流程处理,需要在无网络的情况下实现,数据量多的时候用h5的缓存完全不够,在看了文档之后选择使用SQLite ,早起在mui的时候使用的indexDB;
因为在社区也没收到具体的,所以写下记录下也和小伙伴分享下,有啥问题可以互相交流下。
该文档中用到的两个点 (SQLite 和vue中的mixin)
我这有两个环境,我先说一个简单;
- 主页菜单进去 到列表界面 从该步骤开始缓存本地数据;
- 从列表点击进入到详情,并在详情操作。有网络正常,无网络时存入表中,当切换到有网情况后,进行提交;
使用SQLite时需要先开权限,在配置文件中,如下图
SQLite官方demo ; 在hbuilderX 下新建demo pages/API/sqlite/sqlite 下官方提供的sqlite使用。为了使用方便,我把需要的单独提出来;
该出贴出function 代码,不做具体说明;
function openComDB(name, path, callback) {
plus.sqlite.openDatabase({
name: name,
path: path,
success: function(e) {
// plus.nativeUI.alert('打开数据库成功');
callback(e)
},
fail: function(e) {
// plus.nativeUI.alert("打开数据库失败");
callback(e);
}
})
}
function executeSQL(name, sql, callback) {
plus.sqlite.selectSql({
name: name,
sql: sql,
success: function(e) {
// console.log("查询数据库:" + name + ",表:" + sql + ";的");
// console.log(JSON.stringify(e));
callback(e);
},
fail: function(e) {
console.log("查询数据库失败:" + JSON.stringify(e));
callback(e);
}
})
}
export{
openComDB,
executeSQL
}
该出 进入正题;
在需要用到的vue文件下,引入上方function; 路径是自己的;
import {openComDB,executeSQL,dropSQL} from '../../common/env.js'
说下简单的思路:
1.进入lists界面,先判断是否有网络,有网络正常调用,无网络时需要判断本地数据库中是否有数据,无数据则第一次进入。有数据需要调用本地数据中的数据;
ps
在有些uni使用上可能会和现在的有点出入,因为这个是早期写的NFC写入的功能,用的nvue的,还是weex的模式下。
贴上部分相关代码
created() {
this.getNetworkType(); //初始化网络当前状态;
uni.onNavigationBarButtonTap((e) => {
//该处是因为的导航栏右边加了两个筛选条件。可以忽略。 不过如果做离线需要筛选的,筛选条件等数据同样需要缓存
if (e.index == 1) {
this.pickType();
} else if (e.index == 0) {
this.pickBuild()
}
})
},
methods: {
getNetworkType() {
//获取网络信息
uni.getNetworkType({
success: res => {
this.netWork = res.networkType;
this.isOpenDB();
}
})
},
isOpenDB() {
console.log('是否打开数据库');
var isOpen = plus.sqlite.isOpenDatabase({
name: 'nfc', //数据库的名字
path: '_doc/nfcList.db' //地址
});
console.log(!isOpen);
if (!isOpen) {
console.log('Unoepned:' + isOpen);
// plus.nativeUI.alert('Unopened!');
this.openDB(); //打開DB
} else {
// plus.nativeUI.alert('Opened!');
this.isNet();
// this.getLocalType();
}
},
openDB() {
//SQLite
openComDB('nfc', '_doc/nfcList.db', res => {
console.log('打开数据库');
this.isNet();
});
},
isNet(){
//网络问题;
if (this.netWork == 'wifi' || this.netWork == '4g') {
//在有网络情况下,会先情况之前的表,为了防止没有及时更新到数据。update我是嫌麻烦,没这样写。就这样暴力写了。
console.log('wifi || 4g ');
this.dropTable("pointLists");
this.dropTable("codeTypeTable");
this.dropTable("codeTable");
this.dropTable("statusTable")
//删了之后创建表
this.createCodeTable();
this.createCodeTypeTable();
this.createBuildTable();
this.createLists();
this.createUpateStatus(); // 离线时更新
//然后把数据插入进去
this.getTabType(); //初始化格式
this.getPonitType(); //获取code。默认值
this.getCodeType(); // 类型下的选择项;
this.getBuilds(); // 获取建筑物数据
} else {
//无网络时;
console.log('初始化无网络');
this.getTabType(); //初始化格式
this.locCodeTypeItem();
this.selBuildFun();
}
//上面贴的drop,create,insert 我会在下面贴出部分代码。不会全部贴.SQL语句不会的建议百度找文档多看下,无非就是增删改查
},
}
整个代码贴上太长了,我还是分段写吧;
本身就是个带tab类型的列表。延用nvue 的 weex的形式,未改成uniapp形式,如需参照此处tab,需要看之前的官方demo。 建议用官方新的uniapp模式,我是懒得改。
//初始化
getTabType() {
//初始化列表;
let ary = [];
for (let i = 0; i < this.tabBars.length; i++) {
let aryItem = {
loadingText: "",
data: [],
pageNum: 1
}
ary.push(aryItem);
}
this.newsitems = ary;
if(this.netWork == "wifi" || this.netWork == "4g"){
this.getPointList() //默认加载未处理; 有网络下,正常调用接口
}else{
this.selPointList() //无网络下调用数据库表中的数据;
}
},
再有网络调用接口时需要将 数据insert到创建的数据表中;
创建数据库中的表
我的思路是整个app为一个数据库,有各种不同的表。 目前只有nfc中用到了,所以数据库的name 就取名 nfc 了,未进行修改;
现在正儿八经的创建表;
用简单的建筑信息为例;根据自己需要的进行创建表;
createBuildTable() {
//创建建筑物类型表;
var sqlTable = 'create table if not exists buildTable("id" INT(10) NOT NULL UNIQUE,"name" CHAR,"gridCode" CHAR)'
executeSQL('nfc', sqlTable, res => {})
},
在有网调用接口时,insert 表;需要与上方创建的完全对应,最后一个不加 “,” 只有不对应就会报错
insertBuildCode() {
for (var i = 0; i < this.builds.length; i++) {
var sqlInsert = "insert into buildTable values('";
sqlInsert += this.builds[i].id + "','";
sqlInsert += this.builds[i].name + "','";
sqlInsert += this.builds[i].gridCode + "'";
sqlInsert += ')';
executeSQL('nfc', sqlInsert, res => {
})
}
this.getLocBuilds() //插入成功后,就可以查看结果。此处用到就直接赋值,没用到,或者别处用到,就在别处调用
},
getLocBuilds() {
executeSQL('nfc', 'select * from buildTable', res => {
console.log("建筑物查询结果");
this.builds = res;
this.selBuildFun()
})
},
查看就这样,就写个简单的例子。列表数据比较多。就不放上。如果详情的数据是通过detailById 接口调用,list表的需要将详情的数据加进去。
当整个数据存上后,就是点击list跳转到detail界面。
detail正常的vue文件。
有网络时正常查看,进行提交操作。
无网络时,查看本地数据库的表。
所有创建和插入都是在list进行的,detail.vue中 进行 查看,和更改,如果需要删除 也可。 (因为在list就很有可能是无网络的情况,所以所有的数据都在列表获取到了)
list ---> detail
可以带id ,也可以带json、建议带唯一的id就可以了,可以在detail select 出需要的数据;
我这里是觉着数据已经在list存上了,detail不如直接使用,不管用网络无网络,本身做提交,后台也不会不停的改数据。
比较严谨的可以有网络时候调用详情接口,无网络时在本地查。
查询代码上面贴了列子此处就不放上了。
简单说一下离线提交。会用到VUE的mixin。
离线提交的思路 。在list的时候需要创建 提交的 表 locSubTable(随便起的名字方便后面提到) ,并在最后 加上 flag 字段 (这个自己随意) 类型INT 。因为没有布尔。所以在插入的时候需要用flag 0,1 进行判断是否需要缓存提交。
0 为 false 1 为 true ;
在detail 内,若 是离线时进行提交,在 locSubTable 进行insert 。最后flag 插入 为 0 ;
在无网络切换至 wifi/4g下,查询 locSubTable 表 where flag = 0 的数据。 返回的 res.length = 0 的时候,无离线提交。
ren.length 有数据需要循环提交。接口提交成功之后需要。update locSubTable 中的 flag = 1 。表示缓存的数据已经被成功提交掉。
还需要更新下list 表的数据,将状态从 未处理 update 为 已处理 (此处更改是因为我的列表是tab类型。流程处理后的状态需要及时更新掉。)
写之前是打算贴代码的,写上之后发现不知道从哪里下手贴,只能说下处理的思路。
后面简单说下mixin的使用。是为了在全局监听网络变化,并能在app下进行离线提交。
在社区找到了uniapp是可以支持的。不过好像用的也不多。(此处该贴上官方链接,找到补上)
因为监听网络切换的时候,界面不会只停留在当前页。但是每个页面都写上也太多了,后来在vue的文档中找到了mixin 混入
《当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项 》
在写的时候也发现的一些坑,会一起记录下。(如果写完发现太多了那我就是被疫情在家憋惨了,憋成了话痨,尴尬)
再common中新建 mixin.js ; mixin的基础使用,可以在vue官方文档查找。
var isLoc ={
data() {
return {} // 有需要的data,也可以加上,我没有用到。
},
methoad:{
//因为需要在其他地方查数据库,所以依旧在最开始打开数据库
isOpenDB() {
console.log("mixin 中是否打开数据库");
var isOpen = plus.sqlite.isOpenDatabase({
name: 'nfc',
path: '_doc/nfcList.db'
});
console.log("数据库是否打开:" + !isOpen);
if (!isOpen) {
console.log('unopen:' + isOpen)
this.openDB()
}
},
openDB() {
openComDB('nfc', '_doc/nfcList.db', res => {
console.log("mixin:打开数据库");
this.getLocUser()
})
},
selStatusList() {
//查询是否有离线写入,未提交数据; flag == 0 false。
// console.log("切换至网络,查询是否有缓存未提交数据");
executeSQL('nfc', 'select * from statusTable where flag = 0', res => {
// console.log(res)
// console.log(res.length);
for (var i = 0; i < res.length; i++) {
this.submitPointFun(res[i].id); //返回的参数根据实际情况来的。
}
})
},
submitPointFun(curId) { // 提交巡检接口
//提交接口
getData({id: curId},data => { //填自己的接口、本身这个我封了下,按照自己的来。
urlFuc("xxxxxxxx", data, res => {
console.log("当前ID:" + curId + "提交成功");
//返回 成功后, 对列表的数据进行update 。
this.updatePointStatus(curId);
this.updatePointList(curId)
});
});
},
updatePointStatus(curId) {
// 切换至有网络后,提交成功后,更新已提交成功数据 更新巡检点状态
//修改flag = 1 - true ;
var updateSQL = 'update statusTable set flag = 1 where id = ' + curId;
executeSQL('nfc', updateSQL, res => {
console.log("更新数据");
})
},
updatePointList(curId) {
// 状态更改后,更改列表改ID的数据
var updateSQL = 'update pointLists set status = "已关联" where id = ' + curId;
console.log(updateSQL);
executeSQL('nfc', updateSQL, res => {
console.log("更新列表数据")
})
},
},
},
created() {
this.isOpenDB();
uni.onNetworkStatusChange((res) => {
//监听网络
console.log('MIXIN 下监听网络');
// console.log(res.isConnected);
// console.log(res.networkType);
if (res.networkType == 'none') {
console.log('无网络');
// that.seleceByType();
} else if (res.networkType == 'wifi' || res.networkType == '4g') {
console.log('wifi');
//切换到有网络时,需要查看是否有离线数据,并进行提交。
this.selStatusList(); //查询是否有离线数据
this.selLocTaskList();
// that.getNfCList();
}
})
}
}
export {
isLoc
}
引用
import {
isLocData
} from '../../common/mixin.js'
export default {
mixins: [isLocData],
data(){}
}
遇到的坑,
最开始想放在app.vue下的。 但是安装后会白屏。后来改到main.vue下。
这是一个比较简单的使用,后面还有一个有点复杂的使用,离线提交的时候上报多选择数据和 图片,备注等信息。 (离线图片的时候,需要注意进程关掉再开,之前的缓存图片地址就没了,会导致离线上传时图片的丢失。所以建议不要手动关掉进程,或者将图片存下来,成功后再删掉。)
之后因为图片临时缓存的问题,我在本地存了,提交成功后,删除本地存的文件。(离线中mixin.js一样)
imageList ,当前图片显示,因为会多张图上传。
var files = [];
console.log(data.length);
var that = this;
//将临时文件存为本地文件,不受进程关闭的影响
for (var i = 0; i < data.length; i++) {
uni.saveFile({
tempFilePath: data[i],
success: img => {
var savedFilePath = img.savedFilePath;
console.log(savedFilePath);
that.imageList.push(savedFilePath)
console.log("tupian ")
console.log(that.imageList)
// files.push(savedFilePath);
// console.log(files);
// console.log(JSON.stringify(files));
}
})
}
成功后记得删除本地
removeSaveFiles(filePath) {
//移除本地存储文件;
console.log("需要删除的地址")
console.log(filePath)
uni.removeSavedFile({
filePath: filePath,
complete: function(res) {
console.log(res);
}
});
},
状态也有 未处理,处理中,已结束。多个tabs。用法还是一样的。不做说明了。开始以为就一点点的。没想到会写这么多。
希望有帮助,这个sqlite 也是这次的时候使用过的,之前也没有。毕竟是个前端,搞的时候也问了后台一些思路。可能不是很完美,如果有什么好的建议也可以和我说。
补充说明
有小伙伴在指出
文档中 增删改 使用
void plus.sqlite.executeSql(options);
查询 使用
void plus.sqlite.selectSql(options);
最开始没发现,我封装的 plus.sqlite.selectSql(options); 增删改查都可以使用,也没碰到什么问题。
建议大家按照官方的来使用。
收起阅读 »