NestJs

NestJs

基础概念

  • Module:模块,包含controller、service等,可以引入其余模块,注入service,导出本模块内容;
  • Controller :控制器,用于处理路由,解析请求参数,配置接口地址的地方
  • Service :实现业务逻辑的地方,操作数据库增删改查,返回数据,被controller直接调用
  • Dto :data transfer object,数据传输对象,用于封装请求体里数据的对象,实际就是接口接收的参数ts类型 ,可以在这里配置一些验证条件
  • Entity :对应数据库中的表的实体,这里的字段配置与数据库保持一致,字段类型长度等,创建数据库连接后操作数据库时使用(Repository 对象)
  • Ioc:Inverse of Controller,反转控制、依赖注入,只要声明了依赖,nest会自动注入依赖的实例;
  • Aop :切面编程,在接口相应流程中可以复用的逻辑 ,在module、controller、service阶段插入一些公共或特殊处理逻辑,具体包含middleware、interceotor、guard、exception filter、pipe;

Ioc

IoC 机制是在 class 上标识哪些是可以被注入的,它的依赖是什么,然后从入口开始扫描这些对象和依赖,自动创建和组装对象。

Nest 里通过 @Controller 声明可以被注入的 controller,通过 @Injectable 声明可以被注入也可以注入别的对象的 provider,然后在 @Module 声明的模块里引入。

并且 Nest 还提供了 Module 和 Module 之间的 import,可以引入别的模块的 provider 来注入。

在 main.ts 里调用 NestFactory.create 方法,就会从 AppModule 开始递归解析 Module,实例化其中的 provider、controller,并依次调用它们的 onModuleInit 生命周期方法。

之后会再递归调用每个 Module 的 provider、controller 的还有 Module 自身的 onApplicationBootstrap 生命周期方法。

Aop

Nest 基于 express 这种 http 平台做了一层封装,应用了 MVC、IOC、AOP 等架构思想。

MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。

IOC 是指 Nest 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。

AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。

Nest 的 Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。

Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。

Nest 就是通过这种 AOP 的架构方式,实现了松耦合、易于维护和扩展的架构。

执行顺序:

request => Middleware => Gurad=> Interceptor before => Pipe => handler => Interceptor after => Exception filter => response

  1. 请求会被 middleware 处理,这一层可以复用 express 的中间件生态,实现 session、static files 等功能;可middleware可以注入 provider
  2. 在具体的路由会经历 Guard 的处理,它可以通过 ExecutionContext 拿到目标 class、handler 的metadata 等信息,可以实现权限验证等功能;
  3. Interceptor 可以在请求前后做一些处理,它也可以通过 ExecutionContext 拿到 class、handler 的信息;
  4. 在到达 handler 之前,还会对参数用 Pipe 做下检验和转换;
  5. 这个过程中不管是哪一层抛的异常,都会被 Exception Filter 处理下,返回给用户友好的响应信息;

Nest CLI

bash 复制代码
npx install -g @nest/cli

nest new 项目名

nest -h

middleware中间件

Middleware是在路由处理程序之前 调用的函数。中间件函数可以访问请求响应对象,以及应用程序的请求-响应周期中的next()中间件函数。

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前中间件函数未结束请求-响应周期,则必须调用next()将控制权传递给下一个中间件函数。否则,请求将被搁置。

中间件需要实现 NestMiddleware 接口

ts 复制代码
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...');
    next();
  }
}


// 函数式中间件 - 当中间件不需要任何依赖时,可以考虑使用更简单的 函数式中间件 替代方案。
export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};

在应用中使用中间件

ts 复制代码
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './logger.middleware';
import { UserMiddleware } from './user.middleware';

@Module()
export class AppModule implements NestModule {
  // configure()方法可以使用async/await进行异步化
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(UserMiddleware, LoggerMiddleware) // 多个中间件, 用逗号隔开
      .exclude(
        { path: 'user', method: RequestMethod.POST }, // 排除user路由的POST请求
      )
      .forRoutes({ path: 'user', method: RequestMethod.GET }); // 针对user路由的GET请求使用中间件, path可以使用通配符, 例如 us*er, 将会匹配usaer, us_er, user等
    // .forRoutes(UserController); // 针对UserController的所有路由使用中间件
  }
}

全局使用中间件

ts 复制代码
import { logger } from './logger.middleware';
app.use(logger);

装饰器

@Module, @Global

  • Module:声明模块;
  • Global:标注此模块为全局模块,他的exports可以不引入直接注入了;
ts 复制代码
import { Module } from '@nestjs/common';

@Global()
@Module({
  // 引入其他模块
  imports: [],
  // 本模块的controller
  controller: [],
  // 注入的services
  providers: [],
  // 导出本模块的services给其他模块使用
  exports: []
})
export class AppModule {}

当遇见 A 模块 引入了 B模块,B模块 也引入了 A模块,形成了循环依赖,会出现找不到依赖的情况;

需要使用 forwardRef

ts 复制代码
/* A Module */
import { Module, forwardRef } from '@nestjs/common';
import { BModule } from '../B.module'
@Module({
  imports: [
    forwardRef(() => BModule)
  ]
})
export class AModule {}

