nt

200 阅读9分钟

简介

安装

可以使用 Nest CLI 创建项目,也可以克隆一个项目(两者的结果是一样的)。

使用 Nest CLI 构建项目

$ npm i -g @nestjs/cli
$ nest new project-name

其他安装方式

使用 Git 安装基于 TypeScript 的项目

$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start

要安装基于 JavaScript 的项目,执行上面的命令时使用 javascript-starter.git

还可以通过 npmyarn安装核心和支撑文件,这种情况下,你将自己创建项目样板文件。

$ npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata

概述

第一步

新建

$ npm i -g @nestjs/cli
$ nest new project-name

TypeScript strict模式的项目,将 --strict 传递给 nest new 命令

src/目录几个核心文件。

  • app.controller.spec.ts
  • app.controller.ts
  • app.module.ts
  • app.service.ts
  • main.ts
app.controller.ts带有单个路由的基本控制器示例。
app.controller.spec.ts对于基本控制器的单元测试样例
app.module.ts应用程序的根模块。
app.service.ts带有单个方法的基本服务
main.ts应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。

main.ts 包含一个异步函数,负责引导应用程序:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

使用核心类 NestFactory 创建应用实例。NestFactory 暴露一些静态方法用于创建应用实例。 create() 方法返回实现 INestApplication 接口的对象。该对象提供一组可用的方法,后面章节中对这些方法进行详细描述。 上例中,只是启动 HTTP 服务,让应用程序等待 HTTP 请求。

在创建应用实例时发生错误,应用会退出并返回错误代码 1。如果想让它抛出错误,请禁用 abortOnError 选项 (如,NestFactory.create(AppModule, { abortOnError: false }))。

平台

两种平台开箱即用expressfastify

platform-expressExpress一个著名的极简web框架。默认使用@nestjs/platform-express
platform-fastifyFastify一个高性能和低开销的框架,高度关注提供最大的效率和速度。这里使用。

无论哪个平台,都公开自己的接口。分别被视为NestExpressApplicationNestFastifyApplication

将一个类型传递给NestFactory.create(),应用程序对象将有该平台的方法。除非要访问平台API,否则不用指定类型。

const app = await NestFactory.create<NestExpressApplication>(AppModule);

控制器

控制器处理传入请求并向客户端返回响应。

img

创建有内置验证的CRUD控制器

$ nest g resource [name]

路由

@Controller()装饰器是定义控制器所必需的。在@Controller()装饰器中指定路径前缀cats可对一组相关路由分组。

//cats.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get('list') // 'cats/list'
  findAll(): string {
    return 'This action returns all cats';
  }
}

使用CLI创建控制器

$ nest g controller cats

@Get()装饰器,装饰请求处理方法。

该方法返回200状态码和响应, 介绍两种不同响应:

Standard (recommended)标准(推荐)使用这个内置方法,请求返回对象或数组时,自动序列化为JSON。返回基本类型(如,字符串,数字,布尔值)时只发送值不尝试序列化它。响应状态码默认是200,除了使用201POST请求。添加@HttpCode(…)装饰器,可以改变状态码。
Library-specific 库特有的@Res() 注入库特定的响应对象,(如,findAll(@Res() response))。使用此方法,就能使用由该响应对象暴露的原生响应处理函数。例如,使用 Express,可以使用 response.status(200).send() 构建响应

Nest 检测处理程序何时使用 @Res()@Next(),表明你选择了特定于库的选项。如果在一个处理函数上同时使用了这两个方法,那么此处的标准方式就是自动禁用此路由, 你将不会得到你想要的结果。如果需要在某个处理函数上同时使用这两种方法(例如,通过注入响应对象,单独设置 cookie / header,但把其余部分留给框架),你必须在装饰器 @Res({ passthrough: true }) 中将 passthrough 选项设为 true

请求对象

Nest提供了对底层平台(默认为Express)请求对象的访问。添加@Req()装饰器注入请求对象,从而访问请求对象。

//cats.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

安装@types/express便于类型提示。

请求对象有查询字符串、参数、HTTP头和主体等属性。使用专用的装饰器,如@Body()@Query(),它们是开箱即用的。下面是装饰器和它们代表的对象列表。

@Request(), @Req()req
@Response(), @Res()*****res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

注入@Res()@Response()时,Nest变成Library-specific模式,您将负责管理响应。必须调用响应对象(如res.json(…)res.send(…))发出相应类型的响应,否则HTTP服务器将挂起。

资源

创建POST处理程序:

//cats.controller.ts
import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Nest为标准HTTP方法提供装饰器: @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options()@Head()。此外,@All()定义一个端点处理所有这些。

路由通配符

支持基于模式的路由。如,星号用作通配符,匹配任何字符。

@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

'ab*cd'路由路径将匹配abcd, ab_cd, abecd等。字符?, +, *()可以在路由路径中使用,它们对应正则表达式的子集。

状态码

