服务端
服务端使用基于PHP的Workerman 里面的GatewayWorker,虚拟主机好像不能使用。
GatewayWorker的好处是可以客户端ID、用户ID、用户组为单位处理业务,而且有SESSION缓存功能,还可以在普通页面中调用,不同端口间通信。
官方有集成的数据库连接类,和TP差不多,可以连等、事务、查询等。
客户端
使用VUE编写(对于数据处理,JQ真心不行),因为是按移动端填写的,所以用的是有赞的Vant,这个组件其实是商城的组件。
刚开始想的是写成一个页面,然后不同组件调用。但后来发现不方便,所以引入了Vuex和Vue-routes。主要分成用户列表、用户私聊、用户群聊(暂时只实现大群聊天)、用户页面。因为网站有用户注册登录页面,所以用户页面纯是好看。
开发经过
工具
- 服务端
-- 基本是GatewayWorker,自己写的代码不到100行(包含注释)。 - 客户端
-- Vue Vant Vuex Vue-routes我是直接用的CDN,虽然开发我是用的NODE,但页面中我直接引入CDN,这样打包的JS文件小到想不到合计不到30K。
-- 自己写的UI实在是不规范。不过个人认为比很多所谓专业的都写的好,至少功能实现了。
-- 页面的东西不多,加载也算快。
响应原理
因为客户端运行过程中,WebSocket运行中能响应的只有function onMessage(message),所以一切业务逻辑都在这里面。
这个方法里是把所有响应封装成类。而传入中把类名和方法名传处,再到类里面自动调用。
客户端发送
let tpl={
module:'ChatService',//使用的类名
action:'getUserInfo',//使用的方法
data:{
uid:uid,
client_id:client_id
}
};
this.ws.send(JSON.stringify(tpl));
服务端接收
传入的是发送客户端的ID,数据,和数据库实例
/**
* 当客户端发来消息时触发
* @param int $client_id 连接id
* @param mixed $message 具体消息
*/
public static function onMessage($client_id, $message)
{
$data=json_decode($message,true);
if(!empty($data['module']) && !empty($data['action'])){
$class='\\Workerman\\Service\\'.$data['module'];
// 只传入数组中的data
$class::{$data['action']}($client_id,$data['data'],self::$db,self::$redis);
}
}
客户接收消息
ws.onmessage= (e)=> {
let obj,fn,raw;
//这里很重要,服务端会发送一些异常数据,一定要过滤
if(typeof e.data =='string' && e.data.substring(0,1)=='{' ){
//这里过滤一下PHP转码时的一些空格
raw = e.data.replace(/\\([^u])/g, '$1');
//console.log(e.data);
obj=JSON.parse(raw);
fn='receive'+obj.type;//取得函数名
this[fn](obj);
}
}
用户确定
首先是在用最外面定义window.userInfo,包括 用户ID、用户昵称、用户名、用户头像、和session_id。如果是没有登录的用户,用户昵称会随机生成。头像为默认。因为每个用户在GatewayWorker中会对应一个UID,对于有用户ID的就用ID,没有用户ID的用session_id。这样注册用户可以在不同设备登录。但最大只能登录3台。
在连接一建立,时就发送用户信息,服务端响应,并绑定用户的$_SESSION信息,并分配UID。这里如果从安全来讲,还要有一个登录令牌数据库较验的。
在客户端发送数据后,服务端接收到数据会向所有用户发送用户上线消息。所以客户端还有一个收到某个用户上线的消息。
客户端用户上线发送
let tpl={
module:'ChatService',
action:'onOpen',
data:{ userInfo:this.userInfo }
};
ws.send(JSON.stringify(tpl));
服务端收到后确定UID,踢掉3个以上客户端,并发布用户上线消息
public static function onOpen($client_id,$data ,$db)
{
$data['state']='Login';
$data['type']='Login';
$uid=!empty($data['userInfo']['id'])?$data['userInfo']['id']:$data['userInfo']['session_id'];
$_SESSION['userInfo']=$data['userInfo'];
$arr=Gateway::getClientIdByUid($uid);
if(count($arr)>2){
Gateway::closeClient($arr[0]);
}
Gateway::bindUid($client_id,$uid);
Gateway::joinGroup($client_id, self::$group);
Gateway::sendToGroup(self::$group,json_encode($data,true));
}
客户端收到上线消息处理
用户上线后,把用户上线的对像加入到用户数组中。再对用户数组处理,得到用户列表。
receiveLogin(obj){
this.$store.commit('userListAdd',obj.userInfo);
},
取得在线用户列表
初始用户登录后,或是登录一段时间后,要刷新用户列表。都采用的是,传入值里定义操作类、操作方法。
私聊
就是服务端将信息发送给指定UID,这里发送私聊是在子页面上,而接收私聊是在App.vue,父子组件间传递信息好实现,但不同页面间传递就不好实现。这里就用到了广播。
将所有私聊消息放到Vuex里,以数组的型式存放。到聊在窗口时,对数组过滤。
对话框中对自己说的话进行了左右浮动设置
判断消息是否是自己发出,然后对消息框进行左右浮动。
新消息提示
开发前觉得很难,开发中不知怎么的就想到了将消息ID放到一个数组,对话页面离开时将数组中对应ID全删除。在用户列表页面统计每个ID有几条信息。
群聊
群聊比较简单,是最基本的聊天室功能,暂时没有设计新建群和小群聊天。这次基本没有用到数据库,主要没用到数据库,后面考虑将聊天记录功能加上。用户添加好友的功能加上。
消息保存
重新安装了redis数据库,将聊天记保存到数 据库中,以两个用户名加前缀为KEY值,保存聊天记录,一对一聊天是保存7天,多对对聊天保存近200条,当进入群聊页面时,会自动加载聊天记录,一对一聊天需要下拉刷新。
用户列表
可以将登录过的用户生成用户列表,功能有待完成
总结
踩坑
- 一直报JSON错误。因为没有看过GatewayWorker源码,可能服务端会推送一些不知名数据过来,所以对接收数据进行过滤。开始还以为是PHP里面数组转字符串时错误造成的。
猜测是WebSocket 会发一些不是字符串的数据过来。先判断是不是字符串,再判断第一个字符是不是“{”,最好是再判断最后一个是不是“}”,当然要根据具体反回值,因为我反回的是有下标的数组。
ws.onmessage= (e)=> {
let obj,fn,raw;
if(typeof e.data =='string' && e.data.substring(0,1)=='{' ){
raw = e.data.replace(/\\([^u])/g, '$1');
//console.log(e.data);
obj=JSON.parse(raw);
fn='receive'+obj.type;
this[fn](obj);
}
}
- 对于vue的计算属性computed。在私聊开发过程中,想的时收到私聊信息后,将信息以‘masssge’:{“用户1”:[ 记录1,记录2,……],“用户2”:[ 记录1,记录2,……]}的型式方到vuex里,再到页面中取 对应用户的值,结果vuex里面更新了,到页面的计算属性中没有更新。计算属性中只是对massage监听,而他里面的“用户”的数组改变并不会影响massage。开发的过程中就有想到这一点,没有百度就找到答案了。
- 新消息自动下滑。还好没有走什么弯路,开发前还想的是引入什么框架或是NPM找个轮子
,用到了scrollIntoView(false);这个方法。当有消息增加时,在updated()里添加一次下滑到底部。
心得
- 没有以前的那种什么都要做到无比优化的完美主义了,先实现再优化,前几天看到群里,一个码农说,他们老板在一个还没有写好的项目中不让用JQ的瀑布流插件,原因是JQ太耗性能了,怕手机上打开慢,在群里问原生瀑布流写法。这个聊天室我是先实现功能,再想着优化流程,要不然进行不下去。
结束语
写到最后了才附上聊天室地址。
手机打开效果好,地址
服务端地址:码云
客户端:码云
这个聊天室稍加改造可以成为客服工具,下一步准备写一个手机摇一摇比赛的项目。