/* B Module */
import { Module, forwardRef } from '@nestjs/common';
import { AModule } from '../A.module'
@Module({
  imports: [
    forwardRef(() => AModule)
  ]
})
export class BModule {}

@controller, @Injectable, @Inject

  • controller:声明controller控制器,路由配置;
  • Injectable:声明provider提供者,可以被注入,可通过 Inject 引入使用;
  • Inject:注入Injectable提供的依赖;

同模块内的 controller 和 Injectable 声明的内容, 记得在 @Module 那里加入配置;

ts 复制代码
/* app.service.ts */
import { Injectable } from '@nestjs/common';

// 代表这个可以被注入, 使用@Inject引入
@Injectable()
export class AppService {
  getString(): string {
    return 'Hello World!'
  }
}
ts 复制代码
/* app.controller.ts */
import { Controller, Inject, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  // 构造器注入
  /* constructor(
  	private readonly appService: AppService
  ) {} */
  
  // 属性注入
  @Inject(AppService)
  private readonly appService: AppService;
  
  @Get()
  getHello(): string {
    return this.appService.getString();
  }
}

接口参数类

@Param

Param:get请求参数,url地址栏后面的字符串, 比如 url/get/:id 中的 id

ts 复制代码
@Get(':id')
findOne(@Param('id') id: string) {
  return this.taskService.findOne(+id);
}

@Get('ids/:ids')
getUserByIds(
  @Param(
    'ids',
    new ParseIntPipe({
      errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
    }),
  )
  ids: number,
) {
  return {
    ids,
  };
}
@Query

get请求参数,拼接在地址栏上的参数,比如 /aaa?name=xx 中的 name

ts 复制代码
@Get('users')
// 通过指定参数名称获取, 可以添加参数处理管道
findUser(@Query('name') name: string,  @Query('age', ParseIntPipe) age: string) {
  return {}
}

@Get('users')
// 不指定参数,获取所有query参数
// findUser(@Query() query: UserDto) {
findUser(@Query() query: { name: string, age: string }) {
  return {}
}
@Body

post,put等请求参数,放在body请求体中,通过 dto class接收

ts 复制代码
@Post('create')
async create(@Body() book: CreateBookDto) {
  return this.bookService.create(book);
}

@Post('create')
// 提取单个字段, 也可以使用管道
async create(@Body('name') name: name) {
  return this.bookService.create(name);
}

接口请求方式

@Get, @Post, @Put, @Delete, @Patch, Options, @Head
ts 复制代码
import {
  Body,
  Controller,
  Delete,
  forwardRef,
  Get,
  Inject,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { UserService } from '../user/user.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { TaskService } from './task.service';
// import { FileSizePipe } from '../../hook/file-size.pipe';

@Controller('task')
export class TaskController {
  constructor(
    private readonly taskService: TaskService,
    // 循环依赖, 使用Inject + forwardRef
    @Inject(forwardRef(() => UserService)) private userService: UserService,
  ) {}

  @Post()
  create(@Body() createTaskDto: CreateTaskDto) {
    return this.taskService.create(createTaskDto);
  }

  @Get()
  findAll() {
    return this.taskService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.taskService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
    return this.taskService.update(+id, updateTaskDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.taskService.remove(+id);
  }
}

接口响应类

@HttpCode

指定接口的返回状态码

ts 复制代码
@Get('all')
@HttpCode(200)
async findAll() {
  return this.bookService.findAll();
}

@Post('create')
@HttpCode(201) // nest中的post成功状态码是201
async create(@Body() book: CreateBookDto) {
  return this.bookService.create(book);
}
@Req, @Request
@Res, @Response

@Req@Res装饰器用于直接访问Express.js的请求(Request)和响应(Response)对象。虽然NestJS提供了许多内置装饰器(如@Body@Query等)来简化请求处理,但在某些情况下,你可能需要直接操作原始的请求或响应对象。

注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了, 需要手动 send,除非指定 passthrough 为true

ts 复制代码
import { Controller, Get, Req } from '@nestjs/common&';
import { Request } from 'express';

@Controller('example')
export class ExampleController {
  @Get()
  findAll(@Req() request: Request, @Res() response: Response) {
    console.log(request.headers);
    console.log(request.query);
    console.log(request.body);
    // ...
    response.status(200).send('Hello World!');
  }
}

接口其他处理类装饰器

@Version

控制路由接口的版本。

设置了 version 就必须携带版本号了,没带版本号就会访问不到接口;

ts 复制代码
import { Controller, Get, Version, VERSION_NEUTRAL } from '@nestjs/common';

@Controller({
  path: 'user',
  version: '1' // 所以接口默认版本为1, 这里声明了,那么请求接口就必须带版本号
})
export class UserController {
  @Get()
  getUser(':id') {
    
  }
  
  
  @Version('2') // 指定版本为2
  @Get()
  getUserV2() {
    
  }
}

需要在main.ts中开启接口版本功能

ts 复制代码
/* main.ts */
app.enableVersioning({
  type: VersioningType.HEADER, // URI: 在地址上添加版本; CUSTOM: 自定义
  header: 'version', // 指定需要携带的字段名
});

类型:VersioningType

  • URI :在接口地址上添加版本

  • HEADER :在请求头添加字段指定版本

    • 请求头: version : 2
  • MEDIA_TYPE :在 accept 的 header 里携带版本号

    • 请求头: Accept : application/json;version=2
  • CUSTOM :自定义

    • 第二个参数接收为一个函数,能拿到 requeset对象

    ts 复制代码
    const extractor = (request: Request)=> {
      if(request.headers['disable-custom']) {
        return '';
      }
      return request.url.includes('guang') ? '2' : '1';
    }
    
    app.enableVersioning({
      type: VersioningType.CUSTOM,
      extractor
    })

没带版本号 可以使用 VERSION_NEUTRAL 常量,带的版本号是几都能访问,但是只能按代码顺序访问到第一个匹配的接口;

标记为 VERSION_NEUTRAL 后,其他版本的接口需要提出来放到其他的controller中, 例: UserV2Controller , v2版本要放前面

ts 复制代码
/* controller.ts */
import { Controller, Get, Version, VERSION_NEUTRAL } from '@nestjs/common';

@Controller({
  path: 'user',
  version: VERSION_NEUTRAL, // 使用VERSION_NEUTRAL常量,不带版本,所有版本,都可以访问
})
export class UserController {
  @Get()
  getUser(':id') {
    
  }
  
  
  @Version('2') // 指定版本为2
  @Get()
  getUserV2() {
    
  }
}


/* module.ts */
@Module({
  controllers: [UserV2Controller, UserController]
})
export class userModule {}
@SetMetadata

@SetMetadata装饰器用于在类或方法上设置自定义的元数据。这些元数据可以在运行时通过反射机制(如Reflect对象)获取,从而实现自定义的逻辑或行为。@SetMetadata通常与自定义装饰器或守卫(Guards)和拦截器(Interceptors)结合使用。

ts 复制代码
import { SetMetadata } from '@nestjs/common';

// 设置元数据
@SetMetadata('role', 'admin')
@Get('admin')
findAllAdmin() {
  // ...
}

获取自定义元数据 :在守卫、拦截器或其他地方使用Reflect对象获取元数据。

ts 复制代码
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 roles = this.reflector.get<string[]>('role', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    return roles.includes(request.user.role);
  }
}
ts 复制代码
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const logLevel = this.reflector.get<string>('logLevel', context.getHandler());
    console.log(`Logging level: ${logLevel}`);
    return next.handle();
  }
}
@Headers

