Nest.js实战:构建聊天室的群聊与私聊模块

前言

本文基于Nest.js框架,实现了网页版聊天室功能,其中包含群聊和私聊模块,满足不同场景下的沟通需求。

项目搭建

使用脚手架搭建项目

bash 复制代码
# 安装脚手架
npm install -g @nestjs/cli
# 创建项目
nest new <project-name>//chatApi
# 进入工程目录
cd chatApi
# 安装依赖
npm i
# 启动项目
npm run start:dev//热更新方便开发

项目说明
前端页面 :public
后端接口 :src目录下

前端

说明 :在public创建一个index.html
核心代码实现

javascript 复制代码
<div class="container">
        <h2>聊天室测试 - <span id="username">未连接</span></h2>
        <div class="chat-area">
            <div class="left-panel">
                <h3>在线用户</h3>
                <div class="user-list" id="userList"></div>
                <h3>聊天室</h3>
                <div class="room-list" id="roomList">
                    <button onclick="createRoom()">创建聊天室</button>
                    <div id="rooms"></div>
                </div>
            </div>
            <div class="right-panel">
                <div class="messages" id="messages"></div>
                <div class="controls">
                    <select id="messageType">
                        <option value="group">群聊</option>
                        <option value="private">私聊</option>
                    </select>
                    <select id="recipient" style="display: none;"></select>
                    <input type="text" id="messageInput" placeholder="输入消息...">
                    <button onclick="sendMessage()">发送</button>
                </div>
            </div>
        </div>
    </div>
  • 核心业务逻辑
ini 复制代码
 const socket = io('http://localhost:3000/chat');
        let currentUser = null;
        let currentRoom = null;

        // 切换消息类型时显示/隐藏接收者选择框
        document.getElementById('messageType').addEventListener('change', function(e) {
            document.getElementById('recipient').style.display = 
                e.target.value === 'private' ? 'block' : 'none';
        });

        // 连接成功
        socket.on('connect', () => {
            addMessage('系统', '已连接到服务器');
        });

        // 欢迎消息
        socket.on('welcome', (data) => {
            currentUser = data;
            document.getElementById('username').textContent = data.username;
            addMessage('系统', `欢迎 ${data.username}`);
            
            // 显示现有房间
            if (data.rooms && data.rooms.length > 0) {
                updateRoomList(data.rooms);
            }
        });

        // 房间列表更新
        socket.on('roomList', (rooms) => {
            updateRoomList(rooms);
        });

        // 用户列表更新
        socket.on('userList', (users) => {
            const userList = document.getElementById('userList');
            const recipient = document.getElementById('recipient');
            
            userList.innerHTML = '';
            recipient.innerHTML = '';
            
            users.forEach(user => {
                if (user.userId !== currentUser?.userId) {
                    // 更新用户列表
                    const userDiv = document.createElement('div');
                    userDiv.textContent = `${user.username} (${user.status})`;
                    userList.appendChild(userDiv);

                    // 更新私聊接收者选项
                    const option = document.createElement('option');
                    option.value = user.userId;
                    option.textContent = user.username;
                    recipient.appendChild(option);
                }
            });
        });

        // 私聊消息
        socket.on('privateMessage', (data) => {
            addMessage(data.fromUser.username, data.content, 'private');
        });

        // 群聊消息
        socket.on('groupMessage', (data) => {
            addMessage(data.fromUser.username, data.content, 'group');
        });

        // 加入房间响应
        socket.on('joinedRoom', (data) => {
            currentRoom = data.roomId;
            updateActiveRoom(data.roomId);
            addMessage('系统', `已加入房间 ${data.roomId}`);
        });

        // 错误消息
        socket.on('error', (data) => {
            addMessage('错误', data.message, 'system');
        });

        function addMessage(sender, content, type = 'system') {
            const messages = document.getElementById('messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${type}`;
            messageDiv.textContent = `${sender}: ${content}`;
            messages.appendChild(messageDiv);
            messages.scrollTop = messages.scrollHeight;
        }

        function updateRoomList(rooms) {
            const roomsDiv = document.getElementById('rooms');
            roomsDiv.innerHTML = '';
            
            rooms.forEach(roomId => {
                const roomButton = document.createElement('button');
                roomButton.className = `room-btn ${currentRoom === roomId ? 'active' : ''}`;
                roomButton.textContent = roomId;
                roomButton.onclick = () => joinRoom(roomId);
                roomsDiv.appendChild(roomButton);
            });
        }

        function updateActiveRoom(roomId) {
            const roomButtons = document.querySelectorAll('.room-btn');
            roomButtons.forEach(button => {
                button.classList.toggle('active', button.textContent === roomId);
            });
        }

        function createRoom() {
            const roomId = 'room_' + Math.random().toString(36).substr(2, 9);
            joinRoom(roomId);
        }

        function joinRoom(roomId) {
            socket.emit('joinRoom', { roomId, userId: currentUser.userId });
        }

        function sendMessage() {
            const messageType = document.getElementById('messageType').value;
            const content = document.getElementById('messageInput').value;
            
            if (!content.trim()) return;

            if (messageType === 'private') {
                const recipient = document.getElementById('recipient').value;
                socket.emit('privateMessage', {
                    to: recipient,
                    content: content
                });
            } else {
                if (!currentRoom) {
                    addMessage('系统', '请先加入一个聊天室', 'system');
                    return;
                }
                socket.emit('groupMessage', {
                    roomId: currentRoom,
                    content: content
                });
            }

            document.getElementById('messageInput').value = '';
        }

后端

目录说明
arduino 复制代码
src:.
   ├─chat(说明:单个接口目录名为chat)
   |  |--- chat.gateway.ts//关于网关
   |  |--- chat.interface.ts//关于此接口处理
   |  |--- chat.module.ts//关于此接口模块
   |  |_ chat.service.ts//关于此接口服务      
   ├─ app.controller.ts//控制器
   ├─ app.module.ts//模块
   ├─ app.service.ts//服务
   └─ main.ts//入口文件
下载安装包
bash 复制代码
# 另外要下载的包
uuid 
@types/uuid
socket.io
入口文件(main.ts)
javascript 复制代码
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // 配置静态文件服务
  app.useStaticAssets(join(__dirname, '..', 'public'));//加载编辑的聊天室页面
  
  await app.listen(process.env.PORT ?? 3000);
  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
app.xxx.ts文件(controller,module,service)
  • app.controller.ts
typescript 复制代码
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
  • app.module.ts
python 复制代码
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ChatModule } from './chat/chat.module';

