forked from datawhale/whale-town-end
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
13 KiB
13 KiB
NestJS 使用指南
本文档提供 NestJS 在游戏后端开发中的常用模式和最佳实践。
目录
核心概念
NestJS 采用模块化架构,主要由以下几个部分组成:
- Module(模块):组织代码的基本单元
- Controller(控制器):处理 HTTP 请求
- Provider(提供者):包括 Service、Repository 等,处理业务逻辑
- Gateway(网关):处理 WebSocket 连接
模块化开发
创建游戏模块
使用 NestJS CLI 快速生成模块:
nest g module game
nest g controller game
nest g service game
模块示例
// src/game/game_module.ts
import { Module } from '@nestjs/common';
import { GameController } from './game_controller';
import { GameService } from './game_service';
@Module({
controllers: [GameController],
providers: [GameService],
exports: [GameService], // 导出供其他模块使用
})
export class GameModule {}
在根模块中导入:
// src/app_module.ts
import { Module } from '@nestjs/common';
import { GameModule } from './game/game_module';
@Module({
imports: [GameModule],
})
export class AppModule {}
控制器与路由
控制器负责处理 HTTP 请求,定义 RESTful API。
基础控制器示例
// src/api/player_controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { PlayerService } from '../service/player_service';
import { CreatePlayerDto } from '../model/dto/create_player_dto';
@Controller('api/players')
export class PlayerController {
constructor(private readonly playerService: PlayerService) {}
// GET /api/players
@Get()
findAll() {
return this.playerService.findAll();
}
// GET /api/players/:id
@Get(':id')
findOne(@Param('id') id: string) {
return this.playerService.findOne(id);
}
// POST /api/players
@Post()
create(@Body() createPlayerDto: CreatePlayerDto) {
return this.playerService.create(createPlayerDto);
}
}
常用装饰器
@Get(),@Post(),@Put(),@Delete()- HTTP 方法@Param()- 路由参数@Body()- 请求体@Query()- 查询参数@Headers()- 请求头
服务与依赖注入
服务层包含业务逻辑,通过依赖注入使用。
服务示例
// src/service/player_service.ts
import { Injectable } from '@nestjs/common';
import { CreatePlayerDto } from '../model/dto/create_player_dto';
import { Player } from '../model/player_entity';
@Injectable()
export class PlayerService {
private players: Player[] = [];
findAll(): Player[] {
return this.players;
}
findOne(id: string): Player {
return this.players.find(player => player.id === id);
}
create(createPlayerDto: CreatePlayerDto): Player {
const player: Player = {
id: Date.now().toString(),
...createPlayerDto,
createdAt: new Date(),
};
this.players.push(player);
return player;
}
updatePosition(id: string, x: number, y: number): Player {
const player = this.findOne(id);
if (player) {
player.x = x;
player.y = y;
}
return player;
}
}
WebSocket 实时通信
游戏需要实时通信,使用 WebSocket Gateway。
安装依赖
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
Gateway 示例
// src/api/game_gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
// 玩家连接
handleConnection(client: Socket) {
console.log(`Player connected: ${client.id}`);
}
// 玩家断开
handleDisconnect(client: Socket) {
console.log(`Player disconnected: ${client.id}`);
}
// 监听玩家移动事件
@SubscribeMessage('player-move')
handlePlayerMove(client: Socket, payload: { x: number; y: number }) {
// 广播给所有其他玩家
client.broadcast.emit('player-moved', {
playerId: client.id,
x: payload.x,
y: payload.y,
});
return { success: true };
}
// 监听玩家攻击事件
@SubscribeMessage('player-attack')
handlePlayerAttack(client: Socket, payload: { targetId: string }) {
// 发送给特定玩家
this.server.to(payload.targetId).emit('attacked', {
attackerId: client.id,
});
return { success: true };
}
// 服务器主动推送游戏状态
broadcastGameState(gameState: any) {
this.server.emit('game-state', gameState);
}
}
在模块中注册
// src/game/game_module.ts
import { Module } from '@nestjs/common';
import { GameGateway } from '../api/game_gateway';
import { GameService } from '../service/game_service';
@Module({
providers: [GameGateway, GameService],
})
export class GameModule {}
数据验证
使用 DTO(Data Transfer Object)和 class-validator 进行数据验证。
安装依赖
pnpm add class-validator class-transformer
DTO 示例
// src/model/dto/create_player_dto.ts
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
export class CreatePlayerDto {
@IsString()
@IsNotEmpty()
@MinLength(3)
@MaxLength(20)
name: string;
@IsString()
avatar?: string;
}
启用全局验证管道
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 启用全局验证
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // 自动移除非白名单属性
forbidNonWhitelisted: true, // 存在非白名单属性时抛出错误
transform: true, // 自动转换类型
}));
await app.listen(3000);
}
bootstrap();
异常处理
使用内置异常
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class PlayerService {
findOne(id: string): Player {
const player = this.players.find(p => p.id === id);
if (!player) {
throw new NotFoundException(`Player with ID ${id} not found`);
}
return player;
}
}
常用异常类型
BadRequestException- 400UnauthorizedException- 401NotFoundException- 404ForbiddenException- 403InternalServerErrorException- 500
自定义异常过滤器
// src/utils/http_exception_filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
message: exception.message,
});
}
}
实战案例:房间系统
数据模型
// src/model/room_entity.ts
export interface Room {
id: string;
name: string;
maxPlayers: number;
players: string[];
status: 'waiting' | 'playing' | 'finished';
createdAt: Date;
}
服务层
// src/service/room_service.ts
import { Injectable, BadRequestException } from '@nestjs/common';
import { Room } from '../model/room_entity';
@Injectable()
export class RoomService {
private rooms: Map<string, Room> = new Map();
createRoom(name: string, maxPlayers: number): Room {
const room: Room = {
id: Date.now().toString(),
name,
maxPlayers,
players: [],
status: 'waiting',
createdAt: new Date(),
};
this.rooms.set(room.id, room);
return room;
}
joinRoom(roomId: string, playerId: string): Room {
const room = this.rooms.get(roomId);
if (!room) {
throw new BadRequestException('Room not found');
}
if (room.players.length >= room.maxPlayers) {
throw new BadRequestException('Room is full');
}
if (room.status !== 'waiting') {
throw new BadRequestException('Game already started');
}
room.players.push(playerId);
return room;
}
leaveRoom(roomId: string, playerId: string): void {
const room = this.rooms.get(roomId);
if (room) {
room.players = room.players.filter(id => id !== playerId);
if (room.players.length === 0) {
this.rooms.delete(roomId);
}
}
}
listRooms(): Room[] {
return Array.from(this.rooms.values());
}
}
控制器
// src/api/room_controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { RoomService } from '../service/room_service';
@Controller('api/rooms')
export class RoomController {
constructor(private readonly roomService: RoomService) {}
@Get()
listRooms() {
return this.roomService.listRooms();
}
@Post()
createRoom(@Body() body: { name: string; maxPlayers: number }) {
return this.roomService.createRoom(body.name, body.maxPlayers);
}
@Post(':id/join')
joinRoom(@Param('id') id: string, @Body() body: { playerId: string }) {
return this.roomService.joinRoom(id, body.playerId);
}
}
最佳实践
- 模块化设计:按功能划分模块(player、room、game 等)
- 分层架构:Controller → Service → Data,职责清晰
- 使用 DTO:定义清晰的数据传输对象,启用验证
- 依赖注入:充分利用 NestJS 的 DI 系统
- 异常处理:使用内置异常类,提供友好的错误信息
- 配置管理:使用 @nestjs/config 管理环境变量
- 日志记录:使用内置 Logger 或集成第三方日志库
- 测试:编写单元测试和 E2E 测试
注释规范
文件头注释
每个 TypeScript 文件都应该包含完整的文件头注释:
/**
* 文件功能描述
*
* 功能描述:
* - 主要功能点1
* - 主要功能点2
* - 主要功能点3
*
* 职责分离:
* - 职责描述1
* - 职责描述2
*
* 最近修改:
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
* - YYYY-MM-DD: 修改类型 - 具体修改内容描述
*
* @author 作者名
* @version x.x.x
* @since 创建日期
* @lastModified 最后修改日期
*/
类注释
/**
* 类功能描述
*
* 职责:
* - 主要职责1
* - 主要职责2
*
* 主要方法:
* - method1() - 方法1功能
* - method2() - 方法2功能
*
* 使用场景:
* - 场景描述
*/
@Injectable()
export class ExampleService {
// 类实现
}
方法注释
/**
* 方法功能描述
*
* 业务逻辑:
* 1. 步骤1描述
* 2. 步骤2描述
* 3. 步骤3描述
*
* @param param1 参数1描述
* @param param2 参数2描述
* @returns 返回值描述
* @throws ExceptionType 异常情况描述
*
* @example
* ```typescript
* const result = await service.methodName(param1, param2);
* ```
*/
async methodName(param1: string, param2: number): Promise<ResultType> {
// 方法实现
}
接口注释
/**
* 接口功能描述
*/
export interface ExampleInterface {
/** 字段1描述 */
field1: string;
/** 字段2描述 */
field2: number;
/** 可选字段描述 */
optionalField?: boolean;
}
修改记录规范
当修改现有文件时,必须在文件头注释中添加修改记录:
修改类型定义
- 代码规范优化 - 命名规范、注释规范、代码清理等
- 功能新增 - 添加新的功能或方法
- 功能修改 - 修改现有功能的实现
- Bug修复 - 修复代码缺陷
- 性能优化 - 提升代码性能
- 重构 - 代码结构调整但功能不变
修改记录格式
/**
* 最近修改:
* - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto)
* - 2025-01-07: 代码规范优化 - 修复常量命名(saltRounds -> SALT_ROUNDS)
* - 2025-01-07: 功能新增 - 添加用户验证码登录功能
* - 2025-01-06: Bug修复 - 修复邮箱验证逻辑错误
*
* @version 1.0.1 (修改后需要递增版本号)
* @lastModified 2025-01-07
*/
📏 修改记录长度限制:只保留最近5次修改,超出时删除最旧记录,保持注释简洁。
注释最佳实践
- 保持更新:修改代码时同步更新注释
- 描述意图:注释应该说明"为什么"而不只是"做什么"
- 业务逻辑:复杂的业务逻辑必须有详细的步骤说明
- 异常处理:明确说明可能抛出的异常和处理方式
- 示例代码:复杂方法提供使用示例
- 版本管理:修改文件时必须更新修改记录和版本号