响应状态码默认是200, POST请求除外,它是201。在处理程序级别添加@HttpCode(…)装饰器可改变状态码

import HttpCode from '@nestjs/common'

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

状态码不是静态的,而取决于各种因素。在这种情况下,您可以使用library-specific的响应对象(@Res()注入),或在发生错误时抛出异常。

自定义响应头,使用@Header()装饰器或特定库(library-specific)的响应对象(调用res.header())。

import Header from '@nestjs/common'

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

重定向

将响应重定向到特定URL,使用@Redirect()装饰器或特定库(library-specific)的响应对象(调用res.redirect())。

@Redirect()有两个参数,urlstatusCode,都是可选的。如果省略,statusCode的默认值是302

@Get()
@Redirect('https://nestjs.com', 301)

动态确定HTTP状态码或重定向URL。通过路由控制方法返回特定对象实现:

{
  "url": string,
  "statusCode": number
}

@Redirect()装饰器参数被返回值覆盖。例如:

@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

路由参数

路由参数使用@Param()装饰器访问。

import Param from '@nestjs/common'

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

参数传递给@Param()装饰器,通过名称使用参数。

@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

子域路由

@Controller装饰器传入含host属性对象,请求需匹配特定值。

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

Fastify缺乏对嵌套路由器的支持,使用子域路由时,应使用(默认)Express适配器。

host选项可以使用标记捕获主机名中该位置的动态值。使用@HostParam()装饰器访问主机参数。

@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

作用域

几乎所有内容共享。数据库连接池,全局状态单例服务等。Node.js不遵循请求/响应多线程无状态模型,每个请求由单独线程处理。

某些情况下,控制器基于请求的生命周期是所需的行为,如GraphQL应用程序中请求缓存、请求跟踪或多租户。

异步性

async返回Promise

//cats.controller.ts

@Get()
async findAll(): Promise<any[]> {
  return [];
}

Nest路由处理程序甚至更强大,它能够返回RxJS可观察流(observable streams)。Nest自动订阅底层的源并接受最后发出的值(一旦流完成)。

//cats.controller.ts

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

请求有效载荷

添加@Body()装饰器。

使用TypeScript,需要确定DTO(数据传输对象)。

//create-cat.dto.ts

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}
//cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

ValidationPipe可以过滤掉不该被接收的属性。将可接受的属性列入白名单,没有在白名单中的属性将从结果对象中删除。在CreateCatDto示例中,白名单是名称、年龄和品种属性。

完整的资源示例

//cats.controller.ts

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

特定库方法

讨论了Nest操作响应的标准方式。操作响应的第二种方式是使用特定库(library-specific)的响应对象。注入特定的响应对象,使用@Res()装饰器。

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

对响应对象完全控制(头操作、特定库的特性等)。缺点,代码依赖指定库(响应对象api不同),难测试(模拟响应对象等)。

失去与Nest特性(依赖于Nest标准响应)的兼容性,如Interceptors(拦截器)和@HttpCode() / @Header()装饰器。解决这个问题,设置passthroughtrue

@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

现在根据条件设置cookiesheaders,其余工作留给框架。

提供者

提供者是基本概念。许多基本类被视为提供者——服务(services)、存储库(repositories)、工厂(factories)、助手(helpers)等。提供者主要思想是作为依赖注入; 意味着对象之间可以创建各种关系,“连接”对象实例可以委托给运行系统。

构建的CatsController。控制器处理HTTP请求,复杂的任务委派给提供者。提供者是声明为提供者的普通类。

服务

//cats.service.ts

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

@Injectable()装饰器附加了元数据(metadata),声明CatsService是个由Nest IoC容器管理的类。

//interfaces/cat.interface.ts

export interface Cat {
  name: string;
  age: number;
  breed: string;
}
//cats.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

通过构造函数注入CatsService。这种简写方式立即声明和初始化catsService成员。注意私有语法。

依赖注入

Nest是围绕依赖注入的强设计模式构建的。

TypeScript管理依赖关系很容易,根据类型进行解析。

constructor(private catsService: CatsService) {}

作用域

提供者与应用程序生命周期(作用域)同步。引导(bootstrapped)应用程序时,须解析每个依赖项,须实例化每个提供者。应用程序关闭时,每个提供者将被销毁。有些方法可以使提供者的生命周期作为请求作用域。更多技巧 here.

自定义提供者

内置的控制反转(IoC)容器,用来解决提供者之间的关系。这是依赖注入特性的基础,实际上它更强大。定义提供者有几种方法: 纯值、类、异步或同步工厂。More examples here.

可选提供者

有时,有不一定需要解析的依赖项。类可能依赖于配置对象,没有传配置对象,应使用默认值。依赖项为可选,因为缺少配置提供者不会导致错误。

要表示提供者是可选的,可以在构造函数的签名中使用@Optional()装饰器。

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

例子中,使用一个自定义提供者,这是包含HTTP_OPTIONS自定义令牌的原因。更多自定义提供者及令牌的信息这里

基于属性的注入