@Module({
  imports: [ChatModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • app.service.ts
kotlin 复制代码
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
Chat核心代码
  • chat.gateway.ts:网关
less 复制代码
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { ChatService } from './chat.service';
import { PrivateMessageDto, GroupMessageDto, JoinRoomDto, UserStatusDto } from './chat.interface';
import { v4 as uuidv4 } from 'uuid';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
  namespace: 'chat',
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  private readonly logger = new Logger(ChatGateway.name);
  private rooms: Set<string> = new Set(); // 存储所有房间ID

  constructor(private readonly chatService: ChatService) {}

  @WebSocketServer()
  server: Server;

  async handleConnection(client: Socket) {
    try {
      const userId = uuidv4();
      const username = `User_${userId.slice(0, 8)}`;
      
      // 添加用户到在线列表
      const user = this.chatService.addUser(userId, username, client.id);
      
      // 将用户ID存储在socket中
      client.data.userId = userId;
      
      // 通知所有用户有新用户加入
      this.server.emit('userList', this.chatService.getAllUsers());
      
      // 发送欢迎消息和现有房间列表
      client.emit('welcome', {
        userId,
        username,
        message: `Welcome ${username}!`,
        rooms: Array.from(this.rooms)
      });

      this.logger.log(`Client connected: ${client.id}, userId: ${userId}`);
    } catch (error) {
      this.logger.error(`Error in handleConnection: ${error.message}`);
    }
  }

  async handleDisconnect(client: Socket) {
    try {
      const userId = client.data.userId;
      if (userId) {
        // 从在线列表中移除用户
        this.chatService.removeUser(userId);
        
        // 通知所有用户有用户离开
        this.server.emit('userList', this.chatService.getAllUsers());
        
        this.logger.log(`Client disconnected: ${client.id}, userId: ${userId}`);
      }
    } catch (error) {
      this.logger.error(`Error in handleDisconnect: ${error.message}`);
    }
  }

  @SubscribeMessage('privateMessage')
  async handlePrivateMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: PrivateMessageDto,
  ) {
    try {
      const fromUserId = client.data.userId;
      const fromUser = this.chatService.getUser(fromUserId);
      const toUser = this.chatService.getUser(data.to);

      if (!toUser || !fromUser) {
        client.emit('error', { message: 'User not found' });
        return;
      }

      const message = this.chatService.createMessage(
        fromUserId,
        data.content,
        'private',
        data.to,
      );

      // 发送给接收者
      this.server.to(toUser.socketId).emit('privateMessage', {
        ...message,
        fromUser: {
          userId: fromUser.userId,
          username: fromUser.username,
        },
      });

      // 发送给发送者
      client.emit('privateMessage', {
        ...message,
        fromUser: {
          userId: fromUser.userId,
          username: fromUser.username,
        },
      });
    } catch (error) {
      this.logger.error(`Error in handlePrivateMessage: ${error.message}`);
      client.emit('error', { message: 'Failed to send private message' });
    }
  }

  @SubscribeMessage('joinRoom')
  async handleJoinRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: JoinRoomDto,
  ) {
    try {
      const userId = client.data.userId;
      
      // 将房间ID添加到房间列表
      this.rooms.add(data.roomId);
      
      // 将用户加入房间
      this.chatService.joinRoom(data.roomId, userId);
      await client.join(data.roomId);

      // 获取房间内的用户列表
      const roomUsers = this.chatService.getRoomUsers(data.roomId);
      
      // 通知所有用户有新房间创建
      this.server.emit('roomList', Array.from(this.rooms));
      
      // 通知房间内所有用户
      this.server.to(data.roomId).emit('roomUsers', {
        roomId: data.roomId,
        users: roomUsers,
      });

      client.emit('joinedRoom', { roomId: data.roomId });
    } catch (error) {
      this.logger.error(`Error in handleJoinRoom: ${error.message}`);
      client.emit('error', { message: 'Failed to join room' });
    }
  }

  @SubscribeMessage('leaveRoom')
  async handleLeaveRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: JoinRoomDto,
  ) {
    try {
      const userId = client.data.userId;
      
      // 将用户从房间移除
      this.chatService.leaveRoom(data.roomId, userId);
      await client.leave(data.roomId);

      // 获取房间内的用户列表
      const roomUsers = this.chatService.getRoomUsers(data.roomId);
      
      // 通知房间内所有用户
      this.server.to(data.roomId).emit('roomUsers', {
        roomId: data.roomId,
        users: roomUsers,
      });

      client.emit('leftRoom', { roomId: data.roomId });
    } catch (error) {
      this.logger.error(`Error in handleLeaveRoom: ${error.message}`);
      client.emit('error', { message: 'Failed to leave room' });
    }
  }

  @SubscribeMessage('groupMessage')
  async handleGroupMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: GroupMessageDto,
  ) {
    try {
      const userId = client.data.userId;
      const user = this.chatService.getUser(userId);

      if (!user) {
        client.emit('error', { message: 'User not found' });
        return;
      }

      const message = this.chatService.createMessage(
        userId,
        data.content,
        'group',
        undefined,
        data.roomId,
      );

      // 发送给房间内所有用户
      this.server.to(data.roomId).emit('groupMessage', {
        ...message,
        fromUser: {
          userId: user.userId,
          username: user.username,
        },
      });
    } catch (error) {
      this.logger.error(`Error in handleGroupMessage: ${error.message}`);
      client.emit('error', { message: 'Failed to send group message' });
    }
  }

  @SubscribeMessage('updateStatus')
  async handleStatusUpdate(
    @ConnectedSocket() client: Socket,
    @MessageBody() data: UserStatusDto,
  ) {
    try {
      const userId = client.data.userId;
      const updatedUser = this.chatService.updateUserStatus(userId, data.status);
      
      if (updatedUser) {
        // 通知所有用户状态更新
        this.server.emit('userList', this.chatService.getAllUsers());
      }
    } catch (error) {
      this.logger.error(`Error in handleStatusUpdate: ${error.message}`);
      client.emit('error', { message: 'Failed to update status' });
    }
  }
} 
  • chat.interface.ts:此接口处理
