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

面试题手册

NestJS 部署和 DevOps 如何实现?

NestJS 部署和 DevOps 详解部署概述部署是将应用程序从开发环境转移到生产环境的过程。NestJS 应用程序可以通过多种方式部署,包括传统服务器、容器化部署、云服务等。1. Docker 容器化创建 Dockerfile# 构建阶段FROM node:18-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build# 生产阶段FROM node:18-alpine AS runnerWORKDIR /appENV NODE_ENV productionCOPY package*.json ./RUN npm ci --only=productionCOPY --from=builder /app/dist ./distEXPOSE 3000CMD ["node", "dist/main.js"]创建 .dockerignorenode_modulesdist.git.env*.log构建 Docker 镜像docker build -t nestjs-app .运行 Docker 容器docker run -p 3000:3000 nestjs-app2. Docker Composedocker-compose.ymlversion: '3.8'services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_HOST=db - DATABASE_PORT=3306 - DATABASE_USER=root - DATABASE_PASSWORD=password - DATABASE_NAME=nestjs depends_on: - db restart: unless-stopped db: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=nestjs ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql restart: unless-stoppedvolumes: mysql_data:启动服务docker-compose up -d3. Kubernetes 部署Deployment 配置apiVersion: apps/v1kind: Deploymentmetadata: name: nestjs-appspec: replicas: 3 selector: matchLabels: app: nestjs-app template: metadata: labels: app: nestjs-app spec: containers: - name: nestjs-app image: nestjs-app:latest ports: - containerPort: 3000 env: - name: NODE_ENV value: "production" - name: DATABASE_HOST valueFrom: secretKeyRef: name: db-secret key: host resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 5Service 配置apiVersion: v1kind: Servicemetadata: name: nestjs-app-servicespec: selector: app: nestjs-app ports: - protocol: TCP port: 80 targetPort: 3000 type: LoadBalancerIngress 配置apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: nestjs-app-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: /spec: rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: nestjs-app-service port: number: 804. CI/CD 管道GitHub Actions 配置name: CI/CD Pipelineon: push: branches: [ main, develop ] pull_request: branches: [ main ]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm run test - name: Run lint run: npm run lint - name: Build run: npm run build build-and-push: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: username/nestjs-app:latest,username/nestjs-app:${{ github.sha }} cache-from: type=registry,ref=username/nestjs-app:latest cache-to: type=inline deploy: needs: build-and-push runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - name: Deploy to Kubernetes uses: azure/k8s-deploy@v4 with: manifests: | k8s/deployment.yaml k8s/service.yaml images: | username/nestjs-app:${{ github.sha }} kubeconfig: ${{ secrets.KUBE_CONFIG }}GitLab CI 配置stages: - test - build - deployvariables: NODE_ENV: testtest: stage: test image: node:18 script: - npm ci - npm run test - npm run lint cache: paths: - node_modules/build: stage: build image: docker:latest services: - docker:dind script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - maindeploy: stage: deploy image: bitnami/kubectl:latest script: - kubectl set image deployment/nestjs-app nestjs-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main5. 环境变量管理使用 .env 文件# .env.productionNODE_ENV=productionPORT=3000DATABASE_HOST=localhostDATABASE_PORT=3306DATABASE_USER=rootDATABASE_PASSWORD=passwordDATABASE_NAME=nestjsJWT_SECRET=your-secret-key使用 ConfigModuleimport { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: `.env.${process.env.NODE_ENV}`, }), ],})export class AppModule {}Kubernetes SecretsapiVersion: v1kind: Secretmetadata: name: db-secrettype: Opaquedata: host: bG9jYWxob3N0 port: MzMwNg== user: cm9vdA== password: cGFzc3dvcmQ=6. 健康检查健康检查端点import { Controller, Get } from '@nestjs/common';import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';@Controller('health')export class HealthController { constructor( private health: HealthCheckService, private db: TypeOrmHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ () => this.db.pingCheck('database'), ]); }}Kubernetes 健康检查livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 37. 日志管理结构化日志import { Logger } from '@nestjs/common';export class AppService { private readonly logger = new Logger(AppService.name); async getData() { this.logger.log('Fetching data', { userId: 123, action: 'fetch' }); // 业务逻辑 }}使用 Winstonimport { WinstonModule } from 'nest-winston';import * as winston from 'winston';@Module({ imports: [ WinstonModule.forRoot({ transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp(), winston.format.json(), ), }), new winston.transports.File({ filename: 'logs/error.log', level: 'error', }), new winston.transports.File({ filename: 'logs/combined.log', }), ], }), ],})export class AppModule {}8. 监控和告警使用 Prometheusimport { Controller, Get } from '@nestjs/common';import { MetricsService } from './metrics.service';@Controller('metrics')export class MetricsController { constructor(private metricsService: MetricsService) {} @Get() getMetrics() { return this.metricsService.getMetrics(); }}Grafana 仪表板apiVersion: v1kind: ConfigMapmetadata: name: grafana-dashboarddata: dashboard.json: | { "dashboard": { "title": "NestJS Application", "panels": [ { "title": "Request Rate", "targets": [ { "expr": "rate(http_requests_total[5m])" } ] } ] } }9. 负载均衡Nginx 配置upstream nestjs_backend { server nestjs-app-1:3000; server nestjs-app-2:3000; server nestjs-app-3:3000;}server { listen 80; server_name api.example.com; location / { proxy_pass http://nestjs_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}AWS ALB 配置Resources: LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: nestjs-alb Subnets: - !Ref SubnetA - !Ref SubnetB SecurityGroups: - !Ref SecurityGroup Type: application TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: nestjs-tg Port: 3000 Protocol: HTTP VpcId: !Ref VPC Targets: - Id: !Ref EC2Instance1 - Id: !Ref EC2Instance210. 灾难恢复数据库备份#!/bin/bash# backup.shDATE=$(date +%Y%m%d_%H%M%S)BACKUP_DIR="/backups"DATABASE="nestjs"mysqldump -u root -p$MYSQL_ROOT_PASSWORD $DATABASE | gzip > $BACKUP_DIR/db_backup_$DATE.sql.gz# 保留最近7天的备份find $BACKUP_DIR -name "db_backup_*.sql.gz" -mtime +7 -delete自动扩展apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: nestjs-hpaspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nestjs-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80部署最佳实践环境隔离:开发、测试、生产环境分离自动化部署:使用 CI/CD 自动化部署流程版本控制:所有配置文件纳入版本控制监控告警:实施全面的监控和告警机制备份策略:定期备份关键数据安全加固:实施安全最佳实践文档化:维护详细的部署文档回滚计划:准备快速回滚方案性能测试:部署前进行性能测试渐进式发布:使用蓝绿部署或金丝雀发布总结NestJS 部署和 DevOps 提供了:灵活的容器化方案强大的编排支持自动化的 CI/CD 流程完善的监控体系可靠的灾难恢复掌握部署和 DevOps 是将 NestJS 应用程序成功交付到生产环境的关键。通过合理使用容器化、编排、CI/CD 和监控工具,可以构建出高可用、可扩展的生产环境,确保应用程序的稳定运行和快速迭代。
阅读 0·2月21日 17:11

