写完注册、登录、添加好友、群聊列表等前后端代码后,我们继续来开发聊天的功能。
创建一个 websocket 模块:
css复制代码nest g resource chat --no-spec
安装 websocket 的包:
css复制代码npm i --save @nestjs/websockets @nestjs/platform-socket.io socket.io
改下 ChatGateway:
javascript复制代码import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { ChatService } from './chat.service';
import { Server, Socket } from 'socket.io';
interface JoinRoomPayload {
chatroomId: number
userId: number
}
interface SendMessagePayload {
sendUserId: number;
chatroomId: number;
message: {
type: 'text' | 'image',
content: string
}
}
@WebSocketGateway({cors: { origin: '*' }})
export class ChatGateway {
constructor(private readonly chatService: ChatService) {}
@WebSocketServer() server: Server;
@SubscribeMessage('joinRoom')
joinRoom(client: Socket, payload: JoinRoomPayload): void {
const roomName = payload.chatroomId.toString();
client.join(roomName)
this.server.to(roomName).emit('message', {
type: 'joinRoom',
userId: payload.userId
});
}
@SubscribeMessage('sendMessage')
sendMessage(@MessageBody() payload: SendMessagePayload): void {
const roomName = payload.chatroomId.toString();
this.server.to(roomName).emit('message', {
type: 'sendMessage',
userId: payload.sendUserId,
message: payload.message
});
}
}
监听 joinRoom、sendMessage 消息。
joinRoom 把 client socket 加入房间,房间号为直接用聊天室 id。
sendMessage 接收并广播消息到对应房间。
message 的格式为 type、content,type 可以是 text、image,也就是可以发送文字、图片。
注意,这里要开启 cors 跨域,websocket 也是有跨域问题的。
在前端项目里引入下 socket.io
lua复制代码npm install socket.io-client
然后改下 src/pages/Chat/index.tsx
javascript复制代码import { Input } from "antd";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
interface JoinRoomPayload {
chatroomId: number
userId: number
}
interface SendMessagePayload {
sendUserId: number;
chatroomId: number;
message: Message
}
interface Message {
type: 'text' | 'image'
content: string
}
type Reply = {
type: 'sendMessage'
userId: number
message: Message
} | {
type: 'joinRoom'
userId: number
}
export function Chat() {
const [messageList, setMessageList] = useState<Array<Message>>([]);
const socketRef = useRef<Socket>();
useEffect(() => {
const socket = socketRef.current = io('http://localhost:3005');
socket.on('connect', function() {
const payload: JoinRoomPayload = {
chatroomId: 1,
userId: 1
}
socket.emit('joinRoom', payload);
socket.on('message', (reply: Reply) => {
if(reply.type === 'joinRoom') {
setMessageList(messageList => [...messageList, {
type: 'text',
content: '用户 ' + reply.userId + '加入聊天室'
}])
} else {
setMessageList(messageList => [...messageList, reply.message])
}
});
});
}, []);
function sendMessage(value: string) {
const payload2: SendMessagePayload = {
sendUserId: 1,
chatroomId: 1,
message: {
type: 'text',
content: value
}
}
socketRef.current?.emit('sendMessage', payload2);
}
return <div>
<Input onBlur={(e) => {
sendMessage(e.target.value);
}}/>
<div>
{messageList.map(item => {
return <div>
{item.type === 'image' ? <img src={item.content}/> : item.content }
</div>
})}
</div>
</div>
}
连接服务端的 ws 服务,发送 joinRoom 消息。
然后监听服务端的 message。
如果传过来的是 joinRoom 的消息,就添加一条 用户 xxx 加入聊天室的消息到 messageList。
否则就把传过来 message 加到 messageList。
创建一个 Input,当 blur 的时候发送消息到服务端。
测试下:
现在两个 socket 都在 chatroomId 为 1 的房间里,可以相互发消息。
而用户所在的聊天室会有不同的 chatroomId,不同登录用户会有不同的 userId
把 chatroomId 和 userId 换成真实的,不就能在不同房间聊天了么?
这部分是前端逻辑,我们下节再写。
接下来我们实现下聊天记录的保存,也就是把聊天室里的消息存到数据库表里。
在 prisma 的 schema 添加这个 model
kotlin复制代码model ChatHistory {
id Int @id @default(autoincrement())
content String @db.VarChar(500)
//聊天记录类型 text:0、image:1、file:2
type Int
chatroomId Int
senderId Int
createTime DateTime @default(now())
updateTime DateTime @updatedAt
}
执行 migrate dev 生成迁移 sql 并更新 client 代码:
css复制代码npx prisma migrate dev --name chat-history
没啥问题。
然后创建 chat-history 模块:
css复制代码nest g resource chat-history --no-spec
先改下 ChatHistoryService,实现 list、add 方法:
javascript复制代码import { Inject, Injectable } from '@nestjs/common';
import { ChatHistory } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
export type HistoryDto = Pick<ChatHistory, 'chatroomId' | 'senderId' | 'type' | 'content'>;
@Injectable()
export class ChatHistoryService {
@Inject(PrismaService)
private prismaService: PrismaService;
async list(chatroomId: number) {
return this.prismaService.chatHistory.findMany({
where: {
chatroomId
}
});
}
async add(chatroomId: number, history: HistoryDto) {
return this.prismaService.chatHistory.create({
data: history
});
}
}
我们把 ChatHistoryService 暴露出去,让别的模块可以调用:
在 ChatModule 引入下:
发消息的时候就可以保存聊天记录了:
javascript复制代码@Inject(ChatHistoryService)
private chatHistoryService: ChatHistoryService
@SubscribeMessage('sendMessage')
async sendMessage(@MessageBody() payload: SendMessagePayload) {
const roomName = payload.chatroomId.toString();
await this.chatHistoryService.add(payload.chatroomId, {
content: payload.message.content,
type: payload.message.type === 'image' ? 1 : 0,
chatroomId: payload.chatroomId,
senderId: payload.sendUserId
});
this.server.to(roomName).emit('message', {
type: 'sendMessage',
userId: payload.sendUserId,
message: payload.message
});
}
再聊会天:
这时候聊天内容就保存到了数据库里:
我们还要加一个查询聊天记录的接口:
javascript复制代码import { Controller, Get, Query } from '@nestjs/common';
import { ChatHistoryService } from './chat-history.service';
@Controller('chat-history')
export class ChatHistoryController {
constructor(private readonly chatHistoryService: ChatHistoryService) {}
@Get('list')
async list(@Query('chatroomId') chatroomId: string) {
return this.chatHistoryService.list(+chatroomId);
}
}
postman 里调用下:
我们顺带把 user 信息查出来返回:
javascript复制代码async list(chatroomId: number) {
const history = await this.prismaService.chatHistory.findMany({
where: {
chatroomId
}
});
const res = [];
for(let i = 0; i < history.length; i++) {
const user = await this.prismaService.user.findUnique({
where: {
id: history[i].senderId
},
select: {
id: true,
username: true,
nickName: true,
email: true,
createTime: true,
headPic: true
}
});
res.push({
...history[i],
sender: user
});
}
return res;
}
测试下:
这样,点击切换不同聊天室的时候,就可以把聊天历史记录查出来展示了:
总结
这节我们基于 socket.io 实现了 websocket 服务的前后端。
发送 joinRoom 消息的时候把 client socket 加入房间,房间名为 chatroomId
发送 sendMessage 消息的时候把 message 发送给房间的所有用户。
前端通过 socket.io-client 来实现。
我们还做了聊天记录的保存,每个房间聊天的时候都会把聊天内容存到数据库里。
这样,聊天功能的后端部分就完成了。