NestJS 总结

660 阅读9分钟

介绍说明

  • Nest.js 是一个 Node.js 的后端框架,它对 express 等 http 平台做了一层封装,解决了架构问题。它提供了 express 没有的 MVC、IOC、AOP 等架构特性,使得代码更容易维护、扩展。
  • nest 内实现 AOP 方式有5种方式:包括 Middleware、Guard、Inteceptor、Pipe、ExceptionFilter
    • 执行顺序:Middleware -> Guard -> Inteceptor(前) -> Pipe -> Inteceptor(后) -> ExceptionFilter
  • Nestjs 依赖注入(DI)思想是尽可能使用而不是实例,这样 Nest 就可以在整个模块中重用同一类的实例
  • 依赖注入: 是控制反转思想的一种实现,它将依赖的实例交给Ioc容器去执行。这里的Ioc容器就是@Module()
    • 在nest内基本的实现分为3个步骤
      1. cats.service.ts中使用@Injectable()装饰器声明CatsService类是一个可被Ioc容器管理的类
      2. cats.controller.ts中的CatsController声明一个依赖于CatsService令牌的注入值
        • constructor(private readonly catsService(这是个 value): CatsService(这是个 token) )
      3. app.module.ts中,将标记CatsServicetoken与cats.service.ts文件中CatsService类的关联关系
        • import{ CatsService } from './cats/cats.service'
        • @Module({ provides: [ CatsService ] }) -> { prviders: [{ provide: CatsService-token, useClass: CatsService-class }] }
      4. 只后当Ioc容器实例化CatsController时。执行constructor()就会查找所有的依赖项,并通过@Module内配置的依赖关系去找对应的Provider并实例化(可缓存的)。
      5. 并且这些依赖之间是可以相互依赖的

其他说明

  • 全局API
    • app.useGlobalGuards/useGlobalInterceptors/useGlobalPipes/useGlobalFilters() 添加的处理函数执行顺序按照添加的顺序执行
    • app.enableCors({ options }) 允许跨域
  • req/res.app 可获取全局实例对象
    • req.app.locals 存储相关

Controller

  • 定义路由@Controller(xxx)可设置前缀。
  • @HttpCode(304)设置响应的状态码
  • @Header(key, value)设置响应的头信息
  • @Redirect(url, statusCode)重定向,可配合路由处理方法控制重定向的URL
  • 请求方式@Get/Post/Patch/Delete/Put/Head/Options/All(xxx)路由支持使用通配符
    • 使用:id设置param路由参数
    • 参数获取方式@Param/Body/Query/Headers(key?) xxx
      • 未配置key时,整体赋值给xxx
      • 配置key时,把匹配到的具体值给xxx
    • 其他信息获取@Req/Res/Nest/Session/Ip/HostParam()
      • @Req()可以获取完整的req对象
      • @Res()可获取res对象。可以使用res.get/json/send/sendFile()
        • 注意:nestjs 不建议直接使用原生的 res.json/send() 这类的响应式方式。会让部分拦截器/装饰器失效,但可以通过配置@Res({ passthrough: true })解决
      • req/res.xxx是可以获取express上所有原生方法
  • 定义DTO用于配合body参数的获取,并可配合class-validator进行参数校验
    • dto 直接可通过PartialType/PickType/OmitType/IntersectionType控制继承内容
      • PartialType 将输入类型都改为可选类型
      • PickType(createDto, [...args] as const) {}从类中选择属性继承
      • OmitType(createDto, [...args] as const) {}从类中排除属性继承
      • IntersectionType(xxxDto, xxxDto)从最多4个类继承
  • 其他类型参数也可用class-validator配合校验。使用方式和dto一样
// 验证使用方式
import { IsNumberString } from 'class-validator';

class OneParams {
  @IsNumberString()
  id: number;
}

@Get(':id')
findOne(@Param() params: OneParams){}

Providers 提供者

  • 对应着service服务,通过@Injectable()装饰
  • 依赖注入constructor(private readonly catsService: CatsService){}把模块提供给constructor使当前类下可以访问到内容

Modules 模块

  • 用于关联依赖并传递给IOC容器
  • 模块可以导出自身的controller/provider还可以导出自己exports: [xxxModule]
  • 使用@Global()创建全局模块,这样该模块只要在根模块注册。其他模块中都可访问到

Middlewares 中间件

  • 模块中间件需要实现NestModule接口并设置configure方法
  • 全局中间件app.use(logger);
  • **重要:**这个中间件的逻辑和express内的不相同,按请求的生命周期顺序中间件只会进入一次且也不是函数嵌套的逻辑。所以同样的处理需要使用nest的拦截器方式
// app.module.ts
export class AppModule implements NestModule {
  // * 自定义中间件
  // 需要实现 configure 方法
  configure(consumer: MiddlewareConsumer) {
    consumer
      // 可以是多个中间件。 a(), b()
      .apply(logger)
      // 用于定义排除的 路由。别忘了前缀
      .exclude({ path: 'api/user/:id', method: RequestMethod.GET })
      // 可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类
      .forRoutes(UserController);
  }
}