到目前使用的基于构造函数的注入,提供者通过构造函数注入。特定情况下,基于属性的注入是有用的。顶级类依赖一个或多个提供者,在子类构造函数中调用super()向上传递非常繁琐。避免这种情况,属性级别使用@Inject()装饰器。

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

如果您的类没有扩展其他提供者,应该使用基于构造函数的注入。

提供者登记

//app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

目录结构应该是这样

src
    cats
        dto
        	create-cat.dto.ts
        interfaces
        	cat.interface.ts
        cats.controller.ts
        cats.service.ts
    app.module.ts
    main.ts

手动实例化

可能需要跳出内置的依赖注入系统,手动检索或实例化提供者。讨论两个这样的主题。

获得现有实例或动态实例提供者,参考模块module.

bootstrap()函数中获取提供者(没有控制器的独立应用程序,或引导期间利用配置服务),参阅( Standalone applications)。

模块

模块是一个用@Module()装饰器装饰的类。@Module()装饰器提供了(Nest组织应用程序结构的)元数据。

每个应用程序至少有一个模块,每个模块封装一组密切相关的功能

@Module()装饰器接受单个对象,其属性描述了模块:

providers注入实例化的提供者,在这个模块中共享
controllers模块控制器集,须被实例化
imports导入模块列表
exports导入此模块的其他模块中可用的providers子集

模块默认封装提供者。意味着不可能注入提供者(既不是当前模块的直接组成部分,也不是从导入模块导出的)。可以将从模块导出的提供者视为模块的公共接口或API

功能模块

功能模块组织与功能相关的代码,建立明确边界。有助于管理复杂的使用SOLID原则的项目

//cats/cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
//app.module.ts

import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule {}
src
    cats
        dto
        	create-cat.dto.ts
        interfaces
        	cat.interface.ts
        cats.controller.ts
        cats.module.ts
        cats.service.ts
    app.module.ts
    main.ts

共享模块

模块默认是单例的,可以在多个模块之间共享任何提供者的同一个实例。

模块都是共享的,可以被任何模块重用。在其他几个模块之间共享CatsService的一个实例。将CatsService提供者添加到模块的exports数组中

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService]
})
export class CatsModule {}

任何导入CatsModule模块的模块都可以访问CatsService,与其他导入它的模块共享相同的CatsService实例。

模块重新导出

可以重新导出导入的模块,使它对导入该模块的其他模块可用。

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

依赖注入

模块类可以注入提供者(如,出于配置目的):

//cats.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private catsService: CatsService) {}
}

由于循环依赖,模块类本身不能作为提供者被注入。

全局模块

提供者封装在模块内。不先导入模块,就不能在其他地方使用模块的提供者。

提供一组在任何地方开箱即用的提供者(如,helper,数据库连接等),@Global()装饰器使模块具有全局作用域

import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Global()
@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}

全局模块只注册一次,由根模块或核心模块注册。

动态模块

动态注册、配置提供者。动态模块介绍

import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';

@Module({
  providers: [Connection],
})
export class DatabaseModule {
  static forRoot(entities = [], options?): DynamicModule {
    const providers = createDatabaseProviders(options, entities);
    return {
      module: DatabaseModule,
      providers: providers,
      exports: providers,
    };
  }
}

forRoot()方法可同步或异步的返回一个动态模块(如,通过Promise)。

该模块定义了Connection提供者(在@Module()装饰器元数据中),根据传递到forRoot()方法的entitiesoptions 对象,公开一个提供者集合,如,存储库。动态模块返回属性,扩展(不是覆盖)@Module()装饰器中定义的模块元数据。这是静态声明的Connection 提供者和动态生成的存储库提供者从模块导出的方式。

在全局作用域中注册一个动态模块,global属性设为true

{
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
}

动态模块DatabaseModule通过以下方式导入和配置:

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}

反过来导出一个动态模块,在exports数组中省略forRoot()方法调用:

import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [DatabaseModule.forRoot([User])],
  exports: [DatabaseModule],
})
export class AppModule {}

动态模块一章详细讨论了这个主题,包括一个工作示例

中间件

路由处理之前调用。中间件可以访问 requestresponse 对象、请求-响应周期中的next()(下一个中间件函数)。下一个中间件函数通常由名为next的变量表示。

默认情况下,Nest中间件相当于 express 中间件。官方express文档描述的中间件功能:

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

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

Nest中间件(函数或带有@Injectable()装饰器的类)。类实现NestMiddleware接口,函数没有特殊要求。

//logger.middleware.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();
  }
}

依赖注入

中间件支持依赖注入。像提供者和控制器一样,能够注入在同一模块内可用的依赖项。通过“构造函数(constructor)”完成的。

应用中间件

中间件在模块类configure()方法中设置,不是在@Module()装饰器中。模块须实现NestModule接口。

//app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

/cats路由处理程序设置了LoggerMiddleware中间件。

配置中间件时将包含路由path和请求method的对象传递给forRoutes()方法

