乐闻世界logo
搜索文章和话题

面试题手册

npm、pnpm 和 Bun 有什么不同?我们应该选用哪个包管理工具?

除了 npm 和 Yarn,还有其他流行的 JavaScript 包管理器,如 pnpm 和 Bun。了解它们的特点和选择标准对于项目决策很重要。pnpm基本介绍pnpm 是一个快速、节省磁盘空间的包管理器,使用硬链接和符号链接来管理依赖。核心特性1. 节省磁盘空间pnpm 使用内容寻址存储,所有包都存储在全局存储中,通过硬链接引用:~/.pnpm-store/├── v3/│ └── files/│ └── 00/│ └── 00a1b2c3d4.../ # 包的实际文件project/└── node_modules/ └── .pnpm/ └── package@1.0.0/ └── node_modules/ └── package/ # 硬链接到全局存储2. 严格的依赖隔离pnpm 确保包只能访问其声明的依赖:// package-a 只能访问其声明的依赖// 不能访问 package-b 的依赖3. 快速安装pnpm 的安装速度比 npm 和 Yarn 更快:| 包管理器 | 安装时间(1000+ 依赖) ||---------|----------------------|| npm 6 | ~120s || npm 7+ | ~60s || Yarn 1 | ~45s || Yarn 2+ | ~40s || pnpm | ~30s |pnpm 命令# 安装 pnpmnpm install -g pnpm# 安装依赖pnpm install# 添加依赖pnpm add <package># 添加开发依赖pnpm add -D <package># 移除依赖pnpm remove <package># 更新依赖pnpm update# 运行脚本pnpm run <script># 全局安装pnpm add -g <package>pnpm 工作区{ "name": "my-monorepo", "version": "1.0.0", "private": true, "scripts": { "build": "pnpm -r build", "test": "pnpm -r test" }}# 在所有工作区中运行命令pnpm -r buildpnpm --recursive build# 在特定工作区中运行命令pnpm --filter package-a buildpnpm 配置# 设置存储位置pnpm config set store-dir ~/.pnpm-store# 设置注册表pnpm config set registry https://registry.npmmirror.com# 查看配置pnpm config listBun基本介绍Bun 是一个现代化的 JavaScript 运行时和包管理器,内置了包管理器、测试运行器和打包工具。核心特性1. 极快的安装速度Bun 的安装速度比其他包管理器快得多:| 包管理器 | 安装时间(1000+ 依赖) ||---------|----------------------|| npm 7+ | ~60s || Yarn 2+ | ~40s || pnpm | ~30s || Bun | ~10s |2. 内置工具链Bun 内置了多个工具:包管理器测试运行器打包工具服务器3. 兼容 Node.jsBun 兼容大多数 Node.js API 和 npm 包。Bun 命令# 安装 Buncurl -fsSL https://bun.sh/install | bash# 安装依赖bun install# 添加依赖bun add <package># 添加开发依赖bun add -d <package># 移除依赖bun remove <package># 更新依赖bun update# 运行脚本bun run <script># 运行文件bun run index.jsBun 工作区{ "name": "my-monorepo", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ]}# 在所有工作区中运行命令bun run build --filter "*"包管理器对比功能对比| 特性 | npm | Yarn | pnpm | Bun ||------|-----|------|-------|-----|| 安装速度 | 中 | 快 | 很快 | 极快 || 磁盘空间 | 高 | 高 | 低 | 中 || 依赖隔离 | 低 | 中 | 高 | 中 || 工作区支持 | npm 7+ | Yarn 1+ | 支持 | 支持 || Plug'n'Play | 否 | Yarn 2+ | 否 | 否 || 硬链接 | 否 | 否 | 是 | 否 || 内置工具 | 否 | 否 | 否 | 是 || 生态系统 | 最大 | 大 | 中 | 小 |使用场景对比npm适用场景:新项目,不需要特殊功能团队已经熟悉 npm需要最大的生态系统支持CI/CD 环境(默认支持)优势:随 Node.js 一起安装最大的生态系统良好的文档和社区支持npm 7+ 性能已足够好劣势:磁盘空间占用大依赖隔离较弱Yarn适用场景:大型 monorepo需要离线支持需要更好的用户体验需要 Plug'n'Play 功能优势:更好的工作区支持成熟的离线模式更好的用户体验丰富的插件生态劣势:需要单独安装Yarn 2+ 学习曲线较陡pnpm适用场景:磁盘空间受限需要严格的依赖隔离需要快速安装大型 monorepo优势:极低的磁盘空间占用严格的依赖隔离极快的安装速度良好的工作区支持劣势:生态系统相对较小硬链接可能在某些系统上有问题Bun适用场景:需要极快的安装速度新项目,可以尝试新技术需要内置工具链性能敏感的项目优势:极快的安装速度内置工具链兼容 Node.js现代化的设计劣势:生态系统较小相对较新,稳定性待验证社区支持有限迁移指南从 npm 迁移到 pnpm# 1. 安装 pnpmnpm install -g pnpm# 2. 删除 node_modules 和 lock 文件rm -rf node_modules package-lock.json# 3. 安装依赖pnpm install# 4. 更新脚本(如果需要)# npm run -> pnpm run# npm install -> pnpm install从 Yarn 迁移到 pnpm# 1. 安装 pnpmnpm install -g pnpm# 2. 删除 node_modules 和 lock 文件rm -rf node_modules yarn.lock# 3. 安装依赖pnpm install# 4. 更新脚本# yarn -> pnpm从 npm 迁移到 Bun# 1. 安装 Buncurl -fsSL https://bun.sh/install | bash# 2. 删除 node_modules 和 lock 文件rm -rf node_modules package-lock.json# 3. 安装依赖bun install# 4. 更新脚本# npm run -> bun run最佳实践1. 选择合适的包管理器考虑因素:项目规模和复杂度团队熟悉度性能要求磁盘空间限制CI/CD 环境支持2. 锁定包管理器在项目中明确使用的包管理器:{ "scripts": { "preinstall": "npx only-allow pnpm" }}3. 使用 CI/CD 缓存# GitHub Actions - pnpm- uses: pnpm/action-setup@v2 with: version: 8- uses: actions/setup-node@v3 with: cache: 'pnpm'- run: pnpm install4. 文档化选择在 README 中说明使用的包管理器:## InstallationThis project uses pnpm as the package manager.bashInstall pnpmnpm install -g pnpmInstall dependenciespnpm install```## 性能基准测试### 安装速度测试bash测试 npmtime npm install测试 Yarntime yarn install测试 pnpmtime pnpm install测试 Buntime bun install### 磁盘空间测试bash查看 node_modules 大小du -sh node_modulesnpm: ~1GBYarn: ~1GBpnpm: ~300MBBun: ~800MB```未来趋势npm:持续改进性能和安全性Yarn:推动 Plug'n'Play 和零安装pnpm:改进硬链接和依赖隔离Bun:快速成长,可能成为主流选择选择包管理器应该基于项目需求、团队偏好和长期维护考虑。
阅读 0·2月17日 22:42