// logger.midllerware.ts
export function logger(req: Request, res: Response, next: NextFunction) {
  console.log('-- logger --', req.path);

  next();
}

ExceptionFilters 异常过滤器

  • 通过ExceptionFilter<T>接口实现
  • 方法范围@UseFilters(HttpExceptionFilter)可在Controller内某个路由下添加过滤器
  • 控制器范围@UseFilters(HttpExceptionFilter) -> export class XxxController {}
  • 全局范围app.useGlobalFilters(new HttpExceptionFilter())
// any-exception.filter.ts
// 使用全局挂载时应该放在最后一个 app.useGlobalFilters(...其他处理函数, new AnyExceptionFilter())
@Catch()
export class AnyExceptionFilter implements ExceptionFilter {
  // 依赖注入
  // constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: any, host: ArgumentsHost) {
    console.log('### Any exception filter ###', JSON.stringify(exception));

    // * 使用这种方式就和平台类型无关。但好像报错啊
    // const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();
    const req = ctx.getRequest<Request>();
    const res = ctx.getResponse<Response>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const resBody: ErrorResponse = {
      errno: -1,
      message: exception.message || '服务错误',
      path: req.url,
      // path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    res.status(status).json(resBody);
    // httpAdapter.reply(ctx.getResponse(), resBody, status);
  }
}

Pipes 管道

  • 用于输入数据的转化、数据的有效性校验
  • 使用@Injectable()装饰器注解改类,并基于PipeTransform<T>接口实现transform()方法
  • 内置转化ParseIntPipe/ParseFloatPipe/ParseBoolPipe/ParseEnumPipe/ParseArrayPipe/DefaultValuePipe配合参数获取装饰器使用@Param/Query/Body()

Guards 守卫

  • 用于判断是否满足某些定义的条件,通过后直到到路由程序
  • 使用@Injectable()装饰器注解改类,并基于CanActivate<T>接口实现canActivate()方法。通过return true/false控制
    • 返回 false 是触发 throw new UnauthorizedException()
    • 可以在constructor() {}内注入相关依赖
  • 使用@UseGuards()装饰器使用守卫,同样支持路由方法/控制器/全局等
// 可从路由函数上的 @SetMetadata('roles', ['admin']) 获取相关数据
@Injectable()
export class RolesGuard implements CanActivate {
  // 注入依赖
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    // 获取 SetMetadata 上的元数据
    // *返回 [ 'admin', 'abc' ]
    // 没有返回 undefined
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }

    // 可以从请求上获取信息用来判断
    // const req = context.switchToHttp().getRequest();

    // 返回 false 是触发 throw new UnauthorizedException();
    // return false;
    return true;
  }
}

Interceptors 拦截器

  • 可以在路由函数的前/后两处执行额外的逻辑。有些类似express中间件的逻辑
  • 使用@Injectable()装饰器注解改类,并基于NestInterceptor<T>接口实现intercept()方法
    • 可以绑定在全局/类/路由函数@UseInterceptors()
    • 可以在constructor() {}内注入相关依赖
@Injectable()
// 处理响应数据的格式化
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // *可获取 request 信息
    const req = context.switchToHttp().getRequest();

    return (
      next
        // 调用 handle() 执行路由函数
        .handle()
        // 获取路由函数的返回信息
        .pipe(
          // 这里可以添加多个处理逻辑
          // tap/map/catchError() 从 rxjs/operators 获取
          map((data = {}) => {
            const resData: SuccessResponse = {
              errno: 0,
              message: '请求成功',
              data,
            };
            return resData;
          }),
        )
    );
  }
}

自定义decorator/provider

  • provider
    • useValue/useClass/useFactory(支持 async/await)
      • useFactory在@Module时就会先执行,之后如果在this.xxx读取还会再次执行
      • useFactory可配置inject用于配置参数
    • 所有tokenstring都需要使用@Inject('xxx')装饰器来提供注入
      • 使用@Inject('CONFIG') [private] [readonly] configFactory: aFn方式
    • 注意: 有两种使用方式
// 注入时提供 private 后 IOC 容易会把内容挂载到当前类下
constructor(@Inject('Connection') private connection: string){}
create(){
  // 通过这方法可在 this.xxx 上获取
  this.connection
}

// 这样就只能通过参数来获取,说明注入内容并不在当前 类 挂载
constructor(@Inject('Connection') connection: string){}
create(){
  // 通过这方法可在 xxx 上获取
  connection
}
  • 动态模块
    • 主要是为了在导入其他模块c时可传递一些参数,实际使用的还是a模块内的controller/service
    • 使用@Module({})装饰器声明一个模块类,然后内部实现register(options)函数并返回模块对象
    • 参数通过useValue形式提供,并配合@Inject('xxx') options来获取
    • 之后在需要倒入其他模块@Module({ imports: [ xxxModule.register( {params} ) ] })来传递自己想要的参数内容
  • 作用域:可在Controller/Injectable装饰器内设置作用域范围
    • scope.DEFAULT 同引用生命周期
    • scope.REQUEST 请求处理完后销毁
    • scope.TRANSIENT 每个使用Provider的程序都会得到一个独立的实例