取出某个或全部请求头

ts 复制代码
import { Controller, Get, Headers } from '@nestjs/common';

@Controller('example')
export class ExampleController {
  @Get()
  findAll(@Headers() headers: Record<string, string | string[]>, @Headers('authorization') authorization: string) {
    console.log(headers);
    // headers 是一个包含所有请求头的对象
  }
}
@Ip

拿到请求的 ip地址

ts 复制代码
@Get('ip')
ip(@Ip() ip: string) {
  // ip 是客户端的IP地址
  console.log(ip);
}
@Session

取出 session 对象,但是需要启用 express-session 中间件

ts 复制代码
@Get('session')
session(@Session() session) {
  console.log(session);
}

安装 中间件

bash 复制代码
npm install express-session

在main.ts中引入并启用

ts 复制代码
import * as session from 'express-session';

app.use(session({
  secret: 'secret', // 加密密钥
  cookie: { maxAge: 10000 } // cookie存活时间
}))

启用之后,调用接口会返回一个 set-cookie 的响应头,设置了 cookie,包含 sid 也就是 sesssionid。

之后每次请求都会自动带上这个 cookie;

@HostParam

取出 host 里的参数

ts 复制代码
import { Controller, Get, HostParam } from '@nestjs/common';

// 只有 host 满足 xx.0.0.1 的时候才会路由到这个 controller。
@Controller({ host: ':host.0.0.1', path: 'aaa' })
export class AaaController {
    @Get('bbb')
    hello(@HostParam('host') host) {
        return host;
    }
}

@Catch, @UseFilters

  • Catch:用于过滤器,处理抛出的未捕获的异常;声明 filter 过滤器处理的 exception 类型,搭配 BaseExceptionFilter 类型使用,需实现一个 catch 方法;
  • UseFilters:使用过滤器,可在 controller 和 handler 层使用;
filter异常过滤器
ts 复制代码
/* http-exception.filter.ts */
import {
  Catch,
  HttpException,
  ArgumentsHost,
  HttpStatus,
} from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { Response, Request } from 'express';

/**
 * @description 异常过滤器, 捕获所有类型的 错误和异常,并返回一个对用户友好的响应。
 */
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // 调用父类的catch方法处理异常
    super.catch(exception, host);

    // const { httpAdapter } = this.httpAdapterHost; // 获取http适配器

    const ctx = host.switchToHttp(); // 获取请求上下文
    const response = ctx.getResponse<Response>(); // 获取响应对象
    const request = ctx.getRequest<Request>(); // 获取请求对象

    /* 判断错误类型是哪一类的,根据不同情况做不同的处理  */
    // 获取异常状态码
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let resData = {};

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      resData = exception.getResponse();
    }

    const resBody = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url, // 请求路径
      // path2: httpAdapter.getRequestUrl(ctx.getRequest()), // 请求路径
      resData: resData, // 响应数据
    };

    if (![HttpStatus.OK, HttpStatus.CREATED].includes(status)) return;
    /* 
      这里可以根据需要,做一些判断,返回不同的错误相应内容
      也可以在这里设置sentry上报错误
    */
    response
      .status(status) // 设置响应状态码
      .json(resBody); // 设置响应内容
  }
}