NestJS 安全和认证如何实现?

安全性是任何应用程序的关键组成部分。NestJS 提供了多种安全机制,包括身份验证、授权、数据加密、防护措施等,帮助开发者构建安全可靠的应用程序。身份验证(Authentication)JWT 认证安装依赖npm install @nestjs/jwt @nestjs/passport passport passport-jwtnpm install -D @types/passport-jwt创建 JWT 策略import { Injectable, UnauthorizedException } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { ExtractJwt, Strategy } from 'passport-jwt';import { ConfigService } from '@nestjs/config';@Injectable()export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private configService: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), }); } async validate(payload: any) { return { userId: payload.sub, username: payload.username }; }}创建认证服务import { Injectable } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';import { UsersService } from '../users/users.service';import * as bcrypt from 'bcrypt';@Injectable()export class AuthService { constructor( private usersService: UsersService, private jwtService: JwtService, ) {} async validateUser(username: string, password: string): Promise<any> { const user = await this.usersService.findByUsername(username); if (user && await bcrypt.compare(password, user.password)) { const { password, ...result } = user; return result; } return null; } async login(user: any) { const payload = { username: user.username, sub: user.userId }; return { access_token: this.jwtService.sign(payload), }; } async register(username: string, password: string) { const hashedPassword = await bcrypt.hash(password, 10); return this.usersService.create({ username, password: hashedPassword }); }}创建认证控制器import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';import { AuthService } from './auth.service';import { LocalAuthGuard } from './local-auth.guard';import { JwtAuthGuard } from './jwt-auth.guard';@Controller('auth')export class AuthController { constructor(private authService: AuthService) {} @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req) { return this.authService.login(req.user); } @Post('register') async register(@Body() registerDto: { username: string; password: string }) { return this.authService.register(registerDto.username, registerDto.password); } @UseGuards(JwtAuthGuard) @Post('profile') getProfile(@Request() req) { return req.user; }}本地认证策略import { Injectable, UnauthorizedException } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { Strategy } from 'passport-local';import { AuthService } from './auth.service';@Injectable()export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super({ usernameField: 'username', passwordField: 'password', }); } async validate(username: string, password: string): Promise<any> { const user = await this.authService.validateUser(username, password); if (!user) { throw new UnauthorizedException(); } return user; }}OAuth2 认证安装依赖npm install @nestjs/passport passport passport-google-oauth20npm install -D @types/passport-google-oauth20创建 Google OAuth 策略import { Injectable } from '@nestjs/common';import { PassportStrategy } from '@nestjs/passport';import { Strategy, VerifyCallback } from 'passport-google-oauth20';import { ConfigService } from '@nestjs/config';@Injectable()export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { constructor(private configService: ConfigService) { super({ clientID: configService.get('GOOGLE_CLIENT_ID'), clientSecret: configService.get('GOOGLE_CLIENT_SECRET'), callbackURL: configService.get('GOOGLE_CALLBACK_URL'), scope: ['email', 'profile'], }); } async validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise<any> { const { name, emails, photos } = profile; const user = { email: emails[0].value, firstName: name.givenName, lastName: name.familyName, picture: photos[0].value, accessToken, }; done(null, user); }}授权(Authorization)基于角色的访问控制(RBAC)import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user?.roles?.includes(role)); }}角色装饰器import { SetMetadata } from '@nestjs/common';export const ROLES_KEY = 'roles';export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);使用角色守卫@Controller('users')export class UsersController { @Get('admin') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') getAdminData() { return 'Admin data'; }}密码加密使用 bcryptimport * as bcrypt from 'bcrypt';async hashPassword(password: string): Promise<string> { const salt = await bcrypt.genSalt(10); return bcrypt.hash(password, salt);}async comparePassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash);}安全中间件Helmetimport helmet from 'helmet';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(helmet()); await app.listen(3000);}CORS 配置async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: 'https://example.com', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); await app.listen(3000);}速率限制import rateLimit from 'express-rate-limit';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use( rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs }), ); await app.listen(3000);}数据验证使用 class-validatorimport { IsEmail, IsString, MinLength } from 'class-validator';export class CreateUserDto { @IsEmail() email: string; @IsString() @MinLength(6) password: string;}全局验证管道import { ValidationPipe } from '@nestjs/common';async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); await app.listen(3000);}SQL 注入防护使用参数化查询// 不安全const query = `SELECT * FROM users WHERE name = '${name}'`;// 安全const query = `SELECT * FROM users WHERE name = ?`;const result = await this.userRepository.query(query, [name]);使用 ORM// TypeORM 自动防止 SQL 注入const user = await this.userRepository.findOne({ where: { name: username },});XSS 防护输入清理import * as xss from 'xss';function sanitizeInput(input: string): string { return xss(input);}内容安全策略(CSP)import helmet from 'helmet';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], }, }), ); await app.listen(3000);}CSRF 防护使用 csurfimport * as csurf from 'csurf';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(csurf({ cookie: true })); await app.listen(3000);}安全最佳实践使用 HTTPS:始终在生产环境中使用 HTTPS环境变量:敏感信息存储在环境变量中密码加密:使用 bcrypt 等安全算法加密密码输入验证:验证所有用户输入最小权限原则:用户只拥有必要的权限定期更新依赖:保持依赖项更新以修复安全漏洞日志记录:记录安全相关事件错误处理:不向客户端暴露敏感错误信息会话管理:使用安全的会话管理安全头:设置适当的安全头总结NestJS 安全和认证提供了:多种身份验证策略灵活的授权机制强大的数据保护全面的安全防护易于集成的安全工具掌握安全性和认证是构建安全、可靠的 NestJS 应用程序的关键。通过合理使用认证策略、授权机制和安全最佳实践,可以保护应用程序免受各种安全威胁,确保用户数据和系统的安全。
阅读 0·2月17日 22:41

NestJS 如何集成数据库?

数据库集成概述NestJS 提供了对多种数据库的完整支持,包括关系型数据库(如 MySQL、PostgreSQL)和非关系型数据库(如 MongoDB)。通过使用 TypeORM、Prisma、Mongoose 等 ORM/ODM 库,开发者可以轻松实现数据库操作。TypeORM 集成安装依赖npm install @nestjs/typeorm typeormnpm install mysql2 # 或 pg、sqlite3、better-sqlite3 等配置 TypeORMimport { Module } from '@nestjs/common';import { TypeOrmModule } from '@nestjs/typeorm';import { User } from './entities/user.entity';@Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test', entities: [User], synchronize: true, // 生产环境应设为 false logging: true, }), TypeOrmModule.forFeature([User]), ], providers: [UserService], controllers: [UserController],})export class UserModule {}创建实体import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';@Entity('users')export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @Column() password: string; @Column() name: string; @Column({ default: true }) isActive: boolean; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date;}创建 Repositoryimport { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './entities/user.entity';import { CreateUserDto } from './dto/create-user.dto';@Injectable()export class UserService { constructor( @InjectRepository(User) private userRepository: Repository<User>, ) {} async create(createUserDto: CreateUserDto): Promise<User> { const user = this.userRepository.create(createUserDto); return this.userRepository.save(user); } async findAll(): Promise<User[]> { return this.userRepository.find(); } async findOne(id: number): Promise<User> { return this.userRepository.findOne({ where: { id } }); } async update(id: number, updateUserDto: any): Promise<User> { await this.userRepository.update(id, updateUserDto); return this.findOne(id); } async remove(id: number): Promise<void> { await this.userRepository.delete(id); } async findByEmail(email: string): Promise<User> { return this.userRepository.findOne({ where: { email } }); }}Prisma 集成安装依赖npm install @prisma/clientnpm install prisma --save-devnpx prisma init配置 Prisma// prisma/schema.prismadatasource db { provider = "mysql" url = env("DATABASE_URL")}generator client { provider = "prisma-client-js"}model User { id Int @id @default(autoincrement()) email String @unique password String name String isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt}创建 Prisma 服务import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';import { PrismaClient } from '@prisma/client';@Injectable()export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); }}使用 Prismaimport { Injectable } from '@nestjs/common';import { PrismaService } from './prisma.service';import { CreateUserDto } from './dto/create-user.dto';@Injectable()export class UserService { constructor(private prisma: PrismaService) {} async create(createUserDto: CreateUserDto) { return this.prisma.user.create({ data: createUserDto, }); } async findAll() { return this.prisma.user.findMany(); } async findOne(id: number) { return this.prisma.user.findUnique({ where: { id }, }); } async update(id: number, updateUserDto: any) { return this.prisma.user.update({ where: { id }, data: updateUserDto, }); } async remove(id: number) { return this.prisma.user.delete({ where: { id }, }); }}Mongoose 集成安装依赖npm install @nestjs/mongoose mongoose配置 Mongooseimport { Module } from '@nestjs/common';import { MongooseModule } from '@nestjs/mongoose';import { User, UserSchema } from './schemas/user.schema';import { UserService } from './user.service';import { UserController } from './user.controller';@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost:27017/nest'), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), ], providers: [UserService], controllers: [UserController],})export class UserModule {}创建 Schemaimport { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';import { Document } from 'mongoose';@Schema()export class User extends Document { @Prop({ required: true, unique: true }) email: string; @Prop({ required: true }) password: string; @Prop({ required: true }) name: string; @Prop({ default: true }) isActive: boolean; @Prop({ default: Date.now }) createdAt: Date; @Prop({ default: Date.now }) updatedAt: Date;}export const UserSchema = SchemaFactory.createForClass(User);创建 Mongoose 服务import { Injectable } from '@nestjs/common';import { InjectModel } from '@nestjs/mongoose';import { Model } from 'mongoose';import { User } from './schemas/user.schema';import { CreateUserDto } from './dto/create-user.dto';@Injectable()export class UserService { constructor( @InjectModel(User.name) private userModel: Model<User>, ) {} async create(createUserDto: CreateUserDto): Promise<User> { const createdUser = new this.userModel(createUserDto); return createdUser.save(); } async findAll(): Promise<User[]> { return this.userModel.find().exec(); } async findOne(id: string): Promise<User> { return this.userModel.findOne({ _id: id }).exec(); } async update(id: string, updateUserDto: any): Promise<User> { return this.userModel.findByIdAndUpdate(id, updateUserDto, { new: true }).exec(); } async remove(id: string): Promise<User> { return this.userModel.findByIdAndDelete(id).exec(); }}数据库迁移TypeORM 迁移# 生成迁移npm run typeorm migration:generate -- -n CreateUserMigration# 运行迁移npm run typeorm migration:run# 回滚迁移npm run typeorm migration:revertPrisma 迁移# 创建迁移npx prisma migrate dev --name init# 应用迁移npx prisma migrate deploy# 重置数据库npx prisma migrate reset事务处理TypeORM 事务import { Injectable } from '@nestjs/common';import { InjectRepository } from '@nestjs/typeorm';import { Repository, DataSource } from 'typeorm';import { User } from './entities/user.entity';import { Order } from './entities/order.entity';@Injectable()export class OrderService { constructor( private dataSource: DataSource, @InjectRepository(User) private userRepository: Repository<User>, @InjectRepository(Order) private orderRepository: Repository<Order>, ) {} async createOrderWithUser(userId: number, orderData: any) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const user = await queryRunner.manager.findOne(User, { where: { id: userId } }); const order = queryRunner.manager.create(Order, { ...orderData, user, }); await queryRunner.manager.save(order); await queryRunner.commitTransaction(); return order; } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } }}Prisma 事务async createOrderWithUser(userId: number, orderData: any) { return this.prisma.$transaction(async (prisma) => { const user = await prisma.user.findUnique({ where: { id: userId }, }); const order = await prisma.order.create({ data: { ...orderData, user: { connect: { id: userId }, }, }, }); return order; });}关系映射TypeORM 关系@Entity('users')export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Order, order => order.user) orders: Order[];}@Entity('orders')export class Order { @PrimaryGeneratedColumn() id: number; @Column() userId: number; @ManyToOne(() => User, user => user.orders) user: User; @Column() product: string;}Prisma 关系model User { id Int @id @default(autoincrement()) name String orders Order[]}model Order { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id]) product String}数据库连接池配置TypeORM 连接池TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test', entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: false, extra: { connectionLimit: 10, },})Prisma 连接池datasource db { provider = "mysql" url = env("DATABASE_URL") // 连接池配置 connection_limit = 10}最佳实践使用 DTO:使用数据传输对象来验证和转换数据环境变量:使用环境变量管理数据库配置迁移管理:使用迁移来管理数据库结构变更事务处理:在需要原子性操作时使用事务索引优化:为常用查询字段添加索引连接池:合理配置连接池大小软删除:实现软删除而不是物理删除查询优化:避免 N+1 查询问题总结NestJS 数据库集成提供了:对多种数据库的支持灵活的 ORM/ODM 选择完整的类型安全强大的关系映射便捷的事务处理掌握数据库集成是构建数据驱动的 NestJS 应用程序的基础。通过合理选择和使用 ORM/ODM,遵循最佳实践,可以构建出高性能、可维护的数据访问层。
阅读 0·2月17日 22:40

什么是 NestJS 框架,它的核心特点和应用场景是什么?

NestJS 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,使用 TypeScript 构建,并结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数响应式编程)的元素。核心特点基于 TypeScript:NestJS 完全使用 TypeScript 编写,提供强类型支持模块化架构:应用程序被组织成模块,每个模块封装一组相关功能依赖注入:内置强大的依赖注入系统,便于测试和维护Express/Fastify 兼容:底层可以使用 Express 或 Fastify 作为 HTTP 服务器装饰器驱动:大量使用装饰器来简化代码编写可测试性:提供完整的测试工具和最佳实践与其他框架的对比与 Express 对比:NestJS 提供了更结构化的架构和内置的依赖注入,而 Express 更轻量级但需要手动组织代码与 Koa 对比:NestJS 提供了更完整的开箱即用功能,而 Koa 更注重中间件的灵活性与 Meteor 对比:NestJS 是后端框架,而 Meteor 是全栈框架适用场景企业级后端 API 开发微服务架构实时应用程序(使用 WebSocket)GraphQL API 开发RESTful API 开发为什么选择 NestJS学习曲线平缓:对于有 Angular 经验的开发者来说,NestJS 的概念非常熟悉可维护性高:模块化和依赖注入使代码更易于维护生态系统丰富:支持多种数据库、ORM、验证库等社区活跃:拥有活跃的社区和丰富的文档TypeScript 支持:提供完整的类型安全和智能提示基本架构NestJS 应用程序由以下核心概念组成:Modules(模块):组织应用程序的基本单元Controllers(控制器):处理传入请求并返回响应Providers(提供者):处理业务逻辑Services(服务):Provider 的一种,用于封装可重用的业务逻辑Pipes(管道):数据转换和验证Guards(守卫):权限控制和路由保护Interceptors(拦截器):在函数执行前后添加额外逻辑Exception Filters(异常过滤器):处理异常和错误响应总结NestJS 是一个现代化的 Node.js 后端框架,它通过提供结构化的架构、强大的依赖注入系统和丰富的功能集,使开发者能够构建可维护、可测试和可扩展的服务器端应用程序。它特别适合需要长期维护的大型项目和企业级应用。
阅读 0·2月17日 22:40

