hi - blog

使用 express + socket.io + vue3 实现一对一聊天功能(支持离线缓存)

使用 express + socket.io + vue3 实现一对一聊天功能(支持离线缓存)

前言

总想做点什么,但又没什么可做,就想的从写一下自己博客的后台,然后就走上了一条不归路,看到有个设计稿很不错,就先照着样子做了框架,又见聊天功能心血来潮,说实现一下此功能,这里主要为简单搭建,后期还会在做修改。

什么是 Socket.io

Socket.io 是一个十分流行的 JavaScript 库。它允许我们在浏览器和 Node.js 服务端之间创建一个实时的,双向的通信。它是一个高性能并且可靠的库,经过优化可以以最小的延迟处理大量数据。它遵循 WebSocket 协议并提供更好的功能,比如降级为 HTTP 长链接或者自动重连,这些功能可以协助我们构建一个高效的实时的应用。

socket.io文档

创建 node 应用

创建目录安装所需依赖 (express nodemon cors socket.io)

mkdir socket_node 
cd socket_node && npm init --yes
npm i express nodemon cors socket.io

修改 package.json 中的 scripts

...
    "scripts": {
        "dev": "nodemon index.js"
    },
...

创建 index.js 作为 web 服务器的入口文件

touch index.js

使用 Express 创建一个简单的 Node.js 服务。当我们在浏览器访问 http://localhost:5555/ 的时候,下面的代码会返回一段文字

const express = require('express')
const http = require('http')
const cors = require('cors')

// 实例化 express
const app = express()
const PORT = 5555

// 创建一个 http 服务器
const server = http.createServer(app)

// 跨域处理
app.use(cors())

app.get('/', (req, res) => {
    res.send('work...')
})

// 监听端口号
server.listen(PORT, () => {
    console.log(`服务器正在${PORT}端口运行`)
})

引入 socket.io 到项目中并且创建一个实时连接

...
// 初始化连接用户
let connectedUsers = {}

