前言
本文基于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
核心代码实现
- socket.io包 :借助socket.io实现通信
- 页面结构
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框架开发网页版聊天室的过程,包括群聊和私聊模块的开发与测试环节。