forked from datawhale/whale-town-end
docs:创建必要的文档内容,丰富readme
This commit is contained in:
460
docs/nestjs_guide.md
Normal file
460
docs/nestjs_guide.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# NestJS 使用指南
|
||||
|
||||
本文档提供 NestJS 在游戏后端开发中的常用模式和最佳实践。
|
||||
|
||||
## 目录
|
||||
|
||||
- [核心概念](#核心概念)
|
||||
- [模块化开发](#模块化开发)
|
||||
- [控制器与路由](#控制器与路由)
|
||||
- [服务与依赖注入](#服务与依赖注入)
|
||||
- [WebSocket 实时通信](#websocket-实时通信)
|
||||
- [数据验证](#数据验证)
|
||||
- [异常处理](#异常处理)
|
||||
|
||||
## 核心概念
|
||||
|
||||
NestJS 采用模块化架构,主要由以下几个部分组成:
|
||||
|
||||
- **Module(模块)**:组织代码的基本单元
|
||||
- **Controller(控制器)**:处理 HTTP 请求
|
||||
- **Provider(提供者)**:包括 Service、Repository 等,处理业务逻辑
|
||||
- **Gateway(网关)**:处理 WebSocket 连接
|
||||
|
||||
## 模块化开发
|
||||
|
||||
### 创建游戏模块
|
||||
|
||||
使用 NestJS CLI 快速生成模块:
|
||||
|
||||
```bash
|
||||
nest g module game
|
||||
nest g controller game
|
||||
nest g service game
|
||||
```
|
||||
|
||||
### 模块示例
|
||||
|
||||
```typescript
|
||||
// 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 {}
|
||||
```
|
||||
|
||||
在根模块中导入:
|
||||
|
||||
```typescript
|
||||
// src/app_module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GameModule } from './game/game_module';
|
||||
|
||||
@Module({
|
||||
imports: [GameModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## 控制器与路由
|
||||
|
||||
控制器负责处理 HTTP 请求,定义 RESTful API。
|
||||
|
||||
### 基础控制器示例
|
||||
|
||||
```typescript
|
||||
// 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()` - 请求头
|
||||
|
||||
## 服务与依赖注入
|
||||
|
||||
服务层包含业务逻辑,通过依赖注入使用。
|
||||
|
||||
### 服务示例
|
||||
|
||||
```typescript
|
||||
// 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。
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
yarn add @nestjs/websockets @nestjs/platform-socket.io socket.io
|
||||
```
|
||||
|
||||
### Gateway 示例
|
||||
|
||||
```typescript
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在模块中注册
|
||||
|
||||
```typescript
|
||||
// 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 进行数据验证。
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
yarn add class-validator class-transformer
|
||||
```
|
||||
|
||||
### DTO 示例
|
||||
|
||||
```typescript
|
||||
// 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;
|
||||
}
|
||||
```
|
||||
|
||||
### 启用全局验证管道
|
||||
|
||||
```typescript
|
||||
// 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();
|
||||
```
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 使用内置异常
|
||||
|
||||
```typescript
|
||||
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` - 400
|
||||
- `UnauthorizedException` - 401
|
||||
- `NotFoundException` - 404
|
||||
- `ForbiddenException` - 403
|
||||
- `InternalServerErrorException` - 500
|
||||
|
||||
### 自定义异常过滤器
|
||||
|
||||
```typescript
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实战案例:房间系统
|
||||
|
||||
### 数据模型
|
||||
|
||||
```typescript
|
||||
// src/model/room_entity.ts
|
||||
export interface Room {
|
||||
id: string;
|
||||
name: string;
|
||||
maxPlayers: number;
|
||||
players: string[];
|
||||
status: 'waiting' | 'playing' | 'finished';
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 服务层
|
||||
|
||||
```typescript
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控制器
|
||||
|
||||
```typescript
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **模块化设计**:按功能划分模块(player、room、game 等)
|
||||
2. **分层架构**:Controller → Service → Data,职责清晰
|
||||
3. **使用 DTO**:定义清晰的数据传输对象,启用验证
|
||||
4. **依赖注入**:充分利用 NestJS 的 DI 系统
|
||||
5. **异常处理**:使用内置异常类,提供友好的错误信息
|
||||
6. **配置管理**:使用 @nestjs/config 管理环境变量
|
||||
7. **日志记录**:使用内置 Logger 或集成第三方日志库
|
||||
8. **测试**:编写单元测试和 E2E 测试
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||||
- [NestJS 中文文档](https://docs.nestjs.cn/)
|
||||
- [Socket.IO 文档](https://socket.io/docs/)
|
||||
Reference in New Issue
Block a user