NestJS 性能优化有哪些方法?

NestJS 性能优化详解性能优化概述性能优化是构建高性能 NestJS 应用程序的关键。通过合理的架构设计、代码优化和资源管理,可以显著提升应用程序的响应速度和吞吐量。1. 数据库优化查询优化使用索引@Entity('users')export class User { @PrimaryGeneratedColumn() id: number; @Index() @Column() email: string; @Index() @Column() username: string; @Column() name: string;}避免 N+1 查询// 不好的方式 - N+1 查询async getUsersWithOrders() { const users = await this.userRepository.find(); for (const user of users) { user.orders = await this.orderRepository.find({ where: { userId: user.id } }); } return users;}// 好的方式 - 使用 JOINasync getUsersWithOrders() { return this.userRepository.find({ relations: ['orders'], });}使用分页async findAll(page: number = 1, limit: number = 10) { const [data, total] = await this.userRepository.findAndCount({ skip: (page - 1) * limit, take: limit, }); return { data, total, page, totalPages: Math.ceil(total / limit), };}连接池配置TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'test', entities: [User], extra: { connectionLimit: 20, // 根据服务器配置调整 },})2. 缓存策略使用 Redis 缓存import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';@Injectable()export class UserService { constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} async findOne(id: number) { const cacheKey = `user_${id}`; const cachedUser = await this.cacheManager.get(cacheKey); if (cachedUser) { return cachedUser; } const user = await this.userRepository.findOne({ where: { id } }); await this.cacheManager.set(cacheKey, user, 3600); // 缓存 1 小时 return user; }}HTTP 缓存import { Controller, Get, Header } from '@nestjs/common';@Controller('users')export class UsersController { @Get() @Header('Cache-Control', 'public, max-age=300') // 缓存 5 分钟 findAll() { return this.usersService.findAll(); }}拦截器缓存import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';import { Observable } from 'rxjs';import { tap } from 'rxjs/operators';@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(data => { this.cache.set(cacheKey, data); }), ); }}3. 异步处理使用异步/等待// 好的方式async findAll() { return this.userRepository.find();}// 不好的方式 - 同步阻塞findAll() { return this.userRepository.findSync();}并行处理// 不好的方式 - 串行执行async getUserData(userId: number) { const user = await this.userRepository.findOne({ where: { id: userId } }); const orders = await this.orderRepository.find({ where: { userId } }); const notifications = await this.notificationRepository.find({ where: { userId } }); return { user, orders, notifications };}// 好的方式 - 并行执行async getUserData(userId: number) { const [user, orders, notifications] = await Promise.all([ this.userRepository.findOne({ where: { id: userId } }), this.orderRepository.find({ where: { userId } }), this.notificationRepository.find({ where: { userId } }), ]); return { user, orders, notifications };}使用队列处理耗时任务import { Injectable } from '@nestjs/common';import { InjectQueue } from '@nestjs/bull';import { Queue } from 'bull';@Injectable()export class EmailService { constructor(@InjectQueue('email') private emailQueue: Queue) {} async sendEmail(to: string, subject: string, body: string) { await this.emailQueue.add('send-email', { to, subject, body }); }}4. 压缩启用 Gzip 压缩import compression from 'compression';async function bootstrap() { const app = await NestFactory.create(AppModule); app.use(compression()); await app.listen(3000);}配置压缩级别app.use(compression({ level: 6, // 压缩级别 1-9 threshold: 1024, // 只压缩大于 1KB 的响应}));5. 静态资源优化使用 CDNimport { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';async function bootstrap() { const app = await NestFactory.create(AppModule); // 配置静态资源使用 CDN app.useStaticAssets('public', { prefix: '/static/', cacheControl: true, maxAge: 31536000, // 1 年 }); await app.listen(3000);}图片优化import { Controller, Get, Param, Res } from '@nestjs/common';import { Response } from 'express';import sharp from 'sharp';@Controller('images')export class ImageController { @Get(':filename') async getImage(@Param('filename') filename: string, @Res() res: Response) { const image = sharp(`./public/images/${filename}`); // 根据请求参数优化图片 const width = parseInt(req.query.width) || 800; const height = parseInt(req.query.height) || 600; image .resize(width, height) .jpeg({ quality: 80 }) .pipe(res); }}6. 代码优化使用懒加载模块@Module({ imports: [ // 懒加载模块 UsersModule, OrdersModule, ],})export class AppModule {}避免不必要的计算// 不好的方式async processUsers(users: User[]) { return users.map(user => { const expensiveResult = this.expensiveCalculation(user); return { ...user, result: expensiveResult }; });}// 好的方式 - 使用缓存async processUsers(users: User[]) { const cache = new Map(); return users.map(user => { const cacheKey = user.id; if (!cache.has(cacheKey)) { cache.set(cacheKey, this.expensiveCalculation(user)); } return { ...user, result: cache.get(cacheKey) }; });}使用流处理大数据import { Controller, Get, StreamableFile } from '@nestjs/common';import { createReadStream } from 'fs';@Controller('files')export class FileController { @Get('download/:filename') downloadFile(@Param('filename') filename: string): StreamableFile { const file = createReadStream(`./files/${filename}`); return new StreamableFile(file); }}7. 监控和分析性能监控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 now = Date.now(); const request = context.switchToHttp().getRequest(); return next.handle().pipe( tap(() => { const duration = Date.now() - now; this.logger.log( `${request.method} ${request.url} - ${duration}ms`, ); }), ); }}使用 APM 工具import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { Agent } from '@elastic/apm-node';const apm = new Agent({ serviceName: 'nestjs-app', serverUrl: 'http://localhost:8200',});async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000);}bootstrap();8. 负载均衡使用 PM2 集群模式npm install pm2 -gpm2 start dist/main.js -i max --name nestjs-app配置 PM2// ecosystem.config.jsmodule.exports = { apps: [{ name: 'nestjs-app', script: './dist/main.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000, }, }],};9. 内存优化避免内存泄漏// 不好的方式 - 可能导致内存泄漏@Injectable()export class CacheService { private cache = new Map(); set(key: string, value: any) { this.cache.set(key, value); }}// 好的方式 - 使用 TTL@Injectable()export class CacheService { private cache = new Map(); private ttl = 3600000; // 1 小时 set(key: string, value: any) { this.cache.set(key, { value, expires: Date.now() + this.ttl, }); // 定期清理过期缓存 this.cleanup(); } private cleanup() { const now = Date.now(); for (const [key, data] of this.cache.entries()) { if (data.expires < now) { this.cache.delete(key); } } }}使用对象池@Injectable()export class ObjectPool<T> { private pool: T[] = []; private factory: () => T; constructor(factory: () => T, size: number = 10) { this.factory = factory; for (let i = 0; i < size; i++) { this.pool.push(factory()); } } acquire(): T { return this.pool.pop() || this.factory(); } release(obj: T): void { this.pool.push(obj); }}10. 网络优化使用 HTTP/2import { NestFactory } from '@nestjs/core';import { AppModule } from './app.module';import { NestExpressApplication } from '@nestjs/platform-express';async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); await app.listen(3000);}bootstrap();配置 Keep-Aliveasync function bootstrap() { const app = await NestFactory.create(AppModule); const server = app.getHttpServer(); server.keepAliveTimeout = 65000; server.headersTimeout = 66000; await app.listen(3000);}性能优化最佳实践监控性能:持续监控应用程序性能指标基准测试:建立性能基准并定期测试渐进优化:逐步优化,每次只优化一个方面代码审查:定期进行代码审查以发现性能问题使用缓存:合理使用缓存减少数据库查询异步处理:使用异步和并行处理提高效率资源压缩:启用压缩减少传输数据量负载均衡:使用负载均衡分散请求压力定期清理:定期清理缓存和临时数据文档记录:记录优化过程和结果总结NestJS 性能优化提供了:数据库查询优化多种缓存策略异步处理能力资源压缩技术监控和分析工具掌握性能优化是构建高性能 NestJS 应用程序的关键。通过合理应用各种优化技术和最佳实践,可以显著提升应用程序的性能、响应速度和用户体验。性能优化是一个持续的过程,需要根据实际应用场景不断调整和改进。
阅读 0·2月21日 17:11

