微信小程序即时聊天前后端(TP5+Gateway)

# 效果图

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

原文地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章