//app.module.ts

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

configure()可用async/await

路由通配符

支持基于Pattern(模式)的路由。

forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });

'ab*cd'路由路径匹配abcd, ab_cd, abecd等。?, +, *, 和 ()可以在路由路径中使用,对应正则表达式的子集。

fastify 包使用path-to-regexp包的最新版本,不支持通配符星号*。须使用参数(如,(.*), :splat*)

中间件消费者(consumer

MiddlewareConsumer 一个helper(助手)类。几个内建方法管理中间件,可以链式链接。forRoutes()方法接受一(多)个字符串、一个RouteInfo 对象、一(多)个控制器类,多数情况下,是以逗号分隔的控制器列表。

//app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}

apply()方法接受单个或多个中间件

排除路由

exclude()方法排除路由。接受一(多)个字符串或RouteInfo对象

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);

exclude()方法支持通配符参数使用 path-to-regexp 包。

函数中间件

LoggerMiddleware 类非常简单。没有成员,没有其他方法,没有注入依赖项。可定义为函数。

// logger.middleware.ts

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
};
// app.module.ts
consumer
  .apply(logger)
  .forRoutes(CatsController);

函数中间件不能注入依赖项。类中间件通过“构造函数(constructor)”注入依赖项

多个中间件

apply()中提供一个逗号分隔的中间件列表(顺序执行)

consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

全局中间件

绑定每个注册的路由,使用INestApplication实例提供的use()方法:

// main.ts

const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

全局中间件中不能访问DI(Dependency Injection:依赖注入)容器。app.use()使用函数中间件。 若使用类中间件,在AppModule(或其他模块)中使用.forRoutes('*')

异常过滤器

内置全局异常过滤器,负责应用未处理的异常。代码没处理异常时,它将捕获异常,发送友好响应。

img

内置全局异常过滤器处理HttpException类型(及其子类)的异常。当异常无法识别时(不是HttpException类型(及其子类)),内置异常过滤器会生成默认JSON响应:

{
  "statusCode": 500,
  "message": "Internal server error"
}

内置全局异常过滤器部分支持http-errors库。抛出异常都包含statusCodemessage属性并作为返回响应,不是默认的InternalServerErrorException(处理无法识别的异常)

抛出标准异常

内置的HttpException类,从@nestjs/common包中公开。基于HTTP REST/GraphQL API的应用发生错误时,发送标准HTTP响应对象。

//cats.controller.ts

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

客户端调用时,响应如下

{
  "statusCode": 403,
  "message": "Forbidden"
}

HttpException(response, status)接受两个必填参数:

响应包含两个属性:

  • statusCode状态码
  • message错误描述

第一个参数response(响应),传字符串(覆盖JSON响应体消息部分)。传对象(覆盖整个JSON响应体)。

第二个参数status(状态码),有效的HTTP状态码。使用从@nestjs/common导入的HttpStatus枚举。

第三个参数options(可选),提供错误原因。原因对象没有序列化到响应对象中,但对日志有用。

//cats.controller.ts

@Get()
async findAll() {
    try {
        await this.service.findAll()
    }
    catch (error) {
        throw new HttpException(
            {
                status: HttpStatus.FORBIDDEN,
                error: 'This is a custom message',
            },
            HttpStatus.FORBIDDEN,
            { cause: error }
        );
    }
}

下面是响应:

{
  "status": 403,
  "error": "This is a custom message"
}

自定义异常

可以创建异常体系,自定义异常继承基础的HttpException类。Nest识别你的异常,自动处理错误响应。

//forbidden.exception.ts

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

ForbiddenException扩展了HttpException,与内置异常无缝衔接。

//cats.controller.ts

@Get()
async findAll() {
  throw new ForbiddenException();
}

内置HTTP异常

Nest提供一组继承自HttpException类的标准异常。从@nestjs/common包中公开,代表常见HTTP异常:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

内置异常使用options参数 提供错误 原因 和 描述:

throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })

下面是响应:

{
  "message": "Something bad happened",
  "error": "Some error description",
  "statusCode": 400,
}

异常过滤器

基本的(内置的)异常过滤器可以处理许多情况。完全控制异常层是设计异常过滤器的目的。

创建一个异常过滤器,负责捕获HttpException类型的异常,实现自定义响应。需要访问底层平台RequestResponse对象。

//http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

异常过滤器都应该实现ExceptionFilter<T>接口。提供catch(exception: T, host: ArgumentsHost)方法。T表示异常的类型。

@Catch(HttpException)装饰器将所需的元数据绑定到异常过滤器,告诉Nest这个过滤器寻找类型为HttpException的异常,而不是其他类型的异常。@Catch()装饰器可以接受单个参数,也可接受逗号分隔的列表。允许同时为几种类型的异常设置过滤器。

参数host

catch()方法的参数。exception参数是正在处理的异常对象。host参数是一个ArgumentsHost对象。使用ArgumentsHost上的辅助方法获得所需的RequestResponse对象。了解ArgumentsHost这里