使用异常过滤器

可以放在handle层单独针对某个方法使用,也可以直接放在 @Controller层,对整个控制器的所有方法都应用

ts 复制代码
/* app.controler.ts */
......
import { AllExceptionsFilter } from './http-exception.filter';
import { UseFilters, HttpException, HttpSatus } from '@nestjs/common';
......

@Get()
@UseFilters(AllExceptionsFilter)
getHello(): string {
  throw new HttpException('filter exception', HttpSatus.BAD_REQUEST);
  // return this.appService.getString();
}

全局使用在 main.ts

ts 复制代码
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AllExceptionsFilter } from './http-exception.filter';

...

const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

@UsePipes

UsePipes:管道,具有转换、验证两个作用,应实现 PipeTransform 接口;

  • 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常

在这两种情况下, 管道 参数(arguments) 会由控制器(controllers)的路由处理程序 进行处理,Nest自带很多开箱即用的内置管道。

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

现在比较少用接口管道,@UsePipes(new ValidationPipe()),直接使用参数管道 @Body(ValidationPipe) body: Dto 更为实用

在 Dto 中使用 class-validate 可以直接添加各类参数校验条件

pipe管道
ts 复制代码
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { ObjectSchema } from 'joi';

/**
 * @description 管道pipe
 * 管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
 * 两个作用:
 * 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
 * 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
 */
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToInstance(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

nest有内置许多管道

ts 复制代码
import { ParseIntPipe, DefaultValuePipe, ValidationPipe, Body } from '@nestjs/common';

@Get(':id')
getUserById(
  // 使用参数管道,DefaultValuePipe 赋值默认值,ParseIntPipe将参数转为int类型,可以继续在后面添加自定义管道
  @Param('id', new DefaultValuePipe('1'), ParseIntPipe) id: number,
) {
  return {
    id,
  };
}

@Post('get')
// new ValidationPipe() 搭配 dto 里面设置的 class-validator, 可以实现body参数类型校验
postList(@Body(new ValidationPipe()) body: Dto) {
  
}

全局使用

ts 复制代码
import { ValidationPipe } from '@nestjs/common';

// 全局管道
app.useGlobalPipes(new ValidationPipe({ transform: true })); // transform 将请求参数转换为对应的dto实体类

@UseGuards

UseGuards:守卫,具有单一职责,用于 权限、角色判断处理指定请求,授权;

守卫在所有中间件之后执行,但在拦截器或管道之前执行。

guard守卫

需要实现 canActivate 方法

ts 复制代码
/* jwt.guard.ts */
import {
  CanActivate,
  ExecutionContext,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';

@Injectable()
export class JwtGuard implements CanActivate {
  @Inject()
  private readonly jwtService: JwtService;

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ');
    // 这里做一些权限,角色判断
    if (!token || token.length < 2) {
      throw new UnauthorizedException('token错误');
    }

    // 用户是否有权限调用
    try {
      const decoded = this.jwtService.verify(token[1]);
      request.user = decoded.user;
      return true;
    } catch {
      throw new UnauthorizedException('token失效');
    }
  }
}

在handle方法使用

ts 复制代码
/* app.controller.ts */
......
import { JwtGuard } from './jwt.guard.ts';
import { UseGuards, HttpException, HttpSatus } from '@nestjs/common';
......

@Get()
@UseGuards(AllExceptionsFilter)
getHello(): string {
  throw new HttpException('filter exception', HttpSatus.BAD_REQUEST);
  // return this.appService.getString();
}

全局使用

ts 复制代码
import { JwtGuard } from './jwt.guard.ts';

// 全局守卫
app.useGlobalGuards(new AuthGuard());

@UseInterceptors

拦截器是使用 @Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)
ts 复制代码
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');

    const now = Date.now();
    return next
      .handle()
      .pipe(
      	// 切面拦截
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      	// 响应拦截,返回新的响应对象
      	// map(value => value === null ? '' : value )
      	map(data => ({ data })) // 加一层data对象
      );
  }
}

由于handle()返回一个RxJS Observable,我们可以选择多种操作符来操控流。在上面的示例中,我们使用了tap()操作符,它在可观察流的正常或异常终止时调用我们的匿名日志函数,但不会干扰响应循环。

ts 复制代码
// 使用拦截器
@UseInterceptors(LoggingInterceptor)
export class UserController {
  
  // handle级别的拦截器
  // @UseInterceptors(LoggingInterceptor)
  getAll() {
    
  }
  
}

在调用 UserController 下的路由时,就会看到 LoggingInterceptor 拦截器中的 console.log()

@Render

