# 效果圖
本想把動圖放封面的,考慮到手機的長寬比果斷剪了這張圖。 剪這張圖花了20分鐘,qwq。
開發的時候這個老表幫了我不少忙,幫忙測試找問題,還別說一開始有很多小問題。在這裏感謝這位老表。
目前只支持文字聊天。消息列表頁面仿微信,chat頁面仿QQ,我真是個帶天才哈哈哈。
# 背景
在本文開始前,請確保你對Gateway有一定的瞭解,並且已經配置好,知道怎麼使用。如果這一步有疑問,可以看看這篇帖子TP5整合GatewayWorker。熟悉小程序的websocketApi和頁面間通信也是必要的。
本文將介紹小程序利用websocket與後端(TP5+Gateway)交互實現實時的端到端通訊的一種策略,如果有什麼疑問或建議,請留言給我。
如果文章對你有幫助,麻煩動動小手指點個贊哈哈。
# 環境
- ThinkPHP_v5.1.39
- PHP_v7.2.24
- Gateway-Worker_v3.0.13
- 開發者工具調試基礎庫_v2.8.1
#分析
我們很容易可以看出,效果圖中分兩個頁面—-message_list(消息列表頁)、chat(聊天頁)。所以單從位置來說,用戶有兩個狀態,處於message_list或者chat,我們用一個變量來標識。
# message_list
message_list負責建立連接connect、接收消息onMessage、斷線重連reconnect、心跳heartbeat。
message_list的data(省略了部分非相關變量):
data: {
onMessageListPage: true, //是否在消息列表頁
socketOpen: false, //是否開啓了websocket
current_passive_uid: -1, //當前聊天對象
heartbeat_delay: 50000, //心跳時間
message_page: 1, //當前消息列表頁面,每頁20個列表
message_list: [], //用於渲染的消息列表數據
unreadcount: 0, //總未讀消息數
reconnect_delay: 5000, //重連延遲
eventChannel: null //與chat的通信接口
}
message_list首先加載用戶未讀消息數和最新的未讀消息,拼湊好塞進message_list,渲染到頁面,這個請求走的HTTP。然後,message_list開啓websocket,初始化事件。
openWebSocket() {
let that = this
wx.connectSocket({
url: app.globalData.wss_url,
header: {
'content-type': 'application/json',
'from': 'wechatmini'
}
})
wx.onSocketMessage((msg) => {
let data = JSON.parse(msg.data)
switch (data.errDetail.type) {
case 'new_message':
//接到新消息時,在消息列表頁面,刷新消息列表
if (that.data.onMessageListPage) {
wx.request({
url: app.globalData.url + '/wxgetmessagelist',
method: 'post',
data: {
token: wx.getStorageSync(app.globalData.nameOfCookie),
page: that.data.message_page
},
success: res => {
//console.log(res.data)
for (let i = 0, n = res.data.errDetail.length; i < n; i++) {
res.data.errDetail[i].date = dateTrans(new Date(parseInt(res.data.errDetail[i].timestamp)), new Date())
}
//console.log(res.data.errDetail)
that.setData({
message_list: res.data.errDetail
})
}
})
//刷新小紅點
wx.request({
url: app.globalData.url + '/wxgetunreadcount',
method: 'post',
data: {
token: wx.getStorageSync(app.globalData.nameOfCookie)
},
success: res => {
that.setData({
unreadcount: res.data.errDetail['count']
})
}
})
//接到消息時,在聊天頁面,如果發消息的是正在聊天的對象,則將新消息傳給聊天頁
} else {
const newMsg = data.errDetail.new_message
if (newMsg.active_uid == that.data.current_passive_uid) {
that.data.eventChannel.emit('newMsgChannel', { data: newMsg })
}
}
break;
case 'message_send':
const newMsg = data.errDetail.message_send
that.data.eventChannel.emit('messageSendChannel', { data: newMsg })
break;
}
})
wx.onSocketOpen((msg) => {
that.data.socketOpen = true
sendMsg({
type: 'login',
token: wx.getStorageSync(app.globalData.nameOfCookie)
})
//心跳
that.data.pong = setInterval(() => {
sendMsg({
type: 'pong'
})
}, that.data.heartbeat_delay);
})
wx.onSocketClose((msg) => {
clearInterval(that.data.pong)
//reconnect
that.data.socketOpen = false
let i = setInterval(() => {
wx.connectSocket({
url: app.globalData.wss_url,
header: {
'content-type': 'application/json',
'from': 'wechatmini'
}
})
console.log('reconnect')
if (that.data.socketOpen) clearInterval(i)
}, that.data.reconnect_delay)
})
}
//上文中的sendMsg原型
const sendMsg = function(data) {
wx.sendSocketMessage({
data: JSON.stringify(data)
})
}
代碼中參雜了其他邏輯,這裏總結一下message_list中的websocket事件:
- 當連接成功開啓時,添加心跳,即每隔that.data.heartbeat_delayms,向服務端發送一個數據包{type: ‘pong’},以免連接被幹掉。
- 當連接關閉時,停止心跳,每隔that.data.reconnect_delayms重連一次。
- 當收到服務端的消息包時,根據包內字段type執行不同的操作(需跟服務端約定)。當收到的是新消息時,如果用戶處於chat,並且發送消息的用戶是正在聊天的用戶,那麼將新消息發給chat,否則刷新message_list;當收到的是發送反饋時,將之發給chat。
# chat
chat監聽message_list傳來的消息,並且如果是新消息,那麼發送read給服務端。
chat的data(省略了部分非相關變量):
data: {
a_uid: -1, //用戶uid
p_uid: -1, //對方uid
page: 1,
message: [], //頁面渲染用的數據
}
chat首先按頁加載歷史消息,這裏走的也是HTTP。
/* 用戶發送消息 */
push() {
let that = this
let tosend = {}
tosend.type = 'push'
tosend.token = wx.getStorageSync(app.globalData.nameOfCookie)
tosend.passive_uid = that.data.p_uid
tosend.timestamp = new Date().getTime()
tosend.content = that.data.text
tosend.content_type = 'text'
if (that.data.text && that.data.uid != -1) {
sendMsg(tosend)
}
that.data.value = ''
that.setData({
value: '',
disabled: true
})
}
onLoad: function(options) {
let that = this
that.data.p_uid = options.uid
that.data.a_uid = wx.getStorageSync('uid')
getMessage(that)
wx.setNavigationBarTitle({
title: options.nickname
})
const eventChannel = this.getOpenerEventChannel()
//監聽newMsgChannel
eventChannel.on('newMsgChannel', function(data) {
//新消息加到數組末尾
data.data.isSelf = false
const d = that.data.message
d[d.length] = data.data
that.setData({
message: d
})
let read = {}
read.type = 'read'
read.token = wx.getStorageSync(app.globalData.nameOfCookie),
read.passive_uid = that.data.p_uid
read.mid = data.data.mid
sendMsg(read)
//console.log(read)
setTimeout(() => {
that.pageScrollToBottom()
}, 200)
})
//監聽messageSendChannel
eventChannel.on('messageSendChannel', function(data) {
data.data.isSelf = true
const d = that.data.message
d[d.length] = data.data
that.setData({
message: d
})
setTimeout(() => {
that.pageScrollToBottom()
}, 100)
})
}
//上文中的getMessage原型
const getMessage = function(that) {
wx.request({
url: app.globalData.url + '/wxGetMessage',
method: 'post',
data: {
token: wx.getStorageSync(app.globalData.nameOfCookie),
active_uid: wx.getStorageSync('uid'),
passive_uid: that.data.p_uid,
page: that.data.page
},
success: res => {
if (res.data.errDetail.length != 0) {
let temp = that.data.message
let pastMsg = res.data.errDetail
for (let i = 0, n = pastMsg.length; i < n; i++) {
if (pastMsg[i].active_uid == that.data.a_uid) pastMsg[i].isSelf = true
else pastMsg[i].isSelf = false
temp.unshift(pastMsg[i])
if (pastMsg[n - i - 1].passive_uid == that.data.a_uid &&
pastMsg[n - i - 1].read == 'n') {
//如果拉取的歷史消息中有未讀的,發送read
sendMsg({
type: 'read',
token: wx.getStorageSync(app.globalData.nameOfCookie),
mid: pastMsg[n - i - 1].mid
})
}
}
that.data.page++;
//console.log(temp)
that.setData({
message: temp
})
}
}
})
}
# 服務端
服務端的邏輯比(懶)較(得)簡(寫)單(了),直接上代碼。
<?php
namespace app\api\controller;
use GatewayWorker\Lib\Gateway;
use think\Controller;
use think\Db;
use app\api\model\User as UserModel;
class Events extends Controller
{
/**
* 有消息時 主邏輯處理
* @param integer $client_id 連接的客戶端
* @param mixed $message
* @return void
*/
public static function onMessage($client_id, $message)
{
$message_data = json_decode($message, true);
if (!$message_data) {
return;
}
if (isset($message_data['type']) && !empty($message_data['type'])) {
switch ($message_data['type']) {
// 客戶端迴應服務端的心跳
case 'pong':
break;
case 'login':
$user = UserModel::checkToken($message_data['token']);
if ($user) {
//用戶id與當前連接綁定
Gateway::bindUid($client_id, $user->id);
Gateway::sendToClient($client_id, json_encode(
result_format(0, 'login success')
));
} else break;
break;
case 'push':
$ATTR = ['token', 'passive_uid', 'timestamp', 'type', 'content_type'];
if (wsParamValicate($ATTR, $message_data)) {
$user = UserModel::checkToken($message_data['token']);
if ($user) {
//如果對方在線,則將消息發給他
if (1 === Gateway::isUidOnline($message_data['passive_uid'])) {
$msg = [
'active_uid' => $user->id,
'passive_uid' => $message_data['passive_uid'],
'timestamp' => $message_data['timestamp'],
'content_type' => $message_data['content_type'],
'content' => $message_data['content'],
'read' => 'n'
];
$avatar_url = Db::table('user')->where('id', $msg['active_uid'])
->field(['avatar_url'])
->select();
$id = Db::table('message')->insertGetId($msg);
$msg['mid'] = $id;
$msg['avatar_url'] = $avatar_url[0]['avatar_url'];
Gateway::sendToUid($message_data['passive_uid'], json_encode(
result_format(0, 'new message', ['type' => 'new_message', 'new_message' => $msg])
));
//告訴我發送結果
Gateway::sendToClient($client_id, json_encode(
result_format(0, 'message_send', ['type' => 'message_send', 'message_send' => $msg])
));
} else {
$msg = [
'active_uid' => $user->id,
'passive_uid' => $message_data['passive_uid'],
'timestamp' => $message_data['timestamp'],
'content_type' => $message_data['content_type'],
'content' => $message_data['content'],
'read' => 'n'
];
$avatar_url = Db::table('user')->where('id', $msg['active_uid'])
->field(['avatar_url'])
->select();
$id = Db::table('message')->insertGetId($msg);
$msg['mid'] = $id;
$msg['avatar_url'] = $avatar_url[0]['avatar_url'];
//告訴我發送結果
Gateway::sendToClient($client_id, json_encode(
result_format(0, 'message_send', ['type' => 'message_send', 'message_send' => $msg])
));
}
//更新message_list 雙方都新增消息列表
if (1 == Db::table('messagelist')->where('active_uid', $user->id)->where('passive_uid', $message_data['passive_uid'])->count()) {
} else {
Db::table('messagelist')->insert([
'active_uid' => $user->id,
'passive_uid' => $message_data['passive_uid'],
'enable' => 'y'
]);
Db::table('messagelist')->insert([
'passive_uid' => $user->id,
'active_uid' => $message_data['passive_uid'],
'enable' => 'y'
]);
}
}
}
break;
case 'read':
$ATTR = ['token', 'mid'];
if (wsParamValicate($ATTR, $message_data)) {
$user = UserModel::checkToken($message_data['token']);
if ($user) {
Db::table('message')
->where('passive_uid', $user->id)
->where('id', $message_data['mid'])
->update(['read' => 'y']);
}
}
break;
}
}
}
/**
* 當用戶連接時觸發的方法
* @param integer $client_id 連接的客戶端
* @return void
*/
public static function onConnect($client_id)
{
Gateway::sendToClient($client_id, json_encode(result_format(0, 'connect success', ['client_id' => $client_id])));
}
}