绑定过滤器

将新的HttpExceptionFilter绑定到CatsControllercreate()方法。

//cats.controller.ts

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

@UseFilters()装饰器是从@nestjs/common包导入的。

@UseFilters()@Catch()装饰器类似。接受单个或逗号分隔的过滤器实例。可以传递类(而不是实例),将实例化的责任留给框架。

//cats.controller.ts

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

最好使用类而不是实例。减少内存使用,可以在整个模块中重用同一个类的实例。

上例中,HttpExceptionFilter只应用于单个create()路由处理程序,具有方法作用域。异常过滤器可以在不同的级别上确定作用域:方法作用域、控制器作用域或全局作用域。例如,将过滤器设置为控制器作用域

//cats.controller.ts

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

每个在CatsController中定义的路由处理程序设置HttpExceptionFilter过滤器。

创建一个全局作用域的过滤器:

//main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

useGlobalFilters()方法不为网关或混合应用程序设置过滤器。

全局过滤器针对每个控制器和每个路由处理程序。从模块外部注册的全局过滤器(如上例,使用useGlobalFilters())不能注入依赖,因为他在任何模块上下文之外完成的。使用以下结构,可以从任何模块注册一个全局作用域过滤器:

//app.module.ts

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

在哪个模块中使用了这种构造,过滤器都是全局的。另外,useClass不是处理自定义提供者注册的唯一方法。了解更多这里

可以添加任意数量的过滤器,添加到providers数组中即可。

捕获任何异常

捕获每个未处理异常(无论异常类型),将@Catch()装饰器的参数设为空,例如@Catch()

与平台无关的代码,使用HTTP适配器传递响应,不使用任何平台的对象(Requestresponse):

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // 在某些情况下,' httpAdapter '可能在构造函数方法中不可用,因此我们应该在这里解析它。
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

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

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

继承

通常创建完全定制的异常过滤器,以满足应用需求。某些情况下,只简单扩展内置的默认全局异常过滤器,基于某些因素重写该行为。

将异常处理委托给基本过滤器,需要扩展BaseExceptionFilter并调用继承的catch()方法。

//all-exceptions.filter.ts

import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

警告 扩展BaseExceptionFilter的过滤器(方法作用域、控制器作用域的)不应该用new实例化。让框架自动实例化它们。

全局过滤器可以扩展基本过滤器。有两种方法可以做到这一点。

第一种方法:实例化自定义全局过滤器时注入HttpAdapter引用:

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

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

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

第二种方法:使用APP_FILTER token 如示【绑定过滤器】。

管道

@Injectable()装饰器标注的类,实现了PipeTransform接口。

img

管道有两个典型的用例:

- transformation: 输入数据转换为所需形式(如,字符串到整数)

- validation[验证]: 评估输入数据,有效,简单通过不变; 否则,抛出异常

两种情况,管道都对路由处理程序arguments[参数]进行操作。调用方法之前插入一个管道,管道接收该方法的参数并对它们进行操作。此时发生转换或验证操作,之后使用转换后参数调用路由处理程序。

管道抛出异常时,将由异常层处理(应用到当前的任何异常过滤器)。

内置管道

提供了8个内置管道:

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

@nestjs/common包导出的。

使用ParseIntPipe。这是transformation的示例,确保参数被转换为整数(或转换失败抛出异常)。下面示例也适用其他内置转换管道(ParseBoolPipeParseFloatPipeParseEnumPipeParseArrayPipeParseUUIDPipe,称为Parse*管道)。

绑定管道

方法参数级别绑定管道:

@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

要么findOne()接收的参数是数字,要么调用路由处理程序之前抛出异常。

假设路由调用如下:

GET localhost:3000/abc

会抛出一个异常:

{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

该异常阻止findOne()方法的主体执行。

上例中,传递一个类(ParseIntPipe),不是实例。与守卫一样,可以传递一个实例。传递选项定制内置管道行为:

@Get(':id')
async findOne(
  @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
  id: number,
) {
  return this.catsService.findOne(id);
}

其他转换管道(Parse*管道)的方式类似。都在验证(路由参数、查询字符串参数、请求体值)。

查询字符串参数:

@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

使用ParseUUIDPipe解析参数并验证是否是UUID的示例。

@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
  return this.catsService.findOne(uuid);
}

使用ParseUUIDPipe()时,解析版本3,4或5中的UUID,可以在管道选项中传递版本。

参见验证技术获得验证管道的广泛示例

自定义管道【?】

获取输入值,返回相同的值。

//validation.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

PipeTransform<T, R>是管道必须的接口。泛型接口T表示输入value的类型,R表示transform()方法的返回类型。

管道必须实现transform()方法来实现PipeTransform接口契约。两个参数:

  • value
  • metadata

value方法参数,metadata方法参数的元数据。元数据对象有以下属性:

export interface ArgumentMetadata {
  type: 'body' | 'query' | 'param' | 'custom';
  metatype?: Type<unknown>;
  data?: string;
}