指定渲染用的模版引擎

文件上传

通过 Express 的 Multer 包处理 multipart/form-data 类型的请求中的 file;

提供了 FileInterceptor、FilesInterceptor、FileFieldsInterceptor、AnyFilesInterceptor 的拦截器

安装 multer 的 ts 类型包

bash 复制代码
npm i -D @types/multer

Main.ts 开启 express 底层功能,开启跨域,支持静态文件的访问;

ts 复制代码
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WINSTON_LOGGER_TOKEN } from './modules/winston/winston.module';

async function bootstrap() {
  // NestExpressApplication 使用express底层功能
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // 跨域
  app.enableCors();
  
  // 设置uploads为静态文件目录, useStaticAssets 要开启NestExpressApplication
  app.useStaticAssets(join(__dirname, '../uploads'), { prefix: '/uploads' });
  
  await app.listen(1111);
}

bootstrap()

以下文件上传都是将文件存在本地目录,正常需要存到服务器,或是oss 阿里云 腾讯云 对象存储的方式 的,后面再介绍。

单文件上传

通过 FileInterceptor 拦截器解析请求里的 file 字段,第二个参数是 options 配置

ts 复制代码
import {
  Post,
  Body,
  UseInterceptors,
  BadRequestException,
  UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { mkdirSync } from 'fs';
import { extname } from 'path';

const diskStorage = diskStorage({
    // 确定文件上传后存放的文件夹
    destination: function (req, file, cb) {
      try {
        // 创建文件夹, 已存在会报错
        mkdirSync('uploads');
      } catch (e) {
        console.log(e);
      }
      cb(null, 'uploads');
    },
    // 生成文件名
    filename: function (req, file, cb) {
      const uniqieSuffix =
        Date.now() +
        '-' +
        Math.round(Math.random() * 1e9) +
        '-' +
        file.originalname;
      cb(null, uniqieSuffix);
    },
 });

@Post('upload')
@UseInterceptors(
  FileInterceptor('file', {
    dest: 'uploads', // 文件存放目录
    storage: diskStorage,
    limits: { fileSize: 1024 * 1024 * 5 }, // 5MB
    fileFilter(req, file, cb) {
      const extName = extname(file.originalname);
      if (['.png', '.jpg', '.jpeg', '.gif'].includes(extName)) {
        cb(null, true);
      } else {
        cb(new BadRequestException('Only image files are allowed'), false);
      }
    },
  }),
)
async upload(@UploadedFile() file: Express.Multer.File) {
  console.log('file', file);
  return file.path;
}

多文件上传,大文件上传

前端将文件切片上传,分片文件上传完后,调用合并接口将切片文件合并成原始文件

ts 复制代码
const chunkSize = 20 * 1024; // 分片大小
const fileInput = document.querySelector('#fileInput');
async function chunkFile() {
    const file = fileInput.files[0];
    const chunks = []
    let startPos = 0;
    // 将文件切片储存到chunks数组中
    while (startPos < file.size) {
        chunks.push(file.slice(startPos, startPos + chunkSize));
        startPos += chunkSize;
    }
    const task = []
    // 添加随机字符串防止切片文件名重复
    const randomStr = Math.random().toString().slice(2, 8)
    // 将切片上传到服务器
    chunks.map((item, index) => {
        const data = new FormData();
      	// 在file对象添加 分片文件名,文件名携带分片索引,总分片数 
        data.set('name', `${randomStr}_${file.name}-${index}`);
        data.set('total', chunks.length);
        data.append('file', item);
        task.push(axios.post('http://localhost:1111/book/upload-big-files', data))
    })
    await Promise.all(task)
  	// 分片文件上传完后,调用合并接口将切片文件合并成原始文件
    axios.get('http://localhost:1111/book/merge?name=' + randomStr + '_' + file.name)
}

fileInput.onchange = chunkFile;

后端接收传来的多个 file 文件

ts 复制代码
import {
  Post,
  Body,
  UseInterceptors,
  UploadedFiles,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { mkdirSync, existsSync, cpSync, rmSync } from 'fs';
import { extname } from 'path';


@Post('upload-big-files')
@UseInterceptors(FilesInterceptor('file', 20, { dest: 'uploads' }))
async uploadBigFiles(
  // 单文件这里是不带s的
  @UploadedFiles() files: Express.Multer.File[],
  @Body() body,
) {
  console.log('files', files);
  console.log('body', body);

  // 匹配文件名,去掉文件名末尾 '-' 后面的数字部分
  const fileName = body.name.match(/(.+)\-\d+$/)[1];
  // 生成分片文件储存目录
  const chunkDir = 'uploads/chunks_' + fileName;

  // existsSync 检查文件目录是否存在
  if (!existsSync(chunkDir)) {
    mkdirSync(chunkDir);
  }
  // 将分片文件复制到目录中
  cpSync(files[0].path, chunkDir + '/' + body.name);
  // 删除原始上传的分片文件,节省存储空间
  rmSync(files[0].path);
}

合并分片文件接口

ts 复制代码
import {
  Get,
  Query
} from '@nestjs/common';
import { readdirSync, createReadStream, createWriteStream, statSync, rmSync } from 'fs';
import { extname } from 'path';


@Get('merge')
merge(@Query('name') name: string) {
  // 分片文件所在目录
  const chunkDir = 'uploads/chunks_' + name;
  // 读取目录下的文件
  const files = readdirSync(chunkDir);
  let startPos = 0;
  let count = 0;
  // 便利分片文件
  files.map((file) => {
    const filePath = chunkDir + '/' + file;
    const stream = createReadStream(filePath);
    // 将分片文件写入到合并文件中
    stream
      .pipe(
        createWriteStream('uploads/' + name, {
          start: startPos,
        }),
      )
      .on('finish', () => {
        count++;
        if (count === files.length) {
          // 合并完成,删除分片文件目录
          rmSync(chunkDir, { recursive: true, force: true });
        }
      });

    startPos += statSync(filePath).size;
  });
}

OSS

上传文件一般不会直接存在服务器目录下,一般会用阿里云的 OSS,它会自己做弹性扩展,所以存储空间是无限的。

OSS 对象存储是在一个 bucket 桶下,存放多个文件。

它是用 key-value 存储的,没有目录的概念,阿里云 OSS 的目录只是用元信息来模拟实现的。

不管在哪里上传,都需要 acessKeyId 和 acessKeySecret。

这个是阿里云的安全策略,因为直接用用户名密码,一旦泄漏就很麻烦,而 acessKey 泄漏了也可以禁用。而且建议用 RAM 子用户的方式生成 accessKey,这样可以最小化权限,进一步减少泄漏的风险。

客户端直传 OSS 的方式不需要消耗服务器的资源,但是会有泄漏 acessKey 的风险,所以一般都是用服务端生成临时的签名等信息,然后用这些信息来上传。

这种方案就是最完美的 OSS 上传方案了。

。。。待实现。

Nest Winston

记录日志

数据库集成

TypeORM

TypeORM 是 NestJS 最常用的 ORM,支持多种数据库。

安装和配置
bash 复制代码
npm install @nestjs/typeorm typeorm mysql2
# 或使用其他数据库驱动
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/typeorm typeorm sqlite3
基本配置
ts 复制代码
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'test',
      entities: [User, Task], // 实体类
      synchronize: true, // 开发环境自动同步数据库结构
      logging: true, // 打印SQL语句
    }),
  ],
})
export class AppModule {}
实体定义
ts 复制代码
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column()
  email: string;

  @Column({ select: false }) // 默认查询时不返回密码
  password: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}
