环境搭建
在开始开发之前,我们需要配置好开发环境。本指南将帮助您安装所有必要的工具和依赖。
Node.js 环境
推荐使用 Node.js 18.x 或更高版本
# 检查 Node.js 版本
node --version
# 安装 Node.js (使用 nvm)
nvm install 18
nvm use 18
NestJS CLI
全局安装 NestJS 命令行工具
# 安装 NestJS CLI
npm install -g @nestjs/cli
# 验证安装
nest --version
数据库环境
我们使用 PostgreSQL 作为主数据库,Redis 作为缓存
PostgreSQL
- • 版本 14.x 或更高
- • 支持 JSON 数据类型
- • 配置 UTF-8 编码
Redis
- • 版本 6.x 或更高
- • 会话存储
- • 消息队列
项目初始化
创建新的 NestJS 项目并配置基本结构
1. 创建项目
# 创建新的 NestJS 项目
nest new im-system
# 选择包管理器 (推荐使用 npm)
? Which package manager would you ❤️ to use? npm
# 进入项目目录
cd im-system
这将创建一个包含基本结构的 NestJS 项目
2. 安装依赖
# 安装 Prisma 相关依赖
npm install prisma @prisma/client
# 安装 WebSocket 支持
npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
# 安装认证相关依赖
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
# 安装其他工具库
npm install bcrypt class-validator class-transformer
npm install @nestjs/config
安装项目所需的核心依赖包
3. 项目结构
src/
├── auth/ # 认证模块
├── users/ # 用户模块
├── chat/ # 聊天模块
├── prisma/ # Prisma 配置
├── common/ # 公共组件
├── app.module.ts # 根模块
└── main.ts # 应用入口
推荐的项目目录结构,按功能模块组织代码
数据库设计
使用 Prisma 设计数据库模型,建立实体关系
1. 初始化 Prisma
# 初始化 Prisma
npx prisma init
# 这将创建 prisma/schema.prisma 文件和 .env 文件
初始化 Prisma 并创建配置文件
2. 配置数据库连接
# .env 文件
DATABASE_URL="postgresql://username:password@localhost:5432/im_system?schema=public"
# prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
配置 PostgreSQL 数据库连接
3. 定义数据模型
model User {
id String @id @default(cuid())
email String @unique
username String @unique
password String
avatar String?
status UserStatus @default(ONLINE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sentMessages Message[] @relation("SentMessages")
receivedMessages Message[] @relation("ReceivedMessages")
groupMembers GroupMember[]
@@map("users")
}
model Message {
id String @id @default(cuid())
content String
type MessageType @default(TEXT)
room String
senderId String
receiverId String?
fileUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sender User @relation("SentMessages", fields: [senderId], references: [id])
receiver User? @relation("ReceivedMessages", fields: [receiverId], references: [id])
@@map("messages")
}
enum UserStatus {
ONLINE
OFFLINE
AWAY
BUSY
}
enum MessageType {
TEXT
IMAGE
VIDEO
FILE
}
定义用户和消息的数据模型
4. 生成 Prisma Client
# 生成 Prisma Client
npx prisma generate
# 创建数据库表
npx prisma db push
# 查看数据库
npx prisma studio
生成类型安全的数据库客户端
认证模块
实现基于 JWT 的身份认证和授权系统
1. 创建认证模块
# 使用 NestJS CLI 创建模块
nest generate module auth
nest generate service auth
nest generate controller auth
生成认证模块的基本结构
2. JWT 策略配置
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '1d' },
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
配置 JWT 模块和 Passport 策略
3. JWT 策略实现
// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
实现 JWT 验证策略
WebSocket 实现
使用 Socket.IO 实现实时双向通信
1. 创建 WebSocket 网关
nest generate gateway chat
生成 WebSocket 网关的基本结构
2. 实现聊天网关
// chat/chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway implements
OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
afterInit(server: Server) {
console.log('WebSocket server initialized');
}
handleConnection(client: Socket) {
console.log(`Client connected: ${client.id}`);
}
handleDisconnect(client: Socket) {
console.log(`Client disconnected: ${client.id}`);
}
@SubscribeMessage('message')
async handleMessage(
@MessageBody() payload: { room: string; message: string },
@ConnectedSocket() client: Socket
) {
this.server.to(payload.room).emit('message', {
id: Date.now(),
text: payload.message,
sender: client.id,
timestamp: new Date(),
});
}
@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() payload: { room: string },
@ConnectedSocket() client: Socket
) {
client.join(payload.room);
client.emit('joined', { room: payload.room });
}
}
实现基本的 WebSocket 事件处理
聊天模块
实现完整的聊天功能,包括消息存储和历史记录
1. 创建聊天服务
nest generate service chat
生成聊天服务的基本结构
2. 实现消息服务
// chat/chat.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class ChatService {
constructor(private prisma: PrismaService) {}
async createMessage(data: {
content: string;
senderId: string;
receiverId?: string;
room: string;
}) {
return this.prisma.message.create({
data: {
content: data.content,
senderId: data.senderId,
receiverId: data.receiverId,
room: data.room,
},
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
async getMessages(room: string, limit: number = 50) {
return this.prisma.message.findMany({
where: { room },
orderBy: { createdAt: 'desc' },
take: limit,
include: {
sender: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
}
}
实现消息存储和查询功能
文件上传
实现文件上传功能,支持图片、视频等多种文件类型
1. 配置文件上传
npm install @nestjs/platform-express multer
安装文件上传所需的依赖
2. 实现文件上传控制器
// files/files.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
@Controller('files')
export class FilesController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
callback(null, `${uniqueSuffix}${ext}`);
},
}),
fileFilter: (req, file, callback) => {
const allowedTypes = ['image/jpeg', 'image/png', 'video/mp4', 'application/pdf'];
if (allowedTypes.includes(file.mimetype)) {
callback(null, true);
} else {
callback(new Error('不支持的文件类型'), false);
}
},
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
},
}))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
originalname: file.originalname,
filename: file.filename,
size: file.size,
mimetype: file.mimetype,
url: `/uploads/${file.filename}`,
};
}
}
实现文件上传功能,包含类型和大小限制
测试策略
编写单元测试和集成测试,确保代码质量
1. 测试配置
# 安装测试依赖
npm install --save-dev @nestjs/testing supertest @types/supertest
# 测试脚本
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
配置测试环境和脚本
2. 单元测试示例
// auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('validateUser', () => {
it('should return user data when credentials are valid', async () => {
const result = await service.validateUser('test@example.com', 'password');
expect(result).toBeDefined();
});
});
});
编写服务层的单元测试
部署指南
将应用部署到生产环境
1. 生产环境构建
# 构建应用
npm run build
# 启动生产服务器
npm run start:prod
构建和启动生产版本
2. Docker 部署
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
使用 Docker 容器化部署
3. 环境变量配置
# .env.production
NODE_ENV=production
PORT=3000
DATABASE_URL="postgresql://..."
JWT_SECRET="your-production-secret"
REDIS_URL="redis://..."
配置生产环境变量