这些属性描述参数。

type指示参数是一个body @Body(), query @Query(), param @Param(), or a custom parameter(阅读更多这里)。
metatype参数的元类型,如String。路由方法中省略了类型声明,或使用普通的JavaScript,该值为undefined
data传递给装饰器的字符串,如@Body('string')。装饰符括号为空,则为“未定义”。

接口在编译中消失。如果方法参数类型声明为接口而不是类,metatype 值将是对象。

基于模式的验证

让管道验证更有用。CatsController控制器的create()方法,运行service方法之前,想要确保post body对象是有效的。

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@Body()参数createCatDto,类型是CreateCatDto:

//create-cat.dto.ts

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

确保create方法传入请求包含有效主体。必须验证createCatDto 对象成员。可以在路由处理方法中做,但会破坏单一责任规则 (SRP)

另种方法创建验证器类,并在那里委托任务。缺点是,必须在每个方法开头调用这个验证器。

验证中间件怎么样? 这可以工作,但不幸的是,不可能创建通用中间件,在应用程序所有上下文中使用。因为中间件不知道执行上下文,包括要调用的处理程序及其任何参数。

当然,这正是管道所设计的用例。让我们继续改进我们的验证管道。

对象模式验证

有几种方法以干净的DRY方式进行对象验证。一种常见的是使用基于模式的验证。让我们尝试这种方法。

Joi库使用可读的API创建模式。构建基于joi模式的验证管道。

首先安装所需的软件包:

$ npm install --save joi
$ npm install --save-dev @types/joi

下例中,创建了简单的类,将模式作为constructor 参数。然后应用schema.validate()方法,针对提供的模式验证传入参数。

如前所述,验证管道要么原封不动返回值,要么抛出异常。

下节中使用@UsePipes()装饰器为控制器方法提供适当的模式。这样可以使验证管道在上下文中可重用。

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private schema: ObjectSchema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = this.schema.validate(value);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

绑定验证管道

前面,看到了如何绑定转换管道(如ParseIntPipeParse*管道的其余部分)。

绑定验证管道也非常简单。

本例中,在方法级别绑定管道。做以下事情来使用JoiValidationPipe:

  1. 创建一个JoiValidationPipe实例
  2. 在管道类构造函数中传递特定于上下文的Joi模式
  3. 将管道绑定到方法

使用@UsePipes()装饰器来做到这一点,如下所示:

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@UsePipes()装饰器是从@nestjs/common包中导入的。

类验证器

**警告 **本节中需要TypeScript,如果应用使用普通JavaScript编写,则不可用。

验证技术的另一种实现。

class-validator。这个库基于装饰器验证。功能非常强大,特别与Pipe结合时,因为可以访问metatype。安装所需的包:

$ npm i --save class-validator class-transformer

CreateCatDto 类中添加装饰器。优势:CreateCatDto类仍然是 Post body object 的真实来源(而不必创建一个单独的验证类)。

//create-cat.dto.ts

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

阅读更多关于class-validator装饰器这里

创建一个使用这的ValidationPipe类。

//validation.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

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

上面,使用了class-transformer库。是由class-validator库作者编写,它们可以配合使用。

注意transform()方法标记为asyncclass-validator验证可以是async(利用Promises)。

接下来注意,提取metatype字段。

注意函数toValidate()。当参数是原生JavaScript类型时,它负责绕过验证步骤

接下来,使用plainToClass()将纯JavaScript参数对象转换为类型化对象,以便应用验证。

最后,由于这是一个验证管道,要么原封不动返回值,要么抛出异常。

最后一步绑定ValidationPipe。管道可以是(参数、方法、控制器或全局)作用域。下例中,管道绑定到路由处理程序@Body()装饰器。

//cats.controller.ts

@Post()
async create(
  @Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
  this.catsService.create(createCatDto);
}

验证只涉及一个指定参数时,参数作用域管道非常有用。

全局作用域管道

ValidationPipe创建为尽可能通用,设置全局作用域管道实现全部效用,应用到每个路由处理程序。

//main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

混合应用程序useGlobalPipes()方法不为网关和微服务设置管道。“标准”(非混合)微服务应用程序,useGlobalPipes()可用。

注意,在依赖注入方面,从任何模块外部注册的全局管道(使用useGlobalPipes(),如上例)不能注入依赖项,因为绑定已经在任何模块的上下文中完成。为了解决这个问题,直接从任何模块建立一个全局管道,使用下面的结构:

//app.module.ts

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: ValidationPipe,
    },
  ],
})
export class AppModule {}

内置的ValidationPipe

不必构建通用验证管道,内置的ValidationPipe提供更多选项,这里完整的细节和大量的例子。

转换用例

一个简单ParseIntPipe,字符串解析为整数。(有个内置的更复杂的ParseIntPipe; 下例为自定义转换管道的简单示例)。

//parse-int.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