const io = require('socket.io')(server, {
    // 设定跨域
    cors: {
        // 地址配置,允许那些网址
        origin: '*',
        // 定义可请求的方法
        methods: ['GET', 'POST'],
    },
})
io.on('connection', (socket) => {
    console.log(`socket 客户端已连接${socket.id}`)
    // 用户刷新或关闭触发
    socket.on('disconnect', () => {
        console.log(`socket 客户端已断开`)
        // TODO...
    )
})
...

从上面的代码中看到,每当用户访问页面的时候,io.on("connection") 建立了连接,然后为每个 socket 创建一个唯一的 ID ,然后将 ID 打印到控制台

当你刷新或者关闭页面,socket 会触发 disconnect 事件,显示一个用户已从 socket 断开链接。

客户端链接

// utils/wss.js
import io from 'socket.io-client'
const SERVER = //localhost:5555
let socket = null
export const connectWithScoketIOServer = () => {
    socket = io(SERVER)

    socket.on('connect', () => {
        console.log('socket 成功链接')
        console.log(socket.id)
    })
}

将代码在 main.js 中引入

/// main.js
...
import { connectWithScoketIOServer } from './utils/wss'
connectWithScoketIOServer()
...

以上代码,链接成功控制台打印 socket 成功链接socket.id

关于 socket 的 .emit .to .on

  • emit: 发送出去的事件
  • to: 给谁发送
  • on: 用于监听事件和 emit 出来的事件

初始化用户

客户端

/// utils/wss.js
...
export const createChat = (data) => {
    socket.emit('conn-init', data)
}
...

将以上代码引入到需要聊天的界面

/// /views/main/chat/chat-page.vue

import { useRoute } from 'vue-router'
import {
    createChat,
} from './../../../utils/wss'

const route = useRoute()
createChat({
    sender: route.query.sender, // 当前用户 ID
    receiver: route.query.receiver // 发送给用户 ID
})

node服务端

/// index.js
io.on('connection', (socket) => {
    // 用户初始化
    socket.on('conn-init', (data) => {
        connectedUsers = {
            ...connectedUsers,
            [data.sender]: socket.id,
        }
    )
})

从上面代码中可以看到,我在 conn-init 时,将当前用户和 socketId 做一个配对,保存起来,这么做的原因是,我给某位用户发送信息时方便操作

发送消息

客户端

/// utils/wss.js
...
import { ref } from 'vue'
export const connectWithScoketIOServer = () => {
    ...
    // 接收服务端发来的消息
    socket.on('direct-message', (data: any) => {
        directMessageHandler(data)
    })
    ...
}

export const sendMessage = (identity) => {
    // identity 
    //  { 
    //    messag, // 需要发送的消息
    //    userId, // 发送给谁
    //    id, // 当前用户
    //    type // 收发者类型
    //  }
    const data = {
        ...identity,
        socketId: socket.id
    }
    socket.emit('message', data)
}

export const messageContent = ref([
    {
        message: 'Hey Hi Elena Damyanti…!',
        type: 'receiver',
        userId: '2',
        id: '1'
    },
    {
        message:
                `HThanks, all things went well. 
                Just a little boaring at home.
                desktop publishing software like Aldus PageMaker 
                including versions of Lorem Ipsum.`,
        type: 'sender',
        userId: '1',
        id: '2'
    }
])

const directMessageHandler = (data) => {
    // 将当前接受过来的值存到 messageContent 当中
    messageContent.value.push(data)
}

...
/// /views/main/chat/chat-page.vue

import { useRoute } from 'vue-router'
import {
    createChat,
    sendMessage
} from './../../../utils/wss'

const route = useRoute()
createChat({
    sender: route.query.sender, // 当前用户 ID
    receiver: route.query.receiver // 发送给用户 ID
})

// 发送消息事件
const handleClickSendMessage = async () => {
    await sendMessage({
        message: message.value,
        userId: route.query.receiver,
        id: route.query.sender,
        type: 'sender'
    })
    message.value = ''
}

node服务端

/// index.js
io.on('connection', (socket) => {
    ...
    // 接收信息
    socket.on('message', (data) => {
        directMessageHandler(data, socket)
    })
    ...
})

const TYPE_STATUS = {
    receiver: 'sender',
    sender: 'receiver',
}

const directMessageHandler = async (data, socket) => {
    const identity = {
        ...data,
        type: TYPE_STATUS[data.type], // 需要做一个去反,因为发送过来时,
        // 此 type 为 sender 所以发送个某个用户时,做一个去反 
        id: data.userId,
        userId: data.id,
    }
    
    socket.to(connectedUsers[data.userId]).emit('direct-message', { ...identity })
    
    socket.emit('direct-message', { ...data })
}

以上代码可以做到接收信息了,现在一对一聊天的功能就已经做完了,但是有一个弊端,如果对方此时不在线,就做不到接收消息了,所以需要做一个离线缓存的功能,这里我的想法是做一个消息暂存区,如果当前用户 触发了 conn-int 事件时,我去取出离线缓存到的记录,做一个派发,接下来,我们看代码

/// index.js
io.on('connection', (socket) => {
    ...
    // 初始化用户
    socket.on('conn-init', (data) => {
        ...
        // 进行一个判断,是否有离线缓存的记录
        messageStoreHandler({}, socket, data.sender)
    })
    
    ...
})

const TYPE_STATUS = {
    receiver: 'sender',
    sender: 'receiver',
}

const directMessageHandler = async (data, socket) => {
    // 发送消息时,走中间件,判断对方是否为离线状态
    await messageStoreHandler(data, socket, data.sender)
    const identity = {
        ...data,
        type: TYPE_STATUS[data.type], // 需要做一个去反,因为发送过来时,
        // 此 type 为 sender 所以发送个某个用户时,做一个去反 
        id: data.userId,
        userId: data.id,
    }
    
    socket.to(connectedUsers[data.userId]).emit('direct-message', { ...identity })
    
    socket.emit('direct-message', { ...data })
}

const userMessageStore = {}

const messageStoreHandler = async (data, socket, userId) => {
    // 判断当前 connectUsers 中是否有该用户的信息,如果没有进行缓存
    if (!connectedUsers[data.userId] && data.userId !== undefined) {
        // 如果 data.userId 不为空,说明是发送消息时走的中间件,所以进行缓存
        if (data.userId) {
            let messageStore = []
            if (userMessageStore[data.userId]) {
                    messageStore = [...userMessageStore[data.userId]]
            }
            userMessageStore[data.userId] = [...messageStore, data]
        }
        return false
    }
    
    // 判断 userId 是否为 null 为 null 说明时发送消息时触发的该方法 如果有,则是当前用户登陆了
    // 判断 userMessageStore 是否有该用户的缓存记录,如果没有,return false,如果有则反之
    if (userId && userMessageStore[userId]?.length) {
        // 做一个缓存循环,发送当前信息
        for (let i = 0; i < userMessageStore[userId].length; i++) {
            const newData = userMessageStore[userId][i]
         
            const identity = {
                ...newData,
                type: TYPE_STTAUS[newData.type],
                id: newData.userId,
                userId: newData.id,
            }
            
            // 这里用 io 而不是 socket 的问题是因为不能自己给自己发送消息
            await io
                    .to(connectedUsers[userId])
                    .emit('direct-message', { ...identity })
        }
        // 当信息发完,自动删除当前用户的缓存记录
        delete userMessageStore[userId]
        return false
    }
}

以上代码是我的一些想法,可能后期还会有更优的选择,慢慢来,如果大家有更好的方法,请邮箱留言给我 w920098695@sina.cn,谢谢

总结

Socket.io 是一个非常棒的工具,具有出色的功能,使我们能够构建高效的实时应用程序,例如体育网站、拍卖和外汇交易应用程序,当然还有通过在 Web 浏览器和 Node.js 服务器之间创建持久连接的聊天应用程序

如果你期待在 Node.js 中构建聊天应用程序,Socket.io 可能是一个很好的选择

  • 原文代码

忽略目前的界面还未完善如果喜欢可以去 GitHub 自行拉去

https://github.com/nishuiwei/admin-for-blog - 前端代码

前端测试 demo

启动项目之后

https://github.com/nishuiwei/node_for_admin - 后端代码

Current profile photo
© 2022 hi - blog
京ICP备2022015573号-1