Repository 模式
ts 复制代码
// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async findOne(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = this.userRepository.create(userData);
    return this.userRepository.save(user);
  }

  async update(id: number, userData: Partial<User>): Promise<User> {
    await this.userRepository.update(id, userData);
    return this.findOne(id);
  }

  async remove(id: number): Promise<void> {
    await this.userRepository.delete(id);
  }
}
关系映射
ts 复制代码
// 一对多关系
@Entity('users')
export class User {
  @OneToMany(() => Task, task => task.user)
  tasks: Task[];
}

@Entity('tasks')
export class Task {
  @ManyToOne(() => User, user => user.tasks)
  @JoinColumn({ name: 'user_id' })
  user: User;
}

// 多对多关系
@Entity('users')
export class User {
  @ManyToMany(() => Role, role => role.users)
  @JoinTable()
  roles: Role[];
}

@Entity('roles')
export class Role {
  @ManyToMany(() => User, user => user.roles)
  users: User[];
}

Prisma

Prisma 是另一个流行的 ORM 选择。

安装和配置
bash 复制代码
npm install prisma @prisma/client
npm install -D prisma
npx prisma init
Schema 定义
prisma 复制代码
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
Service 使用
ts 复制代码
// user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}

  async findAll() {
    return this.prisma.user.findMany({
      include: { posts: true }
    });
  }

  async create(data: { email: string; name?: string }) {
    return this.prisma.user.create({ data });
  }
}

配置管理

环境变量

使用 @nestjs/config 包管理配置。

bash 复制代码
npm install @nestjs/config
ts 复制代码
// app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // 全局可用
      envFilePath: ['.env.local', '.env'], // 环境变量文件路径
    }),
  ],
})
export class AppModule {}
ts 复制代码
// .env
DATABASE_URL=mysql://root:password@localhost:3306/test
JWT_SECRET=your-secret-key
PORT=3000
ts 复制代码
// 使用配置
import { ConfigService } from '@nestjs/config';

@Injectable()
export class DatabaseService {
  constructor(private configService: ConfigService) {}

  getDatabaseUrl(): string {
    return this.configService.get<string>('DATABASE_URL');
  }
}

配置验证

ts 复制代码
// config/validation.schema.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test')
    .default('development'),
  PORT: Joi.number().default(3000),
  DATABASE_URL: Joi.string().required(),
  JWT_SECRET: Joi.string().required(),
});

认证和授权

JWT 认证

bash 复制代码
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
ts 复制代码
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: 'your-secret-key',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [AuthService, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}
ts 复制代码
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  async validateUser(username: string, password: string): Promise<any> {
    // 验证用户逻辑
    const user = await this.findUserByUsername(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
ts 复制代码
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'your-secret-key',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

角色权限

ts 复制代码
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
ts 复制代码
// roles.guard.ts
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));
  }
}