绑定到所选的参数,如下:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return this.catsService.findOne(id);
}

使用请求中的id从数据库中选择一个现有用户实体:

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

接收输入值id返回输出值(UserEntity对象)。

提供默认值

Parse*管道需要参数值。接收到null or undefined时抛出异常。DefaultValuePipe提供一个默认值。

@Get()
async findAll(
  @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
  @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
  return this.catsService.findAll({ activeOnly, page });
}

守卫

守卫是用@Injectable()装饰器标注的类,实现了CanActivate接口。

img

守卫只有一个职责。决定请求是否由路由程序处理,取决于(如权限、角色、ACLs等)。称为授权authorization)。

守卫在中间件之后执行,在拦截器、管道之前执行。

授权守卫

已验证的用户(令牌附加到请求头)。

//auth.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

实现身份验证的示例,访问本章。更复杂的授权示例,查看本页

validateRequest()函数可以根据需要简单或复杂。

须实现canActivate()函数。返回布尔值,表示请求是否被允许。可以同步或异步返回响应(通过Promise or Observable)。

执行上下文

canActivate()只有一个参数,ExecutionContext实例。ExecutionContext继承自ArgumentsHost。上例中,使用了ArgumentsHost上的switchToHttp方法,获得Request对象的引用。更多,可回到 exception filters 章节 Arguments host 部分。

通过扩展ArgumentsHostExecutionContext也添加一些新的helper方法,提供额外细节。有助于构建更通用的守卫,更多这里

基于角色的认证

构建更强大的守卫,允许特定角色访问。从一个基本的守卫模板开始,接下来对其进行构建。目前,允许所有请求:

//roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

绑定守卫

管道、异常过滤器一样,守卫是(全局、控制器、方法)作用域。@UseGuards()设置控制器作用域守卫。单个或逗号分隔的参数。

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

@UseGuards()@nestjs/common导入。

上面传递了RolesGuard类型,实例化留给框架并启用依赖注入。与管道、异常过滤器一样,可以传递一个实例:

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

全局守卫,使用Nest应用实例的useGlobalGuards()方法:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

混合应用程序(hybrid application),useGlobalGuards()默认不会为网关、微服务设置守卫(更改此行为,参阅混合应用程序)。“标准”(非混合)微服务应用,useGlobalGuards()全局作用域设置守卫。

全局守卫用于每个路由。依赖注入方面,模块外部注册的全局守卫(如上useGlobalGuards())不能注入依赖。从任何模块建立全局守卫:

//app.module.ts

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

这种方法为守卫执行依赖注入时,无论在哪个模块中,守卫实际上是全局的。

每个处理程序设置角色

RolesGuard还不够智能。没有利用守卫特性 - 执行上下文

灵活、可重用的将角色与路由匹配? 自定义元数据 发挥作用(更多这里)。通过@SetMetadata()将自定义元数据附加到路由处理程序。

//cats.controller.ts

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@SetMetadata()装饰器是从@nestjs/common包中导入的。

roles元数据(roles键,['admin']值)附加到create()方法。路由中直接使用@SetMetadata()不好。可创建自定义装饰器,如下:

//roles.decorator.ts

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

更清晰,可读性更强,是强类型。自定义@Roles()装饰器。

//cats.controller.ts

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

把它放在一起

RolesGuard结合。获取路由的角色(自定义元数据),使用Reflector辅助类,@nestjs/core中公开。

//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 roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

假设request.user包含角色。能在自定义认证守卫(或中间件)中建立关联。查看本章了解更多。

matchRoles()根据需要简单或复杂。

反射器(Reflector)的更多细节,参阅Execution context章节的反射(Reflection)和元数据(metadata)部分。

没有权限用户请求端点时,自动返回以下响应:

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

守卫返回false时,框架会抛出ForbiddenException。希望返回不同的错误响应,应该抛出特定异常。

throw new UnauthorizedException();

守卫抛出的异常都由异常层(exceptions layer)处理(全局异常过滤器、当前上下文异常过滤器)。

授权示例,查看本章

拦截器

拦截器是一个带有@Injectable()装饰器的类,实现了NestInterceptor接口。

img

拦截器有一组有用的功能,这些功能受到面向切面编程 (AOP)技术的启发。

  • 在方法执行之前/之后绑定额外的逻辑
  • 转换函数返回的结果
  • 转换函数抛出的异常
  • 扩展基本函数行为
  • 完全覆盖一个函数根据特定条件(如,为了缓存目的)

基础

实现intercept()方法,两个参数。第一个ExecutionContext实例(与 guards 相同)。ExecutionContext继承ArgumentsHost

调用处理程序

第二个参数CallHandlerCallHandler接口实现了handle()方法,使用它在拦截器中调用路由处理程序。intercept()中不调用handle(),不会执行路由处理程序。

intercept()方法包装了请求/响应流。intercept()中编写代码,调用handle()之前执行,如何影响之后发生的事情?handle()返回一个Observable,使用RxJS操作符操作响应。handle()被称为切入点(Pointcut)