css 复制代码
export interface User {
  userId: string;
  username: string;
  socketId: string;
  status: 'online' | 'offline' | 'away';
}

export interface ChatMessage {
  id: string;
  from: string;
  to?: string;  // 如果是私聊消息,则包含接收者ID
  content: string;
  timestamp: Date;
  type: 'private' | 'group';
  roomId?: string; // 如果是群聊消息,则包含房间ID
}

export interface JoinRoomDto {
  roomId: string;
  userId: string;
}

export interface PrivateMessageDto {
  to: string;
  content: string;
}

export interface GroupMessageDto {
  roomId: string;
  content: string;
}

export interface UserStatusDto {
  status: 'online' | 'offline' | 'away';
} 
  • chat.module.ts:模块
python 复制代码
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { ChatService } from './chat.service';

@Module({
  providers: [ChatGateway, ChatService],
})
export class ChatModule {} 
  • chat.service.ts:服务
kotlin 复制代码
import { Injectable } from '@nestjs/common';
import { User, ChatMessage } from './chat.interface';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class ChatService {
  private users: Map<string, User> = new Map();
  private rooms: Map<string, Set<string>> = new Map(); // roomId -> Set of userIds

  addUser(userId: string, username: string, socketId: string): User {
    const user: User = {
      userId,
      username,
      socketId,
      status: 'online',
    };
    this.users.set(userId, user);
    return user;
  }

  removeUser(userId: string) {
    this.users.delete(userId);
    // Remove user from all rooms
    this.rooms.forEach((users, roomId) => {
      users.delete(userId);
    });
  }

  getUser(userId: string): User | undefined {
    return this.users.get(userId);
  }

  getUserBySocketId(socketId: string): User | undefined {
    return Array.from(this.users.values()).find(user => user.socketId === socketId);
  }

  getAllUsers(): User[] {
    return Array.from(this.users.values());
  }

  joinRoom(roomId: string, userId: string) {
    if (!this.rooms.has(roomId)) {
      this.rooms.set(roomId, new Set());
    }
    const room = this.rooms.get(roomId);
    if (room) {
      room.add(userId);
    }
  }

  leaveRoom(roomId: string, userId: string) {
    const room = this.rooms.get(roomId);
    if (room) {
      room.delete(userId);
      if (room.size === 0) {
        this.rooms.delete(roomId);
      }
    }
  }

  getRoomUsers(roomId: string): User[] {
    const room = this.rooms.get(roomId);
    if (!room) return [];
    return Array.from(room)
      .map(userId => this.users.get(userId))
      .filter((user): user is User => user !== undefined);
  }

  updateUserStatus(userId: string, status: 'online' | 'offline' | 'away') {
    const user = this.users.get(userId);
    if (user) {
      user.status = status;
      return user;
    }
    return null;
  }

  createMessage(from: string, content: string, type: 'private' | 'group', to?: string, roomId?: string): ChatMessage {
    return {
      id: uuidv4(),
      from,
      to,
      content,
      timestamp: new Date(),
      type,
      roomId
    };
  }
} 