缓存

Redis 缓存

bash 复制代码
npm install @nestjs/cache-manager cache-manager cache-manager-redis-store
npm install -D @types/cache-manager
ts 复制代码
// app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
      ttl: 300, // 默认过期时间(秒)
    }),
  ],
})
export class AppModule {}
ts 复制代码
// user.service.ts
import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class UserService {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async findOne(id: number) {
    const cacheKey = `user_${id}`;
    let user = await this.cacheManager.get(cacheKey);
    
    if (!user) {
      user = await this.userRepository.findOne({ where: { id } });
      await this.cacheManager.set(cacheKey, user, 300); // 缓存5分钟
    }
    
    return user;
  }

  async update(id: number, userData: Partial<User>) {
    const result = await this.userRepository.update(id, userData);
    await this.cacheManager.del(`user_${id}`); // 清除缓存
    return result;
  }
}

任务调度

Cron 任务

bash 复制代码
npm install @nestjs/schedule
ts 复制代码
// app.module.ts
import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [ScheduleModule.forRoot()],
})
export class AppModule {}
ts 复制代码
// tasks.service.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class TasksService {
  @Cron('45 * * * * *') // 每分钟的第45秒执行
  handleCron() {
    console.log('Called when the current second is 45');
  }

  @Cron(CronExpression.EVERY_30_SECONDS) // 每30秒执行
  handleInterval() {
    console.log('Called every 30 seconds');
  }

  @Cron('0 0 0 * * *') // 每天午夜执行
  handleDaily() {
    console.log('Called daily at midnight');
  }
}

微服务

TCP 微服务

bash 复制代码
npm install @nestjs/microservices
ts 复制代码
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        host: 'localhost',
        port: 3001,
      },
    },
  );
  await app.listen();
}
bootstrap();
ts 复制代码
// user.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern } from '@nestjs/microservices';

@Controller()
export class UserController {
  @MessagePattern('get_user')
  getUser(data: { id: number }) {
    return { id: data.id, name: 'John Doe' };
  }

  @EventPattern('user_created')
  handleUserCreated(data: { id: number; name: string }) {
    console.log('User created:', data);
  }
}

RabbitMQ 微服务

bash 复制代码
npm install @nestjs/microservices amqplib
npm install -D @types/amqplib
ts 复制代码
// main.ts
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  AppModule,
  {
    transport: Transport.RMQ,
    options: {
      urls: ['amqp://localhost:5672'],
      queue: 'user_queue',
      queueOptions: {
        durable: false,
      },
    },
  },
);

测试

单元测试

ts 复制代码
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './user.entity';

describe('UserService', () => {
  let service: UserService;
  let mockRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should return all users', async () => {
    const mockUsers = [{ id: 1, name: 'John' }];
    mockRepository.find.mockResolvedValue(mockUsers);

    const result = await service.findAll();
    expect(result).toEqual(mockUsers);
    expect(mockRepository.find).toHaveBeenCalled();
  });
});

E2E 测试

ts 复制代码
// app.e2e-spec.ts
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('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

性能优化

压缩响应

bash 复制代码
npm install compression
ts 复制代码
// main.ts
import * as compression from 'compression';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(compression());
  await app.listen(3000);
}

限流

bash 复制代码
npm install @nestjs/throttler
ts 复制代码
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
})
export class AppModule {}
ts 复制代码
// user.controller.ts
import { Throttle } from '@nestjs/throttler';

@Controller('user')
export class UserController {
  @Throttle(5, 60) // 每分钟最多5次请求
  @Get()
  findAll() {
    return this.userService.findAll();
  }
}

PM2

日志管理、进程管理、负载均衡、性能监控、服务重启

安装和配置

bash 复制代码
npm install -g pm2
json 复制代码
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nestjs-app',
    script: 'dist/main.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    log_file: './logs/combined.log',
    out_file: './logs/out.log',
    error_file: './logs/error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
  }]
};

常用命令

bash 复制代码
# 启动应用
pm2 start ecosystem.config.js

# 启动生产环境
pm2 start ecosystem.config.js --env production

# 查看状态
pm2 status

# 查看日志
pm2 logs

# 重启应用
pm2 restart nestjs-app

# 停止应用
pm2 stop nestjs-app

# 删除应用
pm2 delete nestjs-app

# 监控
pm2 monit

生命周期钩子

NestJS 提供了多个生命周期钩子,让你可以在应用启动和关闭时执行特定逻辑。

应用生命周期

ts 复制代码
// app.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';

@Injectable()
export class AppService implements OnModuleInit, OnModuleDestroy, OnApplicationBootstrap, OnApplicationShutdown {
  onModuleInit() {
    console.log('Module initialized');
  }

  onApplicationBootstrap() {
    console.log('Application bootstrapped');
  }

  onModuleDestroy() {
    console.log('Module destroyed');
  }

  onApplicationShutdown(signal?: string) {
    console.log('Application shutdown', signal);
  }
}

模块生命周期

