微信小程序即時聊天前後端(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])));
    }
}

原文地址

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