NestJS 测试和最佳实践有哪些?

NestJS 测试概述NestJS 提供了完整的测试支持,包括单元测试、集成测试和端到端测试。测试框架基于 Jest,提供了丰富的测试工具和实用程序。测试类型1. 单元测试(Unit Testing)单元测试用于测试单个函数、类或模块的行为,通常不涉及外部依赖。服务单元测试示例import { Test, TestingModule } from '@nestjs/testing';import { UsersService } from './users.service';import { getRepositoryToken } from '@nestjs/typeorm';import { User } from './entities/user.entity';import { Repository } from 'typeorm';describe('UsersService', () => { let service: UsersService; let repository: Repository<User>; const mockUserRepository = { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn(), update: jest.fn(), delete: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockUserRepository, }, ], }).compile(); service = module.get<UsersService>(UsersService); repository = module.get<Repository<User>>(getRepositoryToken(User)); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('create', () => { it('should create a new user', async () => { const createUserDto = { name: 'John', email: 'john@example.com' }; const user = { id: 1, ...createUserDto }; mockUserRepository.create.mockReturnValue(user); mockUserRepository.save.mockResolvedValue(user); const result = await service.create(createUserDto); expect(repository.create).toHaveBeenCalledWith(createUserDto); expect(repository.save).toHaveBeenCalledWith(user); expect(result).toEqual(user); }); }); describe('findAll', () => { it('should return an array of users', async () => { const users = [ { id: 1, name: 'John', email: 'john@example.com' }, { id: 2, name: 'Jane', email: 'jane@example.com' }, ]; mockUserRepository.find.mockResolvedValue(users); const result = await service.findAll(); expect(repository.find).toHaveBeenCalled(); expect(result).toEqual(users); }); });});2. 集成测试(Integration Testing)集成测试用于测试多个组件之间的交互,通常涉及数据库、外部服务等。控制器集成测试示例import { Test, TestingModule } from '@nestjs/testing';import { INestApplication } from '@nestjs/common';import * as request from 'supertest';import { AppModule } from './../src/app.module';describe('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('/users (GET)', () => { return request(app.getHttpServer()) .get('/users') .expect(200) .expect([]); }); it('/users (POST)', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John', email: 'john@example.com' }) .expect(201) .expect(res => { expect(res.body).toHaveProperty('id'); expect(res.body.name).toBe('John'); }); }); afterEach(async () => { await app.close(); });});3. 端到端测试(E2E Testing)端到端测试模拟真实用户场景,测试整个应用程序的流程。import { Test, TestingModule } from '@nestjs/testing';import { INestApplication, ValidationPipe } from '@nestjs/common';import * as request from 'supertest';import { AppModule } from './../src/app.module';describe('UsersController (e2e)', () => { let app: INestApplication; let userId: number; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe()); await app.init(); }); it('should create a user', async () => { const response = await request(app.getHttpServer()) .post('/users') .send({ name: 'John Doe', email: 'john@example.com' }) .expect(201); userId = response.body.id; expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe('John Doe'); }); it('should get all users', async () => { const response = await request(app.getHttpServer()) .get('/users') .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should get a user by id', async () => { const response = await request(app.getHttpServer()) .get(`/users/${userId}`) .expect(200); expect(response.body.id).toBe(userId); }); it('should update a user', async () => { const response = await request(app.getHttpServer()) .patch(`/users/${userId}`) .send({ name: 'Jane Doe' }) .expect(200); expect(response.body.name).toBe('Jane Doe'); }); it('should delete a user', async () => { await request(app.getHttpServer()) .delete(`/users/${userId}`) .expect(200); }); afterAll(async () => { await app.close(); });});测试工具和实用程序1. Test.createTestingModule()创建测试模块,用于配置测试环境。const module: TestingModule = await Test.createTestingModule({ imports: [AppModule], providers: [UsersService],}).compile();2. Mock 提供者使用 mock 对象替换真实的提供者。{ provide: UsersService, useValue: { findAll: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue({ id: 1 }), },}3. Jest 函数使用 Jest 的 mock 功能。jest.fn() // 创建 mock 函数jest.mock() // 模块 mockjest.spyOn() // 监听对象方法mockResolvedValue() // 设置返回值mockRejectedValue() // 设置拒绝值mockReturnValue() // 设置同步返回值mockReturnValueOnce() // 设置一次性返回值最佳实践1. 测试组织src/├── users/│ ├── users.controller.spec.ts│ ├── users.service.spec.ts│ └── users.module.spec.tstest/├── app.e2e-spec.ts└── users.e2e-spec.ts2. 测试命名约定使用 describe 分组相关测试使用 it 或 test 描述单个测试用例测试名称应该清晰描述测试内容describe('UsersService', () => { describe('create', () => { it('should create a new user', async () => { // 测试逻辑 }); it('should throw an error if email already exists', async () => { // 测试逻辑 }); });});3. 测试隔离每个测试应该独立运行,不依赖其他测试的状态。beforeEach(() => { // 在每个测试前重置状态});afterEach(() => { // 在每个测试后清理});4. 测试覆盖率使用 Jest 的覆盖率功能确保代码质量。npm run test:cov在 package.json 中配置:{ "jest": { "collectCoverageFrom": [ "src/**/*.(t|j)s", "!src/main.ts", "!src/**/*.module.ts", "!src/**/*.dto.ts" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } }}5. Mock 外部依赖对于数据库、API 等外部依赖,使用 mock 避免实际调用。const mockRepository = { find: jest.fn(), findOne: jest.fn(), save: jest.fn(), delete: jest.fn(),};6. 测试异步代码正确处理异步代码的测试。// 使用 async/awaitit('should return user', async () => { const result = await service.findOne(1); expect(result).toBeDefined();});// 使用 Promiseit('should return user', () => { return service.findOne(1).then(result => { expect(result).toBeDefined(); });});// 使用 done 回调it('should return user', (done) => { service.findOne(1).then(result => { expect(result).toBeDefined(); done(); });});7. 测试边界情况确保测试覆盖各种边界情况和错误场景。describe('findOne', () => { it('should return user when found', async () => { // 正常情况 }); it('should throw NotFoundException when user not found', async () => { // 用户不存在的情况 }); it('should throw BadRequestException when id is invalid', async () => { // 无效 ID 的情况 });});8. 使用测试数据库在集成测试中使用测试数据库,避免影响生产数据。beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }), ], }).compile();});性能测试1. 负载测试使用工具如 Artillery 或 k6 进行负载测试。// artillery.config.jsmodule.exports = { config: { target: 'http://localhost:3000', phases: [ { duration: 60, arrivalRate: 10 }, ], }, scenarios: [ { flow: [ { get: { url: '/users' } }, ], }, ],};2. 响应时间测试确保 API 响应时间在可接受范围内。it('should respond within 200ms', async () => { const start = Date.now(); await request(app.getHttpServer()).get('/users'); const duration = Date.now() - start; expect(duration).toBeLessThan(200);});代码质量最佳实践1. 代码风格使用 ESLint 和 Prettier 保持代码风格一致。npm run lintnpm run format2. 类型安全充分利用 TypeScript 的类型系统。interface CreateUserDto { name: string; email: string;}3. 错误处理统一错误处理机制。@Catch()export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { // 统一错误处理逻辑 }}4. 日志记录使用 NestJS Logger 进行结构化日志记录。import { Logger } from '@nestjs/common';export class UsersService { private readonly logger = new Logger(UsersService.name); async findAll() { this.logger.log('Finding all users'); // 业务逻辑 }}5. 环境配置使用 @nestjs/config 管理环境变量。import { ConfigService } from '@nestjs/config';export class UsersService { constructor(private configService: ConfigService) { const apiKey = this.configService.get('API_KEY'); }}6. 文档化使用 Swagger 生成 API 文档。import { ApiTags, ApiOperation } from '@nestjs/swagger';@ApiTags('users')@Controller('users')export class UsersController { @Get() @ApiOperation({ summary: 'Get all users' }) findAll() { return this.usersService.findAll(); }}总结NestJS 测试和最佳实践提供了:完整的测试支持框架灵活的测试工具和实用程序清晰的测试组织结构高质量的代码标准良好的开发体验掌握测试和最佳实践是构建高质量 NestJS 应用程序的关键。通过全面的测试覆盖和遵循最佳实践,可以确保应用程序的可靠性、可维护性和可扩展性。测试不仅仅是质量保证,更是开发过程中的重要组成部分。
阅读 0·2月17日 22:36

NestJS WebSocket 和实时功能如何实现?

WebSocket 概述WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。NestJS 通过 @nestjs/websockets 和 @nestjs/platform-socket.io 包提供了对 WebSocket 的完整支持,使开发者能够轻松构建实时应用程序。NestJS WebSocket 基础安装依赖npm install @nestjs/websockets @nestjs/platform-socket.io创建 WebSocket 网关网关(Gateway)是 NestJS 中处理 WebSocket 连接的类,类似于控制器处理 HTTP 请求。import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } 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') handleMessage(client: Socket, payload: any): void { this.server.emit('message', payload); }}注册网关import { Module } from '@nestjs/common';import { ChatGateway } from './chat.gateway';@Module({ providers: [ChatGateway],})export class ChatModule {}WebSocket 网关装饰器@WebSocketGateway()配置 WebSocket 网关。@WebSocketGateway({ namespace: '/chat', // 命名空间 cors: { // CORS 配置 origin: '*', }, path: '/ws', // 路径 transports: ['websocket'], // 传输方式})export class ChatGateway {}@WebSocketServer()注入 WebSocket 服务器实例。@WebSocketServer()server: Server;@SubscribeMessage()订阅 WebSocket 消息。@SubscribeMessage('message')handleMessage(client: Socket, payload: any): void { // 处理消息}@MessageBody()提取消息体。@SubscribeMessage('message')handleMessage(@MessageBody() data: any): void { console.log(data);}@ConnectedSocket()获取连接的 Socket 实例。@SubscribeMessage('message')handleMessage(@ConnectedSocket() client: Socket): void { console.log(client.id);}网关生命周期钩子OnGatewayInitafterInit(server: Server) { console.log('Gateway initialized');}OnGatewayConnectionhandleConnection(client: Socket) { console.log(`Client connected: ${client.id}`);}OnGatewayDisconnecthandleDisconnect(client: Socket) { console.log(`Client disconnected: ${client.id}`);}实时聊天应用示例聊天网关import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';import { Server, Socket } from 'socket.io';interface Message { user: string; text: string; timestamp: Date;}@WebSocketGateway({ cors: { origin: '*', },})export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private connectedClients: Map<string, Socket> = new Map(); handleConnection(client: Socket) { this.connectedClients.set(client.id, client); this.server.emit('user-joined', { userId: client.id, userCount: this.connectedClients.size, }); } handleDisconnect(client: Socket) { this.connectedClients.delete(client.id); this.server.emit('user-left', { userId: client.id, userCount: this.connectedClients.size, }); } @SubscribeMessage('join-room') handleJoinRoom(client: Socket, room: string): void { client.join(room); client.emit('joined-room', room); } @SubscribeMessage('leave-room') handleLeaveRoom(client: Socket, room: string): void { client.leave(room); client.emit('left-room', room); } @SubscribeMessage('send-message') handleMessage(client: Socket, payload: Message): void { const message: Message = { ...payload, timestamp: new Date(), }; this.server.emit('message', message); } @SubscribeMessage('private-message') handlePrivateMessage(client: Socket, payload: { to: string; message: Message }): void { const recipient = this.connectedClients.get(payload.to); if (recipient) { recipient.emit('private-message', payload.message); } }}前端客户端import { io, Socket } from 'socket.io-client';class ChatClient { private socket: Socket; constructor(url: string) { this.socket = io(url); } connect() { this.socket.on('connect', () => { console.log('Connected to server'); }); this.socket.on('message', (message: Message) => { console.log('Received message:', message); }); this.socket.on('user-joined', (data) => { console.log('User joined:', data); }); this.socket.on('user-left', (data) => { console.log('User left:', data); }); } sendMessage(message: Message) { this.socket.emit('send-message', message); } sendPrivateMessage(to: string, message: Message) { this.socket.emit('private-message', { to, message }); } joinRoom(room: string) { this.socket.emit('join-room', room); } leaveRoom(room: string) { this.socket.emit('leave-room', room); } disconnect() { this.socket.disconnect(); }}使用 WebSocket 守卫import { WebSocketGateway, SubscribeMessage, OnGatewayConnection } from '@nestjs/websockets';import { UseGuards } from '@nestjs/common';import { WsGuard } from './ws.guard';@WebSocketGateway()@UseGuards(WsGuard)export class ChatGateway implements OnGatewayConnection { @SubscribeMessage('message') handleMessage(client: Socket, payload: any): void { // 只有通过验证的客户端才能处理消息 }}WebSocket 守卫示例import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';import { Socket } from 'socket.io';@Injectable()export class WsGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const client: Socket = context.switchToWs().getClient(); const token = client.handshake.auth.token; if (!token) { throw new UnauthorizedException(); } // 验证 token return this.validateToken(token); } private validateToken(token: string): boolean { // 实现 token 验证逻辑 return true; }}使用 WebSocket 拦截器import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';import { UseInterceptors } from '@nestjs/common';import { LoggingInterceptor } from './logging.interceptor';@WebSocketGateway()@UseInterceptors(LoggingInterceptor)export class ChatGateway { @SubscribeMessage('message') handleMessage(client: Socket, payload: any): void { // 消息会被拦截器处理 }}使用 WebSocket 管道import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';import { UsePipes } from '@nestjs/common';import { ValidationPipe } from './validation.pipe';@WebSocketGateway()@UsePipes(new ValidationPipe())export class ChatGateway { @SubscribeMessage('message') handleMessage(@MessageBody() message: Message): void { // 消息会被管道验证 }}实时通知系统通知网关import { WebSocketGateway, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';import { Server, Socket } from 'socket.io';interface Notification { type: 'info' | 'warning' | 'error' | 'success'; title: string; message: string; timestamp: Date;}@WebSocketGateway({ namespace: '/notifications', cors: { origin: '*' },})export class NotificationGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private userSockets: Map<string, Set<string>> = new Map(); handleConnection(client: Socket) { const userId = client.handshake.query.userId as string; if (!this.userSockets.has(userId)) { this.userSockets.set(userId, new Set()); } this.userSockets.get(userId).add(client.id); } handleDisconnect(client: Socket) { const userId = client.handshake.query.userId as string; const sockets = this.userSockets.get(userId); if (sockets) { sockets.delete(client.id); if (sockets.size === 0) { this.userSockets.delete(userId); } } } sendNotificationToUser(userId: string, notification: Notification) { const sockets = this.userSockets.get(userId); if (sockets) { sockets.forEach(socketId => { this.server.to(socketId).emit('notification', notification); }); } } sendBroadcastNotification(notification: Notification) { this.server.emit('notification', notification); }}在服务中使用通知网关import { Injectable } from '@nestjs/common';import { NotificationGateway } from './notification.gateway';@Injectable()export class NotificationService { constructor(private notificationGateway: NotificationGateway) {} sendOrderNotification(userId: string, orderId: string) { this.notificationGateway.sendNotificationToUser(userId, { type: 'info', title: '订单更新', message: `您的订单 ${orderId} 已更新`, timestamp: new Date(), }); } sendSystemAlert(message: string) { this.notificationGateway.sendBroadcastNotification({ type: 'warning', title: '系统通知', message, timestamp: new Date(), }); }}最佳实践命名空间:使用命名空间组织不同类型的 WebSocket 连接房间:使用房间功能管理用户组认证:在连接时进行身份验证错误处理:妥善处理连接错误和消息错误资源清理:在断开连接时清理资源消息验证:使用管道验证传入的消息日志记录:记录连接和断开事件性能监控:监控连接数和消息吞吐量总结NestJS WebSocket 和实时功能提供了:完整的 WebSocket 支持灵活的网关系统丰富的装饰器和钩子与 NestJS 生态系统的无缝集成易于构建实时应用程序掌握 NestJS WebSocket 功能是构建实时应用程序的关键。通过合理使用网关、命名空间、房间和认证机制,可以构建出高性能、可扩展的实时应用,如聊天应用、实时通知系统、协作工具等。
阅读 0·2月17日 22:35

NestJS 中间件和守卫的区别是什么?

中间件(Middleware)的概念中间件是在路由处理程序之前调用的函数,可以访问请求和响应对象,以及应用程序的请求-响应周期中的 next() 中间件函数。中间件主要用于:执行任何代码更改请求和响应对象结束请求-响应周期调用堆栈中的下一个中间件函数中间件的基本结构import { Injectable, NestMiddleware } from '@nestjs/common';import { Request, Response, NextFunction } from 'express';@Injectable()export class LoggerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { console.log(`Request... ${req.method} ${req.url}`); next(); }}应用中间件在模块中应用中间件import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';import { LoggerMiddleware } from './logger.middleware';@Module({ imports: [], controllers: [], providers: [],})export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .forRoutes('cats'); }}限制中间件应用到特定路由configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) .exclude('cats', { path: 'cats', method: RequestMethod.GET }) .forRoutes(CatsController);}应用多个中间件configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware, AuthMiddleware) .forRoutes(CatsController);}函数式中间件export function logger(req: Request, res: Response, next: NextFunction) { console.log(`Request...`); next();}// 应用函数式中间件configure(consumer: MiddlewareConsumer) { consumer .apply(logger) .forRoutes(CatsController);}全局中间件import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { logger } from './common/middleware/logger.middleware';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(logger); await app.listen(3000);}bootstrap();守卫(Guards)的概念守卫是一个使用 @Injectable() 装饰器的类,实现了 CanActivate 接口。守卫负责确定请求是否应该由路由处理程序处理。主要用于:身份验证授权权限检查守卫的基本结构import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';@Injectable()export class AuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); return this.validateRequest(request); } private validateRequest(request: any): boolean { // 验证逻辑 return true; }}使用守卫在控制器上使用守卫@Controller('cats')@UseGuards(AuthGuard)export class CatsController { @Get() findAll() { return 'This action returns all cats'; }}在特定路由上使用守卫@Controller('cats')export class CatsController { @Get() @UseGuards(AuthGuard) findAll() { return 'This action returns all cats'; }}全局守卫import { Module } from '@nestjs/common';import { APP_GUARD } from '@nestjs/core';import { AuthGuard } from './auth.guard';@Module({ providers: [ { provide: APP_GUARD, useClass: AuthGuard, }, ],})export class AppModule {}角色守卫示例import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); }}// 创建角色装饰器import { SetMetadata } from '@nestjs/common';export const Roles = (...roles: string[]) => SetMetadata('roles', roles);// 使用角色守卫@Controller('cats')@UseGuards(RolesGuard)export class CatsController { @Get() @Roles('admin') findAll() { return 'This action returns all cats'; }}中间件 vs 守卫中间件的特点在 Express 中间件链中执行可以访问请求和响应对象在路由处理程序之前执行适用于全局逻辑(如日志记录、CORS)不了解路由处理程序守卫的特点在 NestJS 依赖注入系统中执行可以访问 ExecutionContext在中间件之后、拦截器之前执行适用于权限和授权逻辑了解路由处理程序和类选择指南使用中间件:日志记录、请求解析、CORS、压缩等使用守卫:身份验证、授权、权限检查等自定义装饰器从请求中提取用户import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const User = createParamDecorator( (data: string | undefined, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user?.[data] : user; },);// 使用自定义装饰器@Get()findOne(@User('id') userId: string) { return this.catsService.findOne(userId);}JWT 认证守卫示例import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';import { JwtService } from '@nestjs/jwt';@Injectable()export class JwtAuthGuard implements CanActivate { constructor(private jwtService: JwtService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); } try { const payload = await this.jwtService.verifyAsync(token); request['user'] = payload; } catch { throw new UnauthorizedException(); } return true; } private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : undefined; }}最佳实践职责分离:中间件用于全局逻辑,守卫用于权限控制使用装饰器:创建自定义装饰器简化守卫使用错误处理:在守卫中抛出适当的异常性能考虑:避免在守卫中执行耗时操作测试覆盖:为中间件和守卫编写测试文档化:为中间件和守卫添加清晰的文档避免过度使用:只在需要时使用中间件和守卫总结NestJS 中间件和守卫系统提供了:灵活的请求处理机制强大的权限控制能力清晰的关注点分离易于测试和维护的代码结构掌握中间件和守卫是构建安全、可维护的 NestJS 应用程序的关键。中间件处理全局逻辑,守卫处理权限控制,它们共同构成了应用程序的安全和功能基础。
阅读 0·2月17日 22:34

NestJS 微服务和架构如何设计?

微服务架构概述微服务架构是一种将应用程序构建为一组小型、独立服务的方法,每个服务运行在自己的进程中,通过轻量级机制(通常是 HTTP API 或消息队列)进行通信。NestJS 提供了完整的微服务支持,使开发者能够轻松构建可扩展的分布式系统。NestJS 微服务基础安装依赖npm install @nestjs/microservices创建微服务import { NestFactory } from '@nestjs/core';import { Transport, MicroserviceOptions } from '@nestjs/microservices';import { AppModule } from './app.module';async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>( AppModule, { transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877, }, }, ); await app.listen();}bootstrap();混合应用(HTTP + 微服务)import { NestFactory } from '@nestjs/core';import { Transport, MicroserviceOptions } from '@nestjs/microservices';import { AppModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(AppModule); const microservice = app.connectMicroservice<MicroserviceOptions>({ transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877, }, }); await app.startAllMicroservices(); await app.listen(3000);}bootstrap();消息模式1. 消息模式(Message Pattern)import { Controller } from '@nestjs/common';import { EventPattern, Payload, Ctx, RmqContext } from '@nestjs/microservices';@Controller()export class MathController { @MessagePattern({ cmd: 'sum' }) accumulate(@Payload() data: number[]): number { return (data || []).reduce((a, b) => a + b, 0); }}2. 事件模式(Event Pattern)import { Controller } from '@nestjs/common';import { EventPattern, Payload } from '@nestjs/microservices';@Controller()export class NotificationController { @EventPattern('user_created') async handleUserCreated(@Payload() data: Record<string, unknown>) { // 处理用户创建事件 }}传输层1. TCP 传输{ transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877, },}2. Redis 传输{ transport: Transport.REDIS, options: { host: 'localhost', port: 6379, },}3. NATS 传输{ transport: Transport.NATS, options: { url: 'nats://localhost:4222', },}4. MQTT 传输{ transport: Transport.MQTT, options: { url: 'mqtt://localhost:1883', },}5. RabbitMQ 传输{ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'cats_queue', queueOptions: { durable: false, }, },}6. Kafka 传输{ transport: Transport.KAFKA, options: { client: { brokers: ['localhost:9092'], }, consumer: { groupId: 'my-consumer', }, },}客户端(Client)创建客户端代理import { Controller, Get } from '@nestjs/common';import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';@Controller()export class AppController { private client: ClientProxy; constructor() { this.client = ClientProxyFactory.create({ transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877, }, }); } @Get() async getSum() { return this.client.send({ cmd: 'sum' }, [1, 2, 3, 4, 5]); }}使用模块配置客户端import { Module } from '@nestjs/common';import { ClientsModule, Transport } from '@nestjs/microservices';@Module({ imports: [ ClientsModule.register([ { name: 'MATH_SERVICE', transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877, }, }, ]), ], controllers: [AppController],})export class AppModule {}注入客户端import { Controller, Get, Inject } from '@nestjs/common';import { ClientProxy } from '@nestjs/microservices';@Controller()export class AppController { constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {} @Get() async getSum() { return this.client.send({ cmd: 'sum' }, [1, 2, 3, 4, 5]); }}消息确认手动确认import { Controller } from '@nestjs/common';import { EventPattern, Payload, Ctx, RmqContext } from '@nestjs/microservices';@Controller()export class NotificationController { @EventPattern('notification_created') async handleNotificationCreated( @Payload() data: any, @Ctx() context: RmqContext, ) { const channel = context.getChannelRef(); const originalMsg = context.getMessage(); // 处理消息 await this.processNotification(data); // 手动确认消息 channel.ack(originalMsg); }}配置预取计数{ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'notifications_queue', prefetchCount: 10, queueOptions: { durable: false, }, },}微服务架构模式1. API 网关模式import { Controller, Get, Param, Post, Body } from '@nestjs/common';import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';@Controller('api')export class ApiController { private userService: ClientProxy; private orderService: ClientProxy; constructor() { this.userService = ClientProxyFactory.create({ transport: Transport.TCP, options: { host: '127.0.0.1', port: 8877 }, }); this.orderService = ClientProxyFactory.create({ transport: Transport.TCP, options: { host: '127.0.0.1', port: 8878 }, }); } @Get('users/:id') getUser(@Param('id') id: string) { return this.userService.send({ cmd: 'get_user' }, { id }); } @Post('orders') createOrder(@Body() orderData: any) { return this.orderService.send({ cmd: 'create_order' }, orderData); }}2. 事件驱动架构// 订单服务@Controller()export class OrderController { @EventPattern('order_created') async handleOrderCreated(@Payload() order: any) { // 处理订单创建事件 await this.sendNotification(order); await this.updateInventory(order); }}// 通知服务@Controller()export class NotificationController { @EventPattern('order_created') async handleOrderCreated(@Payload() order: any) { // 发送通知 await this.sendEmail(order.userEmail, 'Order created'); }}// 库存服务@Controller()export class InventoryController { @EventPattern('order_created') async handleOrderCreated(@Payload() order: any) { // 更新库存 await this.reduceStock(order.items); }}3. CQRS 模式// Command Handler@Controller()export class OrderCommandController { @MessagePattern({ cmd: 'create_order' }) async createOrder(@Payload() command: CreateOrderCommand) { return this.commandBus.execute(command); }}// Query Handler@Controller()export class OrderQueryController { @MessagePattern({ cmd: 'get_order' }) async getOrder(@Payload() query: GetOrderQuery) { return this.queryBus.execute(query); }}服务发现使用 Consulimport { Module } from '@nestjs/common';import { ClientsModule, Transport } from '@nestjs/microservices';@Module({ imports: [ ClientsModule.register([ { name: 'USER_SERVICE', transport: Transport.TCP, options: { host: 'user-service', port: 3000, }, }, ]), ],})export class AppModule {}使用 Kubernetes Service Discovery{ transport: Transport.TCP, options: { host: process.env.USER_SERVICE_HOST || 'user-service', port: parseInt(process.env.USER_SERVICE_PORT) || 3000, },}分布式追踪集成 OpenTelemetryimport { Controller } from '@nestjs/common';import { MessagePattern, Payload } from '@nestjs/microservices';import { trace } from '@opentelemetry/api';@Controller()export class OrderController { @MessagePattern({ cmd: 'create_order' }) async createOrder(@Payload() data: any) { const tracer = trace.getTracer('order-service'); const span = tracer.startSpan('create_order'); try { const result = await this.orderService.create(data); span.end(); return result; } catch (error) { span.recordException(error); span.end(); throw error; } }}最佳实践服务边界:明确定义服务边界和职责异步通信:使用异步消息进行服务间通信幂等性:确保操作是幂等的,可以安全重试错误处理:实现适当的错误处理和重试机制监控:实施全面的监控和日志记录配置管理:使用配置中心管理服务配置版本控制:实现 API 版本控制策略测试:编写集成测试和契约测试总结NestJS 微服务和架构提供了:完整的微服务支持多种传输层选择灵活的消息模式强大的客户端代理易于构建分布式系统掌握 NestJS 微服务架构是构建可扩展、可维护的企业级应用的关键。通过合理使用微服务模式、事件驱动架构和最佳实践,可以构建出高性能、可靠的分布式系统。微服务架构使团队能够独立开发、部署和扩展各个服务,提高开发效率和系统弹性。
阅读 0·2月17日 22:33

NestJS 拦截器和异常过滤器的作用是什么?

拦截器(Interceptor)的概念拦截器是使用 @Injectable() 装饰器装饰并实现 NestInterceptor 接口的类。拦截器具有一系列有用的功能:在方法执行之前/之后绑定额外的逻辑转换从函数返回的结果转换从函数抛出的异常扩展基本函数行为根据所选条件完全重写函数(例如,用于缓存目的)拦截器的基本结构import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';@Injectable()export class TransformInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( map(data => ({ data, timestamp: new Date().toISOString() })) ); }}使用拦截器在控制器上使用拦截器@Controller('cats')@UseInterceptors(TransformInterceptor)export class CatsController {}在方法上使用拦截器@Controller('cats')export class CatsController { @Get() @UseInterceptors(TransformInterceptor) findAll() { return this.catsService.findAll(); }}全局拦截器import { Module } from '@nestjs/common';import { APP_INTERCEPTOR } from '@nestjs/core';import { TransformInterceptor } from './common/interceptors/transform.interceptor';@Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: TransformInterceptor, }, ],})export class AppModule {}常见拦截器用例1. 日志拦截器import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';import { Observable } from 'rxjs';import { tap } from 'rxjs/operators';@Injectable()export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const { method, url, body } = request; const now = Date.now(); this.logger.log(`Incoming request: ${method} ${url}`); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const delay = Date.now() - now; this.logger.log(`Outgoing response: ${statusCode} - ${delay}ms`); }), ); }}2. 缓存拦截器import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable, of } from 'rxjs';@Injectable()export class CacheInterceptor implements NestInterceptor { private cache = new Map(); intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const cacheKey = request.url; if (this.cache.has(cacheKey)) { return of(this.cache.get(cacheKey)); } return next.handle().pipe( tap(response => { this.cache.set(cacheKey, response); }), ); }}3. 超时拦截器import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';import { Observable, throwError, TimeoutError } from 'rxjs';import { catchError, timeout } from 'rxjs/operators';@Injectable()export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( timeout(5000), catchError(err => { if (err instanceof TimeoutError) { return throwError(() => new RequestTimeoutException()); } return throwError(() => err); }), ); }}4. 响应转换拦截器import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';export interface Response<T> { success: boolean; data: T; message: string; timestamp: string;}@Injectable()export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { return next.handle().pipe( map(data => ({ success: true, data, message: 'Success', timestamp: new Date().toISOString(), })), ); }}异常过滤器(Exception Filter)的概念异常过滤器是使用 @Catch() 装饰器装饰的类,用于捕获和处理应用程序中抛出的异常。它们允许你完全控制异常处理流程,包括响应格式、状态码等。异常过滤器的基本结构import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';@Catch(HttpException)export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: exception.message, }); }}使用异常过滤器在方法上使用异常过滤器@Post()@UseFilters(HttpExceptionFilter)create(@Body() createCatDto: CreateCatDto) { return this.catsService.create(createCatDto);}在控制器上使用异常过滤器@Controller('cats')@UseFilters(HttpExceptionFilter)export class CatsController {}全局异常过滤器import { Module } from '@nestjs/common';import { APP_FILTER } from '@nestjs/core';import { HttpExceptionFilter } from './common/filters/http-exception.filter';@Module({ providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter, }, ],})export class AppModule {}常见异常过滤器1. 全局异常过滤器import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';import { Request, Response } from 'express';@Catch()export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest<Request>(); const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message, }); }}2. 自定义异常import { HttpException, HttpStatus } from '@nestjs/common';export class BusinessException extends HttpException { constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { super( { statusCode: status, message, error: 'Business Error', }, status, ); }}// 使用自定义异常@Get()findAll() { if (someCondition) { throw new BusinessException('Invalid operation'); } return this.catsService.findAll();}3. 验证异常过滤器import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';import { ValidationError } from 'class-validator';@Catch(BadRequestException)export class ValidationExceptionFilter implements ExceptionFilter { catch(exception: BadRequestException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); const exceptionResponse = exception.getResponse(); let errors = []; if (typeof exceptionResponse === 'object' && 'message' in exceptionResponse) { errors = (exceptionResponse as any).message; } response.status(status).json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: 'Validation failed', errors, }); }}执行顺序NestJS 中各种处理器的执行顺序如下:中间件(Middleware)守卫(Guards)拦截器前(Interceptors before)管道(Pipes)控制器方法(Controller method)拦截器后(Interceptors after)异常过滤器(Exception filters)最佳实践拦截器最佳实践单一职责:每个拦截器只负责一个功能性能考虑:避免在拦截器中执行耗时操作错误处理:在拦截器中适当处理错误测试覆盖:为拦截器编写测试文档化:为拦截器添加清晰的文档异常过滤器最佳实践统一响应格式:使用统一的异常响应格式日志记录:在异常过滤器中记录错误日志敏感信息:避免在响应中暴露敏感信息自定义异常:创建自定义异常类来表示业务错误全局过滤器:使用全局异常过滤器处理未捕获的异常总结NestJS 拦截器和异常过滤器系统提供了:强大的请求/响应处理能力灵活的异常处理机制清晰的关注点分离易于扩展和定制完整的请求生命周期控制掌握拦截器和异常过滤器是构建健壮、可维护的 NestJS 应用程序的关键。拦截器用于处理请求/响应的横切关注点,异常过滤器用于统一处理错误,它们共同构成了应用程序的错误处理和响应转换基础。
阅读 0·2月17日 22:32