ts 复制代码
// app.module.ts
import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Module({
  // ...
})
export class AppModule implements OnModuleInit, OnModuleDestroy {
  onModuleInit() {
    console.log('AppModule initialized');
  }

  onModuleDestroy() {
    console.log('AppModule destroyed');
  }
}

WebSocket

安装和配置

bash 复制代码
npm install @nestjs/websockets @nestjs/platform-socket.io
npm install -D @types/socket.io
ts 复制代码
// app.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [ChatGateway],
})
export class AppModule {}

Gateway 实现

ts 复制代码
// chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
  OnGatewayConnection,
  OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }

  @SubscribeMessage('join_room')
  handleJoinRoom(
    @MessageBody() data: { room: string },
    @ConnectedSocket() client: Socket,
  ) {
    client.join(data.room);
    this.server.to(data.room).emit('user_joined', {
      message: `User ${client.id} joined room ${data.room}`,
    });
  }

  @SubscribeMessage('send_message')
  handleMessage(
    @MessageBody() data: { room: string; message: string },
    @ConnectedSocket() client: Socket,
  ) {
    this.server.to(data.room).emit('new_message', {
      id: client.id,
      message: data.message,
      timestamp: new Date(),
    });
  }
}

客户端连接

html 复制代码
<!-- 前端 HTML -->
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script>
  const socket = io('http://localhost:3000');

  socket.on('connect', () => {
    console.log('Connected to server');
  });

  socket.emit('join_room', { room: 'general' });

  socket.on('new_message', (data) => {
    console.log('New message:', data);
  });

  function sendMessage() {
    socket.emit('send_message', {
      room: 'general',
      message: 'Hello World!'
    });
  }
</script>

GraphQL

安装和配置

bash 复制代码
npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql
ts 复制代码
// app.module.ts
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,
    }),
  ],
})
export class AppModule {}

Resolver 实现

ts 复制代码
// user.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.entity';
import { CreateUserInput } from './dto/create-user.input';

@Resolver(() => User)
export class UserResolver {
  constructor(private userService: UserService) {}

  @Query(() => [User])
  users(): Promise<User[]> {
    return this.userService.findAll();
  }

  @Query(() => User, { nullable: true })
  user(@Args('id', { type: () => Int }) id: number): Promise<User> {
    return this.userService.findOne(id);
  }

  @Mutation(() => User)
  createUser(@Args('createUserInput') createUserInput: CreateUserInput): Promise<User> {
    return this.userService.create(createUserInput);
  }
}

健康检查

安装和配置

bash 复制代码
npm install @nestjs/terminus
ts 复制代码
// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheckService,
  HealthCheck,
  TypeOrmHealthIndicator,
  MemoryHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private memory: MemoryHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
      () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
    ]);
  }
}

文档生成

Swagger 配置

bash 复制代码
npm install @nestjs/swagger swagger-ui-express
ts 复制代码
// main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API Documentation')
    .setDescription('The API description')
    .setVersion('1.0')
    .addTag('users')
    .addBearerAuth()
    .build();
  
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

最佳实践

项目结构

arduino 复制代码
src/
├── app.module.ts
├── main.ts
├── common/
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   ├── interceptors/
│   └── pipes/
├── config/
├── modules/
│   ├── auth/
│   │   ├── auth.controller.ts
│   │   ├── auth.service.ts
│   │   ├── auth.module.ts
│   │   ├── dto/
│   │   └── strategies/
│   ├── users/
│   │   ├── users.controller.ts
│   │   ├── users.service.ts
│   │   ├── users.module.ts
│   │   ├── dto/
│   │   └── entities/
│   └── database/
│       ├── database.module.ts
│       └── database.service.ts
└── utils/

响应拦截器

ts 复制代码
// common/interceptors/transform.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
  code: number;
  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) => ({
        data,
        code: 200,
        message: 'success',
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

日志记录

ts 复制代码
// common/interceptors/logging.interceptor.ts
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();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const { statusCode } = response;
        const contentLength = response.get('content-length');

        this.logger.log(
          `${method} ${url} ${statusCode} ${contentLength} - ${Date.now() - now}ms`,
        );
      }),
    );
  }
}
相关推荐
前端付豪2 小时前
11、JavaScript 语法:到底要不要写分号?一文吃透 ASI 与坑点清单
前端·javascript
Copper peas2 小时前
Vue 中的 v-model 指令详解
前端·javascript·vue.js
万少2 小时前
十行代码 带你极速接入鸿蒙6新特性 - 应用内打分评价
前端·harmonyos
一写代码就开心2 小时前
VUE 里面 Object.prototype 是什么,如何使用他
前端
GHOME2 小时前
vue3中setup语法糖和setup函数的区别?
前端·vue.js·面试
lecepin2 小时前
AI Coding 资讯 2025-09-25
前端·javascript·后端
前端_逍遥生2 小时前
Vue3 响应式数据最佳实践速查表
前端·vue.js
KenXu2 小时前
EMP微前端实现Vue2、Vue3、React各版本调用方案
前端
xw52 小时前
一文搞懂Flex弹性布局空间分配规则
前端·css·flexbox