可执行上下文 excutionContext

  • 基础类ArgumentsHost允许选择可执行的上下文对象
    • getType/switchToRpc/switchToHttp/switchToWs()
      • switchToHttp() 可返回request/response/next
  • ExecutionContext继承至ArgumentsHost
    • 拓展了getClass/getHandler() 返回当前处理的class/函数
  • Reflector元数据访问类
    • 返回 get/getAllAndOverride/getAllAndMerge(key, 当前处理 class/function)
  • 注意:guard/interface/filter/decorator 等都能拿到可执行上下文

session/cookie 使用

  • redis-session
    • redis+session 添加的方式和 express 里一样
    • 使用可通过两种方式@Req() -> req.session.xxx或者@Session() -> session.xxx
    • 失效方式session=nullstore.destroy(sid, callback)或者从redis里直接删除
  • cookie
    • 添加cookie-parser包用于解析。然后使用res.cookie(key, value [, options])并配置@Res({ passthrough: truee })
    • 建议创建时配置密钥cookieParser('_xxx_'),让之后设置{ signed: true }时报错
    • cookie 内容获取有req.cookies/signedCookies两种方式

Sequelize ORM

  • 主要内容查看 链接
  • 原来关联后附带的方法重命名了
    • Modulex.$set/add/remove/create('key', [ instance ])
    • x.$get/count/has('key')

验证配合 class-validator

  • 其实 nest 内部已经提供了很不错的验证,引入class-validator是为了让dto类也支持校验
  • 内部ValidationPipe的部分配置参数
    • transform 将传入的 js 对象按 dto 配置的进行类型转化
    • whitelist 会删除未配置 validator 的属性
    • disableErrorMessages 验证信息不返回给客户端
    • exceptionFactory 配置异常处理函数
    • forbidNonWhitelisted 当有未配置校验的属性时报错。需要同时配置whitelist: true
    • skipMissingProperties 跳过对缺失属性的校验。当开启式可配合@IsDefined()对想要的缺失属性进行强校验
    • validationError.target 是否显示传入的完整源数据
    • validationError.value 是否显示错误具体的 value
  • 显示手动转化方式
    • param/query方式传入的都是string类型的值,可通过以下方式进行手动转化
      • 通过@Param('id', ParseIntPipe) id: number方式使用。所有请求参数获取方式都支持pipe
    • 导出ParseIntPipe/ParseFloatPipe/ParseBoolPipe/ParseEnumPipe/ParseArrayPipe/DefaultValuePipe
  • 解析和验证数组
// body
@Post()
createBulk(
  @Body(new ParseArrayPipe({ items: CreateUserDto }))
  createUserDtos: CreateUserDto[],
) {
  return 'This action adds new users';
}

// query 如:/xxx?ids=1,2,3
@Get()
findByIds(
  @Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
  ids: number[],
) {
  return 'This action returns users by ids';
}
  • class-validator 文档部分摘录
    • 自定义错误信息@xxx({ message: string|function })
    • Array/Set/Map 项每个都校验@xxx({ each: true })
    • 校验继承性:如果从一个已经有验证规则的dto类继承,并对同个属性添加新的校验。那么这个属性包含两个类的验证规则
    • 嵌套校验@ValidateNested()对应对象必须也是一个配置了校验规则的类
    • promise 对象校验@ValidatePromise()
    • 条件校验@ValidateIf(o => o.otherProperty === 'value')只有返回true才触发校验。o可获取类内的其他属性
  • 常用校验装饰器
    • @IsInt/IsBoolean/IsString()
    • @Length(5, 10)/MinLength/MaxLength(5) 约束 string 长度
    • @IsArray/ArrayNotEmpty/IsObject/IsNotEmptyObject() 数组/对象
    • @IsIn/IsNotIn(values: any[]) array 是否包含指定值
    • @Contains/NotContains( value ) 内容是否包含指定值
    • @IsNumber()
    • @Min/Max(10) 检验 number 是否满足给定的值
    • @IsEnum( 枚举定义 ) 固定的枚举类型
    • @IsDate() 是否是date类型
    • @MinDate/MaxDate( 日期值 ) 在日期范围内
    • @IsEmail()
    • @IsUrl() 是否满是URL
    • @IsAlpha() 只包含a-zA-Z
    • @IsAlphanumeric 只包含字母数字
    • @IsBooleanString/IsDateString/IsNumberString() 是否是指定类型的string
    • @IsBase32/IsBase64/IsDataURI() 指定的数据类型
    • @Matches(/abc/, i) 正则匹配