切面拦截

使用拦截器来记录用户交互(例如,存储用户调用、异步分派事件或计算时间戳)。

//logging.interceptor.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`)),
      );
  }
}

NestInterceptor<T, R>是一个通用接口,T表示Observable<T>的类型(支持响应流),RObservable<R>包装的值的类型。

拦截器,控制器,提供者,守卫等,可以通过constructor注入依赖。

handle()返回一个RxJS Observable,有很多操作流的操作符。上例,使用了tap()操作符,它在可观察流正常或异常终止时调用匿名日志记录函数,但不会干扰响应周期。

绑定拦截器

设置拦截器,使用@nestjs/common中的@UseInterceptors()装饰器。与pipesguards一样,可以是(控制器、方法、全局)范围。

//cats.controller.ts

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

调用GET /cats端点时,看到以下输出:

Before...
After... 1ms

也可以传递一个实例:

//cats.controller.ts

@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

全局拦截器,使用 useGlobalInterceptors()方法:

const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());

全局拦截器用于每个路由程序。在赖注入方面,使用useGlobalInterceptors(),不能注入依赖项,这是在任何模块上下文之外完成的。下面结构从任何模块中建立一个全局拦截器:

//app.module.ts

import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

响应映射

handle()返回Observable。该流包含从路由程序返回的值,RxJSmap()操作符轻松地更改它。

响应映射不适用于library-specific的响应策略(禁止使用@Res()对象)。

RxJSmap()操作符将响应对象分配给新对象data属性,返回新对象。

//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;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
        map( data => {
            return { data }
        })
    );
  }
}

intercept()前可添加异步 async

响应如下,路由返回[]

{
  "data": []
}

创建重用方案,每次出现的null转换为'',绑定到全局

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
        map( value => value === null ? '' : value )
    );
  }
}

异常映射

catchError()操作符覆盖抛出的异常:

//errors.interceptor.ts

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  BadGatewayException,
  CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
        catchError( err => {
            return throwError(() => {
                return new BadGatewayException()
            })
        }),
      );
  }
}

流覆盖

阻止调用处理程序。如,从缓存中返回值的缓存拦截器

//cache.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const isCached = true;
    if (isCached) {
      return of([]);
    }
    return next.handle();
  }
}

RxJSof()操作符创建新流。

更多操作符

RxJS操作符控制流为我们提供许多功能。处理路由请求的超时

//timeout.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          return throwError(() => new RequestTimeoutException());
        }
        return throwError(() => err);
      }),
    );
  };
};

可以在抛出RequestTimeoutException之前添加逻辑(如释放资源)。

自定义路由装饰器

Nest围绕装饰器构建的。更好地理解装饰器,阅读这篇文章。简单的定义:

ES2016装饰器是返回函数的表达式,可接受目标、名称和属性作为参数。前加@应用它。可为类、方法或属性定义装饰器。

参数装饰器

一组参数装饰器,与路由程序一起使用。装饰器和代表的Express(或Fastify)对象

@Request(), @Req()req
@Response(), @Res()res
@Next()next
@Session()req.session
@Param(param?: string)req.params / req.params[param]
@Body(param?: string)req.body / req.body[param]
@Query(param?: string)req.query / req.query[param]
@Headers(param?: string)req.headers / req.headers[param]
@Ip()req.ip
@HostParam()req.hosts

属性user附加到request对象。创建自定义@User()装饰器,在路由程序中提取它。

//user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

适合的地方使用它。

@Get()
async findOne(@User() user: UserEntity) {
  console.log(user);
}

传递数据

data将参数传给装饰器工厂函数。登录用户如下:

{
  "id": 101,
  "firstName": "Alan",
  "lastName": "Turing",
  "email": "alan@email.com",
  "roles": ["admin"]
}

属性名作为键,返回相关值或undefined(属性名不存在,或user对象未创建)。

//user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;

    return data ? user?.[data] : user;
  },
);

@User()装饰器访问一个属性:

@Get()
async findOne(@User('firstName') firstName: string) {
  console.log(`Hello ${firstName}`);
}

使用管道

user参数执行管道。管道应用到自定义装饰器:

@Get()
async findOne( @User(new ValidationPipe({ validateCustomDecorators: true })) user: UserEntity ) {
  console.log(user);
}

validateCustomDecorators 选项须设置为trueValidationPipe 默认不验证自定义装饰器注释的参数。

装饰器组合

一个方法组合多个装饰器。

//auth.decorator.ts

import { applyDecorators } from '@nestjs/common';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard, RolesGuard),
    ApiBearerAuth(),
    ApiUnauthorizedResponse({ description: 'Unauthorized' }),
  );
}

使用自定义@Auth()装饰器,如下:

@Get('users')
@Auth('admin')
findAllUsers() {}

一个声明应用所有四个装饰器。

@nestjs/swagger包的@ApiHideProperty()装饰器不可组合,不能用于applyDecorators函数。