# 效果图
本想把动图放封面的,考虑到手机的长宽比果断剪了这张图。 剪这张图花了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])));
}
}