服務(wù)端
服務(wù)端使用基于PHP的Workerman 里面的GatewayWorker,虛擬主機(jī)好像不能使用。
GatewayWorker的好處是可以客戶端ID、用戶ID、用戶組為單位處理業(yè)務(wù),而且有SESSION緩存功能,還可以在普通頁面中調(diào)用,不同端口間通信。
官方有集成的數(shù)據(jù)庫連接類,和TP差不多,可以連等、事務(wù)、查詢等。
客戶端
使用VUE編寫(對于數(shù)據(jù)處理,JQ真心不行),因為是按移動端填寫的,所以用的是有贊的Vant,這個組件其實是商城的組件。
剛開始想的是寫成一個頁面,然后不同組件調(diào)用。但后來發(fā)現(xiàn)不方便,所以引入了Vuex和Vue-routes。主要分成用戶列表、用戶私聊、用戶群聊(暫時只實現(xiàn)大群聊天)、用戶頁面。因為網(wǎng)站有用戶注冊登錄頁面,所以用戶頁面純是好看。
開發(fā)經(jīng)過
工具
- 服務(wù)端
-- 基本是GatewayWorker,自己寫的代碼不到100行(包含注釋)。 - 客戶端
-- Vue Vant Vuex Vue-routes我是直接用的CDN,雖然開發(fā)我是用的NODE,但頁面中我直接引入CDN,這樣打包的JS文件小到想不到合計不到30K。
-- 自己寫的UI實在是不規(guī)范。不過個人認(rèn)為比很多所謂專業(yè)的都寫的好,至少功能實現(xiàn)了。
-- 頁面的東西不多,加載也算快。
響應(yīng)原理
因為客戶端運(yùn)行過程中,WebSocket運(yùn)行中能響應(yīng)的只有function onMessage(message),所以一切業(yè)務(wù)邏輯都在這里面。
這個方法里是把所有響應(yīng)封裝成類。而傳入中把類名和方法名傳處,再到類里面自動調(diào)用。
客戶端發(fā)送
let tpl={
module:'ChatService',//使用的類名
action:'getUserInfo',//使用的方法
data:{
uid:uid,
client_id:client_id
}
};
this.ws.send(JSON.stringify(tpl));
服務(wù)端接收
傳入的是發(fā)送客戶端的ID,數(shù)據(jù),和數(shù)據(jù)庫實例
/**
* 當(dāng)客戶端發(fā)來消息時觸發(fā)
* @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'];
// 只傳入數(shù)組中的data
$class::{$data['action']}($client_id,$data['data'],self::$db,self::$redis);
}
}
客戶接收消息
ws.onmessage= (e)=> {
let obj,fn,raw;
//這里很重要,服務(wù)端會發(fā)送一些異常數(shù)據(jù),一定要過濾
if(typeof e.data =='string' && e.data.substring(0,1)=='{' ){
//這里過濾一下PHP轉(zhuǎn)碼時的一些空格
raw = e.data.replace(/\\([^u])/g, '$1');
//console.log(e.data);
obj=JSON.parse(raw);
fn='receive'+obj.type;//取得函數(shù)名
this[fn](obj);
}
}
用戶確定
首先是在用最外面定義window.userInfo,包括 用戶ID、用戶昵稱、用戶名、用戶頭像、和session_id。如果是沒有登錄的用戶,用戶昵稱會隨機(jī)生成。頭像為默認(rèn)。因為每個用戶在GatewayWorker中會對應(yīng)一個UID,對于有用戶ID的就用ID,沒有用戶ID的用session_id。這樣注冊用戶可以在不同設(shè)備登錄。但最大只能登錄3臺。
在連接一建立,時就發(fā)送用戶信息,服務(wù)端響應(yīng),并綁定用戶的$_SESSION信息,并分配UID。這里如果從安全來講,還要有一個登錄令牌數(shù)據(jù)庫較驗的。
在客戶端發(fā)送數(shù)據(jù)后,服務(wù)端接收到數(shù)據(jù)會向所有用戶發(fā)送用戶上線消息。所以客戶端還有一個收到某個用戶上線的消息。
客戶端用戶上線發(fā)送
let tpl={
module:'ChatService',
action:'onOpen',
data:{ userInfo:this.userInfo }
};
ws.send(JSON.stringify(tpl));
服務(wù)端收到后確定UID,踢掉3個以上客戶端,并發(fā)布用戶上線消息
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));
}
客戶端收到上線消息處理
用戶上線后,把用戶上線的對像加入到用戶數(shù)組中。再對用戶數(shù)組處理,得到用戶列表。
receiveLogin(obj){
this.$store.commit('userListAdd',obj.userInfo);
},
取得在線用戶列表
初始用戶登錄后,或是登錄一段時間后,要刷新用戶列表。都采用的是,傳入值里定義操作類、操作方法。
私聊
就是服務(wù)端將信息發(fā)送給指定UID,這里發(fā)送私聊是在子頁面上,而接收私聊是在App.vue,父子組件間傳遞信息好實現(xiàn),但不同頁面間傳遞就不好實現(xiàn)。這里就用到了廣播。
將所有私聊消息放到Vuex里,以數(shù)組的型式存放。到聊在窗口時,對數(shù)組過濾。
對話框中對自己說的話進(jìn)行了左右浮動設(shè)置
判斷消息是否是自己發(fā)出,然后對消息框進(jìn)行左右浮動。
新消息提示
開發(fā)前覺得很難,開發(fā)中不知怎么的就想到了將消息ID放到一個數(shù)組,對話頁面離開時將數(shù)組中對應(yīng)ID全刪除。在用戶列表頁面統(tǒng)計每個ID有幾條信息。
群聊
群聊比較簡單,是最基本的聊天室功能,暫時沒有設(shè)計新建群和小群聊天。這次基本沒有用到數(shù)據(jù)庫,主要沒用到數(shù)據(jù)庫,后面考慮將聊天記錄功能加上。用戶添加好友的功能加上。
消息保存
重新安裝了redis數(shù)據(jù)庫,將聊天記保存到數(shù) 據(jù)庫中,以兩個用戶名加前綴為KEY值,保存聊天記錄,一對一聊天是保存7天,多對對聊天保存近200條,當(dāng)進(jìn)入群聊頁面時,會自動加載聊天記錄,一對一聊天需要下拉刷新。
用戶列表
可以將登錄過的用戶生成用戶列表,功能有待完成
總結(jié)
踩坑
- 一直報JSON錯誤。因為沒有看過GatewayWorker源碼,可能服務(wù)端會推送一些不知名數(shù)據(jù)過來,所以對接收數(shù)據(jù)進(jìn)行過濾。開始還以為是PHP里面數(shù)組轉(zhuǎn)字符串時錯誤造成的。
猜測是WebSocket 會發(fā)一些不是字符串的數(shù)據(jù)過來。先判斷是不是字符串,再判斷第一個字符是不是“{”,最好是再判斷最后一個是不是“}”,當(dāng)然要根據(jù)具體反回值,因為我反回的是有下標(biāo)的數(shù)組。
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。在私聊開發(fā)過程中,想的時收到私聊信息后,將信息以‘masssge’:{“用戶1”:[ 記錄1,記錄2,……],“用戶2”:[ 記錄1,記錄2,……]}的型式方到vuex里,再到頁面中取 對應(yīng)用戶的值,結(jié)果vuex里面更新了,到頁面的計算屬性中沒有更新。計算屬性中只是對massage監(jiān)聽,而他里面的“用戶”的數(shù)組改變并不會影響massage。開發(fā)的過程中就有想到這一點,沒有百度就找到答案了。
- 新消息自動下滑。還好沒有走什么彎路,開發(fā)前還想的是引入什么框架或是NPM找個輪子
,用到了scrollIntoView(false);這個方法。當(dāng)有消息增加時,在updated()里添加一次下滑到底部。
心得
- 沒有以前的那種什么都要做到無比優(yōu)化的完美主義了,先實現(xiàn)再優(yōu)化,前幾天看到群里,一個碼農(nóng)說,他們老板在一個還沒有寫好的項目中不讓用JQ的瀑布流插件,原因是JQ太耗性能了,怕手機(jī)上打開慢,在群里問原生瀑布流寫法。這個聊天室我是先實現(xiàn)功能,再想著優(yōu)化流程,要不然進(jìn)行不下去。
結(jié)束語
寫到最后了才附上聊天室地址。
手機(jī)打開效果好,地址
服務(wù)端地址:碼云
客戶端:碼云
這個聊天室稍加改造可以成為客服工具,下一步準(zhǔn)備寫一個手機(jī)搖一搖比賽的項目。