测试

测试流程

bash 复制代码
# 启动项目
npm run start:dev
# 项目链接
http://localhost:3000/

打开浏览器(建议使用不同的浏览器打开项目链接:例如:google firefox Edge)

测试群聊

  • 用户User_10f56323: 创建聊天室并进入聊天室 发送一条信息
  • 用户User_a73d148f: 点击聊天室列表进入用户User_10f56323创建的聊天室 获取聊天信息发送一条信息
  • 用户User_a1032532 :点击聊天室列表进入用户User_10f56323创建的聊天室 获取聊天信息发送一条信息

测试私聊

  • 用户User_10f56323 : 点击私聊 选择要私聊的用户(User_a73d148f) 发送一条信息
  • 用户User_a73d148f :被私信的用户收到一条信息

总结

以上内容完整呈现了基于Nest.js框架开发网页版聊天室的过程,包括群聊和私聊模块的开发与测试环节。

相关推荐
星星电灯猴3 分钟前
iOS 性能调试全流程:从 Demo 到产品化的小团队实战经验
后端
aklry3 分钟前
uniapp三步完成一维码的生成
前端·vue.js
Rubin9310 分钟前
判断元素在可视区域?用于滚动加载,数据埋点等
前端
爱学习的茄子11 分钟前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
用户38022585982411 分钟前
使用three.js实现3D地球
前端·three.js
程序无bug12 分钟前
手写Spring框架
java·后端
程序无bug14 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
zhanshuo14 分钟前
鸿蒙UI开发全解:JS与Java双引擎实战指南
前端·javascript·harmonyos
JohnYan14 分钟前
模板+数据的文档生成技术方案设计和实现
javascript·后端·架构
全干engineer25 分钟前
Spring Boot 实现主表+明细表 Excel 导出(EasyPOI 实战)
java·spring boot·后端·excel·easypoi·excel导出