NestJS GraphQL 如何集成?

NestJS GraphQL 集成详解GraphQL 概述GraphQL 是一种用于 API 的查询语言和运行时环境。NestJS 通过 @nestjs/graphql 包提供了完整的 GraphQL 支持,使开发者能够构建灵活、高效的 GraphQL API。安装依赖npm install @nestjs/graphql graphql apollo-server-expressnpm install -D @types/graphql基本配置配置 GraphQL 模块import { Module } from '@nestjs/common';import { GraphQLModule } from '@nestjs/graphql';import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';import { join } from 'path';@Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), sortSchema: true, playground: true, introspection: true, context: ({ req }) => ({ req }), }), ],})export class AppModule {}定义 Schema使用装饰器定义 Schemaimport { Field, Int, ObjectType, Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';import { User } from './entities/user.entity';@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field() email: string; @Field(() => Int) age: number; @Field() isActive: boolean; @Field() createdAt: Date; @Field() updatedAt: Date;}定义输入类型import { InputType, Field } from '@nestjs/graphql';@InputType()export class CreateUserInput { @Field() name: string; @Field() email: string; @Field() password: string; @Field(() => Int, { nullable: true }) age?: number;}@InputType()export class UpdateUserInput { @Field(() => ID) id: number; @Field({ nullable: true }) name?: string; @Field({ nullable: true }) email?: string; @Field(() => Int, { nullable: true }) age?: number;}创建 Resolver基本 Resolverimport { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';import { UsersService } from './users.service';import { User } from './entities/user.entity';import { CreateUserInput, UpdateUserInput } from './dto/user.input';@Resolver(() => User)export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => [User]) async users(): Promise<User[]> { return this.usersService.findAll(); } @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { return this.usersService.findOne(id); } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); } @Mutation(() => User, { nullable: true }) async updateUser(@Args('input') input: UpdateUserInput): Promise<User> { return this.usersService.update(input.id, input); } @Mutation(() => Boolean) async deleteUser(@Args('id', { type: () => ID }) id: number): Promise<boolean> { return this.usersService.remove(id); }}使用自定义装饰器import { createParamDecorator, ExecutionContext } from '@nestjs/common';export const CurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); return request.user; },);// 在 Resolver 中使用@Mutation(() => User)async createUser( @Args('input') input: CreateUserInput, @CurrentUser() user: any,): Promise<User> { return this.usersService.create(input, user.id);}关系和加载一对一关系@ObjectType()export class Profile { @Field(() => ID) id: number; @Field() bio: string; @Field(() => User) user: User;}@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => Profile, { nullable: true }) profile?: Profile;}一对多关系@ObjectType()export class Post { @Field(() => ID) id: number; @Field() title: string; @Field() content: string; @Field(() => User) author: User;}@ObjectType()export class User { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[];}多对多关系@ObjectType()export class Tag { @Field(() => ID) id: number; @Field() name: string; @Field(() => [Post]) posts: Post[];}@ObjectType()export class Post { @Field(() => ID) id: number; @Field() title: string; @Field(() => [Tag]) tags: Tag[];}分页基于游标的分页import { ArgsType, Field, Int } from '@nestjs/graphql';@ArgsType()export class PaginationArgs { @Field(() => Int, { nullable: true }) first?: number; @Field(() => String, { nullable: true }) after?: string;}@ObjectType()export class PaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Boolean) hasNextPage: boolean; @Field(() => String, { nullable: true }) endCursor?: string;}@Resolver(() => User)export class UsersResolver { @Query(() => PaginatedUsers) async users(@Args() pagination: PaginationArgs): Promise<PaginatedUsers> { const { data, hasNextPage, endCursor } = await this.usersService.paginate(pagination); return { data, hasNextPage, endCursor }; }}基于偏移量的分页@ArgsType()export class OffsetPaginationArgs { @Field(() => Int, { nullable: true, defaultValue: 0 }) skip?: number; @Field(() => Int, { nullable: true, defaultValue: 10 }) take?: number;}@ObjectType()export class OffsetPaginatedUsers { @Field(() => [User]) data: User[]; @Field(() => Int) total: number; @Field(() => Int) page: number; @Field(() => Int) totalPages: number;}@Resolver(() => User)export class UsersResolver { @Query(() => OffsetPaginatedUsers) async users(@Args() pagination: OffsetPaginationArgs): Promise<OffsetPaginatedUsers> { const { data, total, page, totalPages } = await this.usersService.paginate(pagination); return { data, total, page, totalPages }; }}订阅(Subscriptions)配置订阅import { PubSub } from 'graphql-subscriptions';@Module({ imports: [ GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, installSubscriptionHandlers: true, context: ({ req, connection }) => { if (connection) { return { req: connection.context }; } return { req }; }, }), ],})export class AppModule {}创建订阅 Resolverimport { Resolver, Subscription, Root } from '@nestjs/graphql';import { PubSub } from 'graphql-subscriptions';@ObjectType()export class Message { @Field(() => ID) id: number; @Field() content: string; @Field() createdAt: Date;}@Resolver(() => Message)export class MessagesResolver { private pubSub: PubSub; constructor() { this.pubSub = new PubSub(); } @Subscription(() => Message, { filter: (payload, variables) => { return payload.chatId === variables.chatId; }, }) messageAdded(@Root() message: Message, @Args('chatId') chatId: number): Message { return message; } async publishMessage(message: Message) { await this.pubSub.publish('messageAdded', { messageAdded: message }); }}数据加载器(DataLoader)创建 DataLoaderimport { DataLoader } from 'dataloader';import { UsersService } from './users.service';export class UserDataLoader { private loader: DataLoader<number, User>; constructor(private usersService: UsersService) { this.loader = new DataLoader(async (ids) => { const users = await this.usersService.findByIds(ids); return ids.map(id => users.find(user => user.id === id)); }); } async load(id: number): Promise<User> { return this.loader.load(id); } async loadMany(ids: number[]): Promise<User[]> { return this.loader.loadMany(ids); }}在 Resolver 中使用 DataLoaderimport { Resolver, Query, Args, ID, Parent } from '@nestjs/graphql';import { UserDataLoader } from './user-dataloader';@Resolver(() => Post)export class PostsResolver { constructor(private userDataLoader: UserDataLoader) {} @Query(() => [Post]) async posts(): Promise<Post[]> { return this.postsService.findAll(); } @FieldResolver(() => User) async author(@Parent() post: Post): Promise<User> { return this.userDataLoader.load(post.authorId); }}验证和转换使用 class-validatorimport { IsEmail, IsString, MinLength } from 'class-validator';@InputType()export class CreateUserInput { @Field() @IsEmail() email: string; @Field() @IsString() @MinLength(6) password: string; @Field() @IsString() name: string;}自定义验证器import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';export function IsStrongPassword(validationOptions?: ValidationOptions) { return function (object: Object, propertyName: string) { registerDecorator({ name: 'isStrongPassword', target: object.constructor, propertyName: propertyName, options: validationOptions, validator: { validate(value: any) { const hasUpperCase = /[A-Z]/.test(value); const hasLowerCase = /[a-z]/.test(value); const hasNumber = /[0-9]/.test(value); return hasUpperCase && hasLowerCase && hasNumber; }, defaultMessage(args: ValidationArguments) { return 'Password must contain uppercase, lowercase, and numbers'; }, }, }); };}@InputType()export class CreateUserInput { @Field() @IsStrongPassword() password: string;}错误处理自定义错误类import { ApolloError } from 'apollo-server-express';export class UserNotFoundError extends ApolloError { constructor(id: number) { super(`User with ID ${id} not found`, 'USER_NOT_FOUND', { id, }); }}export class ValidationError extends ApolloError { constructor(message: string, fields: string[]) { super(message, 'VALIDATION_ERROR', { fields }); }}在 Resolver 中使用错误@Resolver(() => User)export class UsersResolver { constructor(private usersService: UsersService) {} @Query(() => User, { nullable: true }) async user(@Args('id', { type: () => ID }) id: number): Promise<User> { const user = await this.usersService.findOne(id); if (!user) { throw new UserNotFoundError(id); } return user; } @Mutation(() => User) async createUser(@Args('input') input: CreateUserInput): Promise<User> { try { return await this.usersService.create(input); } catch (error) { throw new ValidationError('Invalid input', error.fields); } }}权限控制创建权限守卫import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';import { GqlExecutionContext } from '@nestjs/graphql';@Injectable()export class GqlAuthGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const ctx = GqlExecutionContext.create(context); const { req } = ctx.getContext(); if (!req.user) { throw new UnauthorizedException(); } return true; }}使用权限守卫import { Resolver, Mutation, UseGuards } from '@nestjs/graphql';import { GqlAuthGuard } from './guards/gql-auth.guard';@Resolver(() => User)export class UsersResolver { @Mutation(() => User) @UseGuards(GqlAuthGuard) async createUser(@Args('input') input: CreateUserInput): Promise<User> { return this.usersService.create(input); }}性能优化查询复杂度分析import { GraphQLModule } from '@nestjs/graphql';import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ queryComplexity({ maximumComplexity: 1000, variables: {}, onComplete: (complexity) => { console.log(`Query complexity: ${complexity}`); }, }), ],})查询深度限制import { depthLimit } from 'graphql-depth-limit';GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, validationRules: [ depthLimit(5), ],})最佳实践Schema 优先:优先使用 Schema 优先的方法类型安全:充分利用 TypeScript 的类型系统分页:始终实现分页以避免大量数据传输数据加载器:使用 DataLoader 解决 N+1 查询问题错误处理:提供清晰的错误信息权限控制:实现适当的权限控制机制性能监控:监控查询性能和复杂度文档化:使用 GraphQL Playground 和文档工具总结NestJS GraphQL 集成提供了:完整的 GraphQL 支持灵活的 Schema 定义强大的类型安全丰富的查询功能易于集成的生态系统掌握 NestJS GraphQL 集成是构建现代、灵活 API 的关键。通过合理使用 GraphQL 的特性,可以构建出高效、类型安全、易于维护的 API,满足前端对数据的精确需求。GraphQL 的查询语言特性使客户端能够精确获取所需数据,减少网络传输和提升性能。
阅读 0·2月21日 17:11

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