Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • 深入浅出node

  • nest框架

    • 框架对比
    • 概述
      • 基本原理
    • koa

    • node
    • nest框架
    2022-05-06
    目录

    概述

    # 01. 第一步

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

    或者直接下载demo (opens new window)

    目录中包含几个核心文件

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

    # 02. 控制器

    控制器负责处理传入的请求并将响应返回给客户端

    控制器的目的是接收应用程序的特定请求。 路由机制控制哪个控制器接收哪个请求。 很多时候,每个控制器都有多个路由,不同的路由可以执行不同的操作。

    为了创建一个基本的控制器,我们使用类和装饰器。 装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)。

    提示

    为了快速创建带有内置验证的控制器,我们可以使用 CLI 的 命名:nest generate controller [name] 也可以使用 nest g co [name]。 支持\划分文件夹。 比如我们想把某个控制器放在 src/module目录的wsh文件夹下, 可以使用: nest g co module/wsh

    应用程序的根模块会自定引入该控制器。 不想要生成对应的测试文件可以追加 --no-spec

    # 路由

    在下面的例子中,我们使用 @Controller() 装饰器定义一个基本的控制器。可选 路由路径前缀设置为 cats。在 @Controller() 装饰器中使用路径前缀可以使我们轻松地对一组相关的路由进行分组,并最大程度地减少重复代码。例如,我们可以选择将一组用于管理与 /customers 下的客户实体进行互动的路由进行分组。这样,我们可以在 @Controller() 装饰器中指定路径前缀 customers,这样就不必为文件中的每个路由重复路径的那部分。

    import { Controller, Get } from '@nestjs/common';
    
    @Controller('cats')
    export class CatsController {
      @Get()
      findAll(): string {
        return 'This action returns all cats';
      }
    }
    
    

    findAll() 方法之前的 @Get() HTTP 请求方法装饰器告诉 Nest 为 HTTP 请求的特定端点创建处理程序。端点对应于 HTTP 请求方法(在本例中为 GET )和路由路径(如 GET /cats )。

    什么是路由路径 ? 一个处理程序的路由路径是通过连接为控制器 (Controller) 声明的(可选)前缀和请求装饰器中指定的任何路径来确定的。由于我们已经为每个 route(cats) 声明了一个前缀,并且没有在装饰器中添加任何路由信息,因此 Nest 会将 GET /cats 请求映射到此处理程序。如上所述,该路径包括可选的控制由路径前缀和请求方法装饰器中声明的任何路径字符串。例如,路径前缀 customers 与装饰器 @Get('profile') 组合会为 GET /customers/profile 请求生成路由映射。

    • Nest 使用两种不同的操作响应选项的概念
    标准(推荐) 使用这个内置方法,当请求处理程序返回一个 JavaScript 对象或数组时,它将自动序列化为 JSON。但是,当它返回一个 JavaScript 基本类型(例如string、number、boolean)时, Nest 将只发送值,而不尝试序列化它。这使响应处理变得简单:只需要返回值,其余的由 Nest 负责。
    类库特有的 我们可以在函数签名处通过 @Res() 注入类库特定的响应对象(例如, Express)。使用此方法,你就能使用由该响应对象暴露的原生响应处理函数。例如,使用 Express,您可以使用 response.status(200).send() 构建响应

    注意

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

    # Request

    处理程序有时需要访问客户端的请求细节。Nest 提供了对底层平台(默认为 Express)的请求对象(request)的访问方式。我们可以在处理函数的签名中使用 @Req() 装饰器,指示 Nest 将请求对象注入处理程序。

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

    提示

    为了在 express 中使用 Typescript (如 request: Request 上面的参数示例所示),请安装 @types/express 。

    Request对象代表 HTTP 请求,并具有查询字符串,请求参数参数,HTTP 标头(HTTP header) 和 正文(HTTP body)的属性。在多数情况下,不必手动获取它们。 我们可以使用专用的装饰器,比如开箱即用的 @Body()或@Query()。 下面是 Nest 提供的装饰器及其代表的底层平台特定对象的对照列表。

    @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
    

    为了与底层 HTTP 平台(例如,Express 和 Fastify)之间的类型兼容, Nest 提供了 @Res()和 @Response() 装饰器。@Res() 只是 @Response() 的别名。两者都直接暴露了底层平台的 response 对象接口。在使用它们时,还应该导入底层库的类型声明(如:@types/express)以充分利用它们。需要注意的是,在请求处理函数中注入 @Res()或 @Response() 时,会将 Nest 置于该处理函数的特定于库(Library-specific mode)的模式下,并负责管理响应。这样做时,必须通过调用 response 对象(例如,res.json(…) 或 res.send(…))发出某种响应,否则 HTTP 服务器将挂起。

    # 资源

    创建一个新记录的端点。为此,让我们创建一个POST处理程序。

    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 方法提供了相应的装饰器:@Put()、@Delete()、@Patch()、@Options()、以及 @Head()。此外,@All() 则用于定义一个用于处理所有 HTTP 请求方法的处理程序。

    # 路由通配符

    支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

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

    路由路径 'ab*cd' 将匹配 abcd 、ab_cd 、abecd 等。字符 ? 、+ 、 * 以及 () 是它们的正则表达式对应项的子集。连字符(-) 和点(.)按字符串路径逐字解析.

    # 状态码

    如前所述,默认情况下响应状态代码始终为 200,除了 201 的 POST 请求。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器轻松更改此行为。

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

    通常,状态码不是固定的,而是取决于各种因素。在这种情况下,您可以使用类库特有(library-specific)的 response (通过@Res()注入 )对象(或者在出现错误时,抛出异常)。

    # Headers

    要指定自定义响应标头,可以使用 @Header() 装饰器或特定于库的响应对象(并直接调用 res.header() )。

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

    # 重定向

    要将响应重定向到特定 URL,可以使用 @Redirect() 装饰器或特定于库的响应对象(并直接调用 res.redirect() )。

    @Redirect() 接受一个必需的 url 参数和一个可选的 statusCode 参数。 如果省略,statusCode 默认为 302。

      @Get('redirect')
      @Redirect('https://www.baidu.com', 301)
    

    有时可能希望动态确定 HTTP 状态代码或重定向 URL。 通过从以下的路由处理方法返回一个对象来做到这一点:

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

    返回的值将覆盖传递给 @Redirect()装饰器的所有参数。 例如

    @Get('docs')
    @Redirect('https://www.baidu.com', 302)
    getDocs(@Query('type') type) {
      if (type === 'news') {
        return { url: 'http://news.baidu.com/' };
      }
    }
    

    # 路由参数

    当需要接受动态数据作为请求的一部分时(例如,使用GET /cats/1 来获取 id 为 1 的 cat),带有静态路径的路由将无法工作。为了定义带参数的路由,我们可以在路由路径中添加路由参数标记以捕获请求 URL 中该位置的动态值。下面的 @Get() 装饰器示例中的路由参数标记演示了此用法。以这种方式声明的路由参数可以使用 @Param() 装饰器访问,该装饰器应添加到函数签名中。

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

    @Param() 用于修饰一个方法的参数(上面示例中的 params),并在该方法内将路由参数作为被修饰的方法参数的属性。如上面的代码所示,我们可以通过引用 params.id来访问(路由路径中的) id 参数。 您还可以将特定的参数标记传递给装饰器,然后在方法主体中按参数名称直接引用路由参数。

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

    # 子域路由

    @Controller 装饰器可以接受一个 host 选项,以要求传入请求的 HTTP 主机匹配某个特定值。

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

    提示

    由于 Fastify 缺乏对嵌套路由器的支持,因此当使用子域路由时,应该改用(默认) Express 适配器(Express adapter)。

    与路由路径类似,hosts 选项可以使用标记来捕获主机名中该位置的动态值。下面的 @Controller() 装饰器示例中的主机参数标记演示了这种用法。可以使用 @HostParam() 装饰器访问以这种方式声明的主机参数,该装饰器应添加到方法签名中。

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

    # 作用域

    对于来自不同编程语言背景的人来说,在 Nest 中得知几乎所有内容都是在传入请求之间共享的,这可能是出乎意料的。我们有一个到数据库的连接池、具有全局状态的单例服务等。请记住,Node.js 不遵循请求/响应多线程无状态模型,其中每个请求都由单独的线程处理。因此,使用单例实例对我们的应用程序来说是完全安全的。

    # 异步性

    每个异步函数都必须返回一个 Promise。这意味着可以返回 Nest 能够自行解析的延迟值。

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

    这是完全有效的。此外,通过返回 RxJS observable 流,Nest 路由处理程序将更加强大。 Nest 将自动订阅下面的源并获取最后发出的值(在流完成后)。

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

    # 请求负载

    DTO

    DTO 是一个对象,用于封装数据并从一个应用程序发送到另一个应用程序。 DTO帮助我们定义系统内的接口或输入与输出。 nest g class cats/dto/create-cat.dto --no-spec

    如果使用 TypeScript,我们需要确定 DTO(数据传输对象)模式。我们可以使用 TypeScript 接口或简单的类来确定 DTO 模式。有趣的是,我们建议在这里使用类。为什么?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中被保留为真实实体。另一方面,由于 TypeScript 接口在转译过程中被移除,Nest 无法在运行时引用它们。这很重要,因为 Pipes 等功能在运行时可以访问变量的元类型时提供了额外的可能性。

    创建 CreateCatDto 类:

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

    # 处理错误

    该示例利用几个可用的装饰器来创建基本控制器。 该控制器暴露了几个访问和操作内部数据的方法。

    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 依然不知道 CatsController 是否存在,所以它不会创建这个类的一个实例。

    控制器总是属于模块,这就是为什么我们在 @Module() 装饰器中包含 controllers 数组的原因。

    /* app.module.ts */
    import { Module } from '@nestjs/common';
    import { CatsController } from './cats/cats.controller';
    @Module({
      controllers: [CatsController],
    })
    export class AppModule {}
    
    

    我们使用 @Module() 装饰器将元数据附加到模块类中,现在,Nest 可以轻松反射出哪些控制器(controller)必须被使用。

    # 类库特有方式

    到目前为止,我们已经讨论了 Nest 操作响应的标准方式。操作响应的第二种方法是使用类库特有的响应对象(Response)。为了注入特定的响应对象,我们需要使用 @Res() 装饰器。为了对比差异,让我们来重写 CatsController:

    /* cats.controller.ts */
    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([]);
      }
    }
    

    尽管此方法有效,并且实际上通过提供对响应对象的完全控制(标头操作,特定于库的功能等)在某些方面提供了更大的灵活性,但应谨慎使用此种方法。通常来说,这种方式非常不清晰,并且有一些缺点。 主要的缺点是你的代码变得依赖于平台(因为不同的底层库在响应对象(Response)上可能具有不同的 API),并且更加难以测试(必须模拟响应对象等)。

    而且,在上面的示例中,失去与依赖于 Nest 标准响应处理的 Nest 功能(例如,拦截器(Interceptors) 和 @HttpCode()/@Header() 装饰器)的兼容性。要解决此问题,可以将 passthrough 选项设置为 true,如下所示:

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

    现在,就能与底层框架原生的响应对象(Response)进行交互(例如,根据特定条件设置 Cookie 或 HTTP 头),并将剩余的部分留给 Nest 处理。

    # 03.提供者

    Providers 是 Nest 的一个基本概念。许多基本的 Nest 类可能被视为 provider - service,repository, factory, helper 等等。 他们都可以通过 constructor 注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 Nest运行时系统。 Provider 只是一个用 @Injectable() 装饰器注释的类。

    控制器应处理 HTTP 请求并将更复杂的任务委托给 providers。Providers 是纯粹的 JavaScript 类,在其类声明之前带有 @Injectable()装饰器。

    @Injectable的三个作用域周期

    1. @Injectable({scope: Scope.DEFAULT}) 每个提供者可以跨多个类共享。提供者生命周期严格绑定到应用程序生命周期。一旦应用程序启动,所有提供程序都已实例化。默认情况下使用单例范围。
    2. @Injectable({scope: Scope.REQUEST}) 在请求处理完成后,将为每个传入请求和垃圾收集专门创建提供者的新实例
    3. @Injectable({scope: Scope.TRANSIENT}) 临时提供者不能在提供者之间共享。每当其他提供者向 Nest 容器请求特定的临时提供者时,该容器将创建一个新的专用实例

    对于大多数用例,建议使用单例范围。请求之间共享提供者可以降低内存消耗,从而提高应用程序的性能(不需要每次实例化类)。

    # Services

    创建一个简单的 CatsService 开始。该服务将负责数据存储和检索,其由 CatsController 使用,因此把它定义为 provider,是一个很好的选择。因此,我们用 @Injectable() 来装饰这个类 。

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

    CatsService 具有一个属性和两个方法的基本类。唯一的新特点是它使用@Injectable() 装饰器。该 @Injectable() 附加有元数据,因此 Nest 知道这个类是一个 Nest provider。需要注意的是,上面有一个 Cat 接口。看起来像这样:

    export interface Cat {
      name: string;
      age: number;
      breed: string;
    }
    

    CatsService 是通过类构造函数注入的。注意这里使用了私有的只读语法。这意味着我们已经在同一位置创建并初始化了 catsService 成员。

    # 依赖注入

    Nest 是建立在强大的设计模式, 通常称为依赖注入。

    在 Nest 中,借助 TypeScript 功能,管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,Nest 将 catsService 通过创建并返回一个实例来解析 CatsService(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):

    constructor(private readonly catsService: CatsService) {}
    

    # 作用域

    Provider 通常具有与应用程序生命周期同步的生命周期(“作用域”)。在启动应用程序时,必须解析每个依赖项,因此必须实例化每个提供程序。同样,当应用程序关闭时,每个 provider 都将被销毁。但是,有一些方法可以改变 provider 生命周期的请求范围。

    # 自定义提供者

    Nest 有一个内置的控制反转("IoC")容器,可以解决 providers 之间的关系。 此功能是上述依赖注入功能的基础,但要比上面描述的要强大得多。@Injectable() 装饰器只是冰山一角, 并不是定义 providers 的唯一方法。相反,可以使用普通值、类、异步或同步工厂。

    # 可选提供者

    有时,可能需要解决一些依赖项。例如,某个类可能依赖于一个配置对象,但如果没有传递,则应使用默认值。在这种情况下,关联变为可选的, provider 不会因为缺少配置导致错误。

    要配置 provider 是可选的,需要在 constructor 的参数中使用 @Optional() 装饰器。

    import { Injectable, Optional, Inject } from '@nestjs/common';
    @Injectable()
    export class HttpService<T> {
      constructor(
        @Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
      ) {}
    }
    

    # 基于属性的注入

    目前使用的技术称为基于构造函数的注入,即通过构造函数方法注入 providers。在某些非常特殊的情况下,基于属性的注入可能会有用。例如,如果顶级类依赖于一个或多个 providers,那么通过从构造函数中调用子类中的 super() 来传递它们就会非常烦人了。因此,为了避免出现这种情况,可以在属性上使用 @Inject() 装饰器

    import { Injectable, Inject } from '@nestjs/common';
    @Injectable()
    export class HttpService<T> {
      @Inject('HTTP_OPTIONS')
      private readonly httpClient: T;
    }
    

    # 注册提供者

    在 Nest 中注册该服务CatsService,以便它可以执行注入。 为此,我们可以编辑模块文件(app.module.ts),然后将服务添加到@Module()装饰器的 providers 数组中。

    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.service.ts
    │    └──cats.controller.ts
    ├──app.module.ts
    └──main.ts
    

    提示

    nest提供了几种自定义提供程序的方法:

    1. value based Provider

    假设我们正在Nest 容器中添加一个外库,或者我们用Mock对象代替服务的真实实现。

    @Module({
      providers: [{provid: catsService, useValue: ['布偶', '英短']}]
    })
    
    1. NonClassedbased Provider Token

    @Inject: 基于属性的注入

    1. Class Providers

    useClass:允许我们动态确定一个Token 应该解析到的Class。

    useClass: process.env.NODE_ENV === 'development' ? DevelopmentConfigService ? ProductionConfigService
    
    1. useFactory

    需要将提供者的值基于各种其它依赖项,值等。 用法一:

    @Module({
      providers: [{provid: catsService, useFactory: () => ['布偶', '英短']}]
    })
    

    用法二:

    @Injectable()
    export class CatBrandFactory {
        create() {
            return ['布偶', '英短']
        }
    }
    @Module({
        providers: [
            CatBrandFactory,{
            provide: 'coffee_brand',
            useFactory: (brandFactory: CatBrandFactory) => brandFactory.create(),
            inject: [CatBrandFactory]
        }],
    })
    
    1. leverage async Providers

    异步提供程序:比如在数据库连接前不接受任何请求 将async/await 与useFactory 语法结合。我们传递给Nest的工厂函数返回一个Promise,允许它等待任何异步任务。 在实例化任何依赖于this提供者的类之前,nest将等待承诺的解决。 假设我们注入数据库连接,然后查询:

    @Module({
        providers: [
            CatBrandFactory,{
            provide: 'coffee_brand',
            useFactory: async (connection: Connection): Primise<string[]> => {
              const catBands = await Promise.resolve(['布偶', '英短']);
              return catBands;
            },
            inject: [CatBrandFactory]
        }],
    })
    

    # 04. 模块

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

    每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。

    @module() 装饰器接受一个描述模块属性的对象:

    • providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
    • controllers 必须创建的一组控制器
    • imports 导入模块的列表,这些模块导出了此模块中所需提供者
    • exports 由本模块提供并应在其他模块中可用的提供者的子集。

    默认情况下,该模块封装提供程序。这意味着无法注入既不是当前模块的直接组成部分,也不是从导入的模块导出的提供程序。因此,可以从模块导出的提供程序视为模块的公共接口或API。

    # 功能模块

    CatsController 和 CatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule。

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

    创建了 cats.module.ts 文件,并把与这个模块相关的所有东西都移到了 cats 目录下。我们需要做的最后一件事是将这个模块导入根模块 (ApplicationModule)。

    最新目录结构:

    src
    ├──cats
    │    ├──dto
    │    │   └──create-cat.dto.ts
    │    ├──interfaces
    │    │     └──cat.interface.ts
    │    ├─cats.service.ts
    │    ├─cats.controller.ts
    │    └──cats.module.ts
    ├──app.module.ts
    └──main.ts
    

    # 共享模块

    在 Nest 中,默认情况下,模块是单例,因此可以轻松地在多个模块之间共享同一个提供者实例。

    实际上,每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 CatsService 实例。 我们需要把 CatsService 放到 exports 数组中,如下所示

    // cats.module.ts
    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 {}
    

    # 依赖注入

    提供者也可以注入到模块(类)中(例如,用于配置目的):

    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 readonly catsService: CatsService) {}
    }
    

    提示

    由于循环依赖性,模块类不能注入到提供者中。

    # 全局模块

    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 {}
    

    @Global 装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。 在上面的例子中,CatsService 组件将无处不在,而想要使用 CatsService 的模块则不需要在 imports 数组中导入 CatsModule

    # 动态模块

    动态模块: 和TypeOrmModule 根据options参数配置功能 Nest 模块系统包括一个称为动态模块的强大功能。此功能可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。

    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() 装饰器元数据中)定义提供程序,但此外-根据传递给方法的 entities 和 options 对象 forRoot() -公开提供程序的集合,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)@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 {}
    

    如果要依次重新导出动态模块,则可以 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 {}
    

    # 05. 中间件

    中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next() 中间件函数。 next() 中间件函数通常由名为 next 的变量表示。

    Nest 中间件实际上等价于 express 中间件。 下面是Express官方文档中所述的中间件功能:

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

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

    在函数中或在具有 @Injectable() 装饰器的类中实现自定义 Nest中间件。 这个类应该实现 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();
      }
    }
    

    # 依赖注入

    Nest中间件完全支持依赖注入。 就像提供者和控制器一样,它们能够注入属于同一模块的依赖项(通过 constructor )

    # 应用中间件

    中间件不能在 @Module() 装饰器中列出。我们必须使用模块类的 configure() 方法来设置它们。包含中间件的模块必须实现 NestModule 接口。我们将 LoggerMiddleware 设置在 ApplicationModule 层上。

    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');
      }
    }
    

    我们还可以在配置中间件时将包含路由路径的对象和请求方法传递给forRoutes()方法。我们为之前在CatsController中定义的/cats路由处理程序设置了LoggerMiddleware。我们还可以在配置中间件时将包含路由路径的对象和请求方法传递给 forRoutes()方法,从而进一步将中间件限制为特定的请求方法。在下面的示例中,请注意我们导入了 RequestMethod来引用所需的请求方法类型。

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

    提示

    可以使用 async/await来实现 configure()方法的异步化(例如,可以在 configure()方法体中等待异步操作的完成)。

    # 路由通配符

    路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

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

    以上路由地址将匹配 abcd 、 ab_cd 、 abecd 等。字符 ? 、 + 、 * 以及 () 是它们的正则表达式对应项的子集。连字符 (-) 和点 (.) 按字符串路径解析。

    # 中间件消费者

    MiddlewareConsumer 是一个帮助类。它提供了几种内置方法来管理中间件。他们都可以被简单地链接起来。forRoutes() 可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类。在大多数情况下,我们可能只会传递一个由逗号分隔的控制器列表。以下是单个控制器的示例:

    // 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.ts';
    @Module({
      imports: [CatsModule],
    })
    export class AppModule implements NestModule {
      configure(consumer: MiddlewareConsumer) {
        consumer
          .apply(LoggerMiddleware) // 该 apply() 方法可以使用单个中间件,也可以使用多个参数来指定多个多个中间件。
          .forRoutes(CatsController);
      }
    }
    

    有时我们想从应用中间件中排除某些路由。我们可以使用该 exclude() 方法轻松排除某些路由。此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由,如下所示:

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

    # 函数式中间件

    把 logger 转换成函数:

    // logger.middleware.ts
    
    export function logger(req, res, next) {
      console.log(`Request...`);
      next();
    };
    

    在AppModule使用:

    // app.module.ts
    consumer
      .apply(logger)
      .forRoutes(CatsController);
    

    # 多个中间件

    为了绑定顺序执行的多个中间件,我们可以在 apply() 方法内用逗号分隔它们。

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

    # 全局中间件

    一次性将中间件绑定到每个注册路由,我们可以使用由INestApplication实例提供的 use()方法

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

    # 06. 异常过滤器

    内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应。

    开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理类型 HttpException(及其子类)的异常。每个发生的异常都由全局异常过滤器处理, 当这个异常无法被识别时 (既不是 HttpException 也不是继承的类 HttpException ) , 用户将收到以下 JSON 响应:

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

    # 基础异常类

    Nest 提供了一个内置的 HttpException 类,从 @nestjs/common 包中公开。 对于典型的基于 HTTP REST/GraphQL API 的应用程序,最好在发生某些错误情况时发送标准 HTTP 响应对象。

    例如,在 CatsController 中,我们有一个 findAll() 方法(一个 GET 路由处理程序)。 让我们假设这个路由处理程序由于某种原因抛出了一个异常。 为了证明这一点,我们将对其进行硬编码,如下所示:

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

    HttpException 构造函数有两个必要的参数来决定响应:

    • response 参数定义 JSON 响应体。它可以是 string 或 object,如下所述。
    • status参数定义HTTP状态代码 (opens new window)。

    默认情况下,JSON 响应主体包含两个属性:

    {
        "statusCode": 403,
        "message": "Forbidden"
    }
    
    • statusCode:默认为 status 参数中提供的 HTTP 状态代码
    • message:基于状态的 HTTP 错误的简短描述

    仅覆盖 JSON 响应主体的消息部分,请在 response参数中提供一个 string。 要覆盖整个 JSON 响应主体,请在response 参数中传递一个object。 Nest将序列化对象,并将其作为JSON 响应返回。 第二个构造函数参数-status-是有效的 HTTP 状态代码。 最佳实践是使用从@nestjs/common导入的 HttpStatus枚举。

    覆盖整个响应正文的示例

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

    响应如下所示::

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

    # 自定义异常

    在许多情况下,我们不需要编写自定义异常,并且可以使用内置的 Nest HTTP 异常,如下一节所述。 如果确实需要创建自定义异常,最好创建自己的异常层次结构,其中自定义异常继承自基 HttpException 类。 通过这种方法,Nest 将识别您的异常,并自动处理错误响应。 让我们实现这样一个自定义异常: // forbidden.exception.ts

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

    由于 ForbiddenException 扩展了基础 HttpException,它将和核心异常处理程序一起工作,因此我们可以在 findAll()方法中使用它。

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

    # 内置HTTP异常

    为了减少样板代码,Nest 提供了一系列继承自核心异常 HttpException 的可用异常。所有这些都可以在@nestjs/common包中找到:

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

    # 异常过滤器

    虽然基本(内置)异常过滤器可以自动处理许多情况,但有时我们可能希望对异常层拥有完全控制权,例如,可能要添加日志记录或基于一些动态因素使用其他 JSON模式。 异常过滤器正是为此目的而设计的。 它们使我们可以控制精确的控制流以及将响应的内容发送回客户端。

    创建一个异常过滤器,它负责捕获作为HttpException类实例的异常,并为它们设置自定义响应逻辑。为此,我们需要访问底层平台 Request和 Response。我们将访问Request对象,以便提取原始 url并将其包含在日志信息中。我们将使用 Response.json()方法,使用 Response对象直接控制发送的响应。

    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() 装饰器绑定所需的元数据到异常过滤器上。它告诉 Nest这个特定的过滤器正在寻找 HttpException 而不是其他的。在实践中,@Catch() 可以传递多个参数,所以你可以通过逗号分隔来为多个类型的异常设置过滤器。

    # 参数主机

    catch()方法的参数: exception 参数是当前正在处理的异常对象。 host 参数是一个 ArgumentsHost 对象。 ArgumentsHost 是一个功能强大的实用程序对象。在此代码示例中,我们使用它来获取对传递给原始请求处理程序(在引发异常的控制器中)的请求和响应对象的引用。在此代码示例中,我们在 ArgumentsHost 上使用了一些辅助方法来获取所需的 Request 和 Response 对象。

    这种抽象级别的原因是 ArgumentsHost 在所有上下文中都起作用(例如,我们现在使用的 HTTP 服务器上下文,还有微服务和 WebSockets)。在执行上下文一章中,我们将看到如何利用 ArgumentsHost 及其辅助函数的强大功能访问任何执行上下文的适当底层参数。这将允许我们编写跨所有上下文操作的通用异常过滤器。

    # 绑定过滤器

    将 HttpExceptionFilter 绑定到 CatsController 的 create() 方法上

    // cats.controller.ts
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create(@Body() createCatDto: CreateCatDto) {
      throw new ForbiddenException();
    }
    

    @UseFilters() 装饰器和 @Catch()装饰器类似,它可以使用单个过滤器实例,也可以使用逗号分隔的过滤器实例列表。 我们创建了 HttpExceptionFilter 的实例。另一种可用的方式是传递类(不是实例),让框架承担实例化责任并启用依赖注入。

    // cats.controller.ts
    @Post()
    @UseFilters(HttpExceptionFilter)
    async create(@Body() createCatDto: CreateCatDto) {
      throw new ForbiddenException();
    }
    

    提示

    尽可能使用类而不是实例来应用过滤器。 它减少了内存使用,因为 Nest 可以轻松地在整个模块中重用同一类的实例.

    在上面的示例中,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 {}
    

    # 捕获异常

    为了捕获每一个未处理的异常(不管异常类型如何),将 @Catch() 装饰器的参数列表设为空,例如 @Catch()。

    过滤器将捕获抛出的每个异常,而不管其类型(类)如何。

    // any-exception.filter.ts
    import {
      ExceptionFilter,
      Catch,
      ArgumentsHost,
      HttpException,
      HttpStatus,
    } from '@nestjs/common';
    @Catch()
    export class AllExceptionsFilter implements ExceptionFilter {
      catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();
        const request = ctx.getRequest();
        const status =
          exception instanceof HttpException
            ? exception.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
        response.status(status).json({
          statusCode: status,
          timestamp: new Date().toISOString(),
          path: request.url,
        });
      }
    }
    

    # 继承

    为了将异常处理委托给基础过滤器,需要继承 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);
      }
    }
    

    提示

    继承自基础类的过滤器必须由框架本身实例化(不要使用 new 关键字手动创建实例)

    全局过滤器可以扩展基本过滤器。这可以通过两种方式来实现。

    1. 可以通过注入 HttpServer 来使用继承自基础类的全局过滤器。
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const { httpAdapter } = app.get(HttpAdapterHost);
      app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
      await app.listen(3000);
    }
    bootstrap();
    
    1. 使用 APP_FILTER token

    # 07. 管道

    管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

    管道有两个类型:

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

    在这两种情况下, 管道 参数(arguments) 会由 控制器(controllers)的路由处理程序 进行处理. Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。

    提示

    管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。

    # 内置管道

    Nest 自带六个开箱即用的管道,即

    • ValidationPipe
    • ParseIntPipe
    • ParseBoolPipe
    • ParseArrayPipe
    • ParseUUIDPipe
    • DefaultValuePipe

    让我们快速了解一下 ParseIntPipe 的使用。 这是转换用例的示例,其中管道确保将方法处理程序参数转换为 JavaScript 整数(或在转换失败时引发异常)。 在本章后面,我们将展示 ParseIntPipe 的简单自定义实现。 下面的示例技术也适用于其他内置转换管道(ParseBoolPipe、ParseFloatPipe、ParseEnumPipe、ParseArrayPipe 和 ParseUUIDPipe,我们将在本章中将其称为 Parse* 管道)。

    # 绑定管道

    要使用管道,我们需要将管道类的实例绑定到适当的上下文。 在我们的 ParseIntPipe 示例中,我们希望将管道与特定的路由处理程序方法相关联,并确保它在调用该方法之前运行。 我们使用以下构造来做到这一点,我们将其称为在方法参数级别绑定管道:

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

    这确保了以下两个条件之一为真:我们在 findOne() 方法中收到的参数是一个数字(正如我们对 this.catsService.findOne() 的调用中所预期的那样),或者在路由之前抛出异常 处理程序被调用。

    http://localhost:3000/cats/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* pipes)的工作方式类似。 这些管道都在验证路由参数、查询字符串参数和请求正文值的上下文中工作。 例如使用查询字符串参数:

    @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);
    }
    

    # 自定义管道

    如前所述,您可以构建自己的自定义管道。 虽然 Nest 提供了强大的内置 ParseIntPipe 和 ValidationPipe,但让我们从头开始构建每个的简单自定义版本,以了解如何构建自定义管道。

    我们从一个简单的 ValidationPipe 开始。 最初,我们将让它简单地接受一个输入值并立即返回相同的值,表现得像一个恒等函数。

    // validate.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() 方法。 这个方法有两个参数:

    • value
    • metadata

    value 是当前处理的参数,而 metadata 是其元数据。元数据对象包含一些属性:

    export interface ArgumentMetadata {
      type: 'body' | 'query' | 'param' | 'custom';
      metatype?: Type<unknown>;
      data?: string;
    }
    
    参数 描述
    type 告诉我们该属性是一个 body @Body(),query @Query(),param @Param()还是自定义参数。
    metatype 属性的元类型,例如 String。 如果在函数签名中省略类型声明,或者使用原生 JavaScript,则为 undefined。
    data 传递给装饰器的字符串,例如 @Body('string')。 如果您将括号留空,则为 undefined。

    提示

    TypeScript接口在编译期间消失,所以如果你使用接口而不是类,那么 metatype 的值将是一个 Object。

    # 基于模式的验证

    CatsController 下的的 create() 方法

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

    createCatDto 主体参数。 它的类型是 CreateCatDto:

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

    我们希望确保对 create 方法的任何传入请求都包含有效的正文。 所以我们必须验证 createCatDto 对象的三个成员。 我们可以在路由处理程序方法中执行此操作,但这样做并不理想,因为它会破坏单一责任规则 (SRP)。

    另一种方法可能是创建一个验证器类并在那里委派任务。 这样做的缺点是我们必须记住在每个方法的开头调用这个验证器。

    创建验证中间件? 不幸的是,不可能创建可在整个应用程序的所有上下文中使用的通用中间件。 这是因为中间件不知道执行上下文,包括将被调用的处理程序及其任何参数。

    在这种情况下,我们应该考虑使用管道。

    # 对象结构验证

    有几种方法可进行对象验证。 一种常见的方法是使用基于结构的验证。

    Joi库允许我们使用可读的 API 以直接的方式创建模式。 让我们构建一个使用基于 Joi 的验证管道。

    安装依赖:

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

    在下面的代码中,我们先创建一个简单的 class,在构造函数中传递 schema 参数. 然后我们使用 schema.validate() 方法验证.

    就像是前面说过的,验证管道 要么返回该值,要么抛出一个错误。

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

    # 绑定验证管道

    之前,我们看到了如何绑定转换管道(如 ParseIntPipe 和其余的 Parse* 管道)。

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

    在这种情况下,我们希望在方法调用级别绑定管道。 在我们当前的示例中,我们需要执行以下操作才能使用 JoiValidationPipe:

    • 创建 JoiValidationPipe 的实例
    • 在管道的类构造函数中传递特定于上下文的 Joi
    • 将管道绑定到方法

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

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

    # 类验证器

    Nest 与 class-validator (opens new window)配合得很好。 这个强大的库允许您使用基于装饰器的验证。 基于装饰器的验证非常强大,尤其是与 Nest 的 Pipe 功能结合使用时,因为我们可以访问已处理属性的元类型。 在开始之前,我们需要安装所需的软件包:

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

    安装这些后,我们可以向 CreateCatDto 类添加一些装饰器。 在这里,我们看到了这种技术的一个显着优势:CreateCatDto 类仍然是我们的 Post body 对象的单一事实来源(而不是必须创建一个单独的验证类)。

    // create-cat.dto.tsJS
    
    import { IsString, IsInt } from 'class-validator';
    
    export class CreateCatDto {
      @IsString()
      name: string;
    
      @IsInt()
      age: number;
    
      @IsString()
      breed: string;
    }
    

    现在我们来创建一个 ValidationPipe 类。

    // validation.pipe.tsJS
    
    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);
      }
    }
    

    让我们来看看这段代码。 首先,请注意 transform() 方法被标记为异步。 这是可能的,因为 Nest 同时支持同步和异步管道。 我们使这个方法异步,因为一些类验证器验证可以是异步的(利用 Promises)。

    接下来请注意,我们正在使用解构将元类型字段(仅从 ArgumentMetadata 中提取此成员)提取到我们的元类型参数中。 这只是获取全部ArgumentMetadata 然后有一个附加语句来分配元类型变量的简写。

    接下来,注意辅助函数 toValidate()。 当前正在处理的参数是原生 JavaScript 类型时,它负责跳过验证步骤(这些不能附加验证装饰器,因此没有理由通过验证步骤运行它们)。

    接下来,我们使用class-transformer 的 plainToClass()方法 将我们的普通 JavaScript 参数对象转换为类型化对象,以便我们可以应用验证。 我们必须这样做的原因是传入的 post body 对象在从网络请求反序列化时没有任何类型信息(这是底层平台(例如 Express)的工作方式)。 Class-validator 需要使用我们之前为 DTO 定义的验证装饰器,因此我们需要执行此类型转换以将传入的主体视为经过适当装饰的对象,而不仅仅是普通的 vanilla 对象。

    最后,如前所述,由于这是一个验证管道,它要么返回不变的值,要么抛出异常。

    最后一步是绑定 ValidationPipe。 管道可以是参数范围、方法范围、控制器范围或全局范围。 早些时候,在我们基于 Joi 的验证管道中,我们看到了在方法级别绑定管道的示例。 在下面的示例中,我们将管道实例绑定到路由处理程序 @Body() 装饰器,以便调用我们的管道来验证示例。

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

    当验证逻辑只涉及一个指定的参数时,参数范围的管道很有用。

    # 全局范围的管道

    由于 ValidationPipe 被创建为尽可能通用,我们可以通过将其设置为全局范围的管道来实现它的完整实用程序,以便将其应用于整个应用程序中的每个路由处理程序。

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

    ::: 注意 对于hybrid应用程序,useGlobalPipes() 方法不会为网关和微服务设置管道。 对于“标准”(非hybrid)微服务应用程序,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)的模块。 此外,useClass 不是处理自定义提供程序注册的唯一方法。

    # 内置的 ValidationPipe

    提示

    您不必自己构建通用验证管道,因为 ValidationPipe 由开箱即用的 Nest 提供。 内置的 ValidationPipe 提供了比我们在本章中构建的示例更多的选项,为了说明定制管道的机制,该示例保持基本状态。

    # 转换用例

    验证并不是自定义管道的唯一用例。 在本章开头,我们提到管道还可以将输入数据转换为所需的格式。 这是可能的,因为从转换函数返回的值完全覆盖了参数的先前值。

    这什么时候有用? 考虑到有时从客户端传递的数据需要经过一些更改——例如将字符串转换为整数——才能被路由处理程序方法正确处理。 此外,可能缺少一些必需的数据字段,我们希望应用默认值。 转换管道可以通过在客户端请求和请求处理程序之间插入处理功能来执行这些功能。

    这是一个简单的 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);
    }
    

    # 提供默认值

    Parse* 管道期望定义一个参数的值。 它们在接收到 null 或 undefined 值时抛出异常。 为了允许端点处理丢失的查询字符串参数值,我们必须提供一个默认值,以便在 Parse* 管道对这些值进行操作之前注入。 DefaultValuePipe 用于此目的。 只需在相关 Parse* 管道之前的 @Query() 装饰器中实例化 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 });
    }
    

    # 08. 守卫

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

    守卫有一个单一的责任。他们根据运行时的某些条件(如权限、角色、ACL等)来决定一个给定的请求是否会被路由处理程序处理。这通常被称为授权。在传统的Express应用程序中,授权(以及它的表亲,即通常与之合作的认证)通常由中间件来处理。中间件对于认证来说是一个很好的选择,因为像令牌验证和为请求对象附加属性这样的事情与特定的路由上下文(及其元数据)并无紧密联系。

    但是,中间件,就其本质而言,是dumb的。它不知道在调用next()函数后,哪个处理程序将被执行。另一方面,守卫可以访问ExecutionContext实例,因此知道下一步将执行什么。它们的设计很像异常过滤器、管道和拦截器,可以让你在请求/响应周期中的正确位置插入处理逻辑,而且是以声明的方式进行。这有助于保持你的代码的简洁性和声明性。

    提示

    守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

    # 授权

    如前所述,授权是守卫的一个很好的用例,因为只有当调用者(通常是一个特定的认证用户)有足够的权限时,特定的路由才能使用。我们现在要建立的AuthGuard假定有一个经过认证的用户(因此,在请求头文件中附有一个令牌)。它将提取并验证令牌,并使用提取的信息来确定请求是否可以继续。

    // auth.guard.tsJS
    
    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或Observable)。Nest使用返回值来控制下一个动作。

    • 如果它返回true,该请求将被处理。
    • 如果它返回false,Nest将拒绝该请求。

    # 执行上下文

    canActivate()函数需要一个参数,即ExecutionContext实例。ExecutionContext 继承自 ArgumentsHost。我们之前在异常过滤器章节中看到了ArgumentsHost。在上面的例子中,我们只是使用了定义在ArgumentsHost上的同样的辅助方法,就像我们之前在异常过滤器使用的那样,来获取对Request对象的引用。

    通过扩展ArgumentsHost,ExecutionContext还增加了几个新的辅助方法,提供关于当前执行过程的额外细节。这些细节有助于构建更多的通用守护,这些守护可以在广泛的控制器、方法和执行上下文中工作。

    # 基于角色的验证

    让我们建立一个功能更强的守卫,只允许具有特定角色的用户访问。我们将从一个基本的守卫模板开始,并在接下来的章节中对其进行构建。

    // roles.guard.tsJS
    
    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 {}
    

    上面,我们传递了RolesGuard类型(而不是一个实例),将实例化的责任留给了框架,并实现了依赖注入。与管道和异常过滤器一样,我们也可以传递一个实例。

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

    上面的构造将守卫附加到这个控制器所声明的每个处理程序上。如果我们决定守卫只限制于一个方法, 我们只需要在方法级别设置守卫。

    为了绑定全局守卫, 我们使用 Nest 应用程序实例的 useGlobalGuards() 方法:

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

    提示

    对于hybrid应用程序,useGlobalGuards() 方法不会为网关和微服务设置守卫。对于“标准”(非hybrid)微服务应用程序,useGlobalGuards()在全局安装守卫。

    全局守卫在整个应用程序中使用,用于每个控制器和每个路由处理程序。在依赖注入方面,从任何模块之外注册的全局守卫(如上面的例子中使用useGlobalGuards())不能注入依赖,因为这是在任何模块的上下文之外进行的。为了解决这个问题,你可以使用下面的结构直接从任何模块中设置一个守卫。

    // app.module.tsJS
    
    import { Module } from '@nestjs/common';
    import { APP_GUARD } from '@nestjs/core';
    
    @Module({
      providers: [
        {
          provide: APP_GUARD,
          useClass: RolesGuard,
        },
      ],
    })
    export class AppModule {}
    

    当使用这种方法为守卫进行依赖注入时,请注意,无论在哪个模块采用这种结构,守卫实际上都是全局的。这应该在哪里进行呢?选择定义了守卫(上面例子中的RolesGuard)的模块。另外,useClass并不是处理自定义提供者注册的唯一方法。

    # 设置每个处理程序的角色

    我们的RolesGuard正在工作,但它还不是很智能。我们还没有利用最重要的防护功能--执行环境。它还不知道角色,或者每个处理程序允许哪些角色。例如,CatsController可以为不同的路线提供不同的权限方案。有些可能只对管理员用户开放,而有些可能对所有人开放。我们怎样才能以一种灵活和可重用的方式将角色与路由相匹配?

    这就是自定义元数据发挥作用的地方。Nest提供了通过@SetMetadata()装饰器将自定义元数据附加到路由处理程序的能力。该元数据提供了我们缺失的角色数据,智能守卫需要这些数据来做出决定。

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

    通过上面的结构,我们将角色元数据(角色是一个键,而['admin']是一个特定的值)附加到create()方法中。虽然这很有效,但在你的路由中直接使用@SetMetadata()并不是好的做法。相反,创建你自己的装饰器,如下所示。

    // roles.decorator.ts
    
    import { SetMetadata } from '@nestjs/common';
    
    export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
    

    这种方法更简洁、更易读,而且是强类型的。现在我们有一个自定义的@Roles()装饰器,我们可以用它来装饰create()方法。

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

    # 组装

    现在让我们回去把它和我们的RolesGuard联系起来。目前,它在所有情况下都简单地返回true,允许每个请求继续进行。我们想在比较分配给当前用户的角色和当前正在处理的路由所要求的实际角色的基础上,使返回值成为条件。为了访问路由的角色(自定义元数据),我们将使用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);
      }
    }
    

    提示

    在node.js世界中,通常的做法是将授权用户附加到请求对象中。因此,在我们上面的示例代码中,我们假设 request.user 包含用户实例和允许的角色。在你的应用程序中,你可能会在你的自定义认证守卫(或中间件)中进行这种关联。

    当权限不足的用户请求一个端点时,Nest自动返回以下响应。

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

    实际上,返回 false 的守卫会抛出一个 HttpException 异常。如果您想要向最终用户返回不同的错误响应,你应该抛出特定异常

    throw new UnauthorizedException();
    

    由守卫引发的任何异常都将由异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理。

    # 09. 拦截器

    拦截器是一个用 @Injectable() 装饰器注解的类,它实现了 NestInterceptor 接口。

    拦截器具有一系列有用的功能,这些功能受到面向切面编程(AOP)技术的启发。它们使以下工作成为可能。

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

    # 基础

    每个拦截器都实现了intercept()方法,它需要两个参数。第一个是ExecutionContext实例(和guard(守卫)的对象完全一样)。ExecutionContext 继承自 ArgumentsHost。我们之前在异常过滤器一章中看到过ArgumentsHost。在那里,我们看到它是一个围绕着已经传递给原始处理程序的参数的包装器,并且根据应用程序的类型包含不同的参数数组。

    # 执行上下文

    通过扩展ArgumentsHost,ExecutionContext还增加了几个新的辅助方法,提供关于当前执行过程的额外细节。这些细节有助于构建更多的通用拦截器,这些拦截器可以在广泛的控制器、方法和执行上下文中工作。

    # 处理调用程序

    第二个参数是一个CallHandler。CallHandler接口实现了handle()方法,你可以用它来在拦截器的某个时刻调用路由处理方法。如果你不在拦截()方法的实现中调用handle()方法,路由处理程序方法就根本不会被执行。

    这种方法意味着intercept()方法有效地包装了请求/响应流。因此,你可以在执行最终路由处理程序之前和之后都实现自定义逻辑。很明显,你可以在你的intercept()方法中编写代码,在调用handle()之前执行,但你如何影响之后发生的事情?因为handle()方法返回一个Observable,我们可以使用强大的RxJS操作符来进一步操作响应。使用AOP的编程术语,路由处理程序的调用(即调用handle())被称为Pointcut,表明它是我们的附加逻辑被插入的点。

    比方说,有人提出了 POST /cats 请求。此请求指向在 CatsController 中定义的 create() 处理程序。如果在此过程中未调用拦截器的 handle() 方法,则 create() 方法不会被计算。只有 handle() 被调用(并且已返回值),最终方法才会被触发。为什么?因为Nest订阅了返回的流,并使用此流生成的值来为最终用户创建单个响应或多个响应。而且,handle() 返回一个 Observable,这意味着它为我们提供了一组非常强大的运算符,可以帮助我们进行例如响应操作。

    # 侧面拦截器

    使用一个拦截器来记录用户的交互:

    // 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> 的类型(在流后面),而 R 表示包含在返回的 Observable<R>中的值的返回类型。

    拦截器,可以像控制器、提供者、守护者等,通过其构造函数注入依赖关系。

    由于handle()返回一个RxJS的Observable,我们有大量的操作符可以用来操作流。在上面的例子中,我们使用了tap()操作符,它可以在可观察流的优雅或特殊终止时调用我们的匿名日志函数,但不会干扰响应周期。

    # 绑定拦截器

    为了设置拦截器,我们使用从@nestjs/common包导入的@UseInterceptors()装饰器。像管道和守卫一样,拦截器可以是控制器范围的,方法范围的,或者全局范围的。

    // cats.controller.tsJS
    
    @UseInterceptors(LoggingInterceptor)
    export class CatsController {}
    

    使用上述结构,CatsController中定义的每个路由处理程序都将使用LoggingInterceptor。当有人调用GET /cats端点时,你会在标准输出中看到以下输出。

    Before...
    After... 1ms
    

    注意,我们传递的是LoggingInterceptor类型(而不是一个实例),把实例化的责任留给了框架,并实现了依赖注入。就像管道、守卫和异常过滤器一样,我们也可以传递一个立即创建的实例。

    // cats.controller.tsJS
    
    @UseInterceptors(new LoggingInterceptor())
    export class CatsController {}
    

    如前所述,上面的结构将拦截器附在这个控制器声明的每个处理程序上。如果我们想把拦截器的范围限制在一个方法上,我们只需在方法层面上应用装饰器。

    为了设置一个全局拦截器,我们使用Nest应用程序实例的useGlobalInterceptors()方法。

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

    全局拦截器在整个应用程序中使用,用于每个控制器和每个路由处理器。在依赖注入方面,从任何模块外注册的全局拦截器(使用useGlobalInterceptors(),如上面的例子)不能注入依赖关系,因为这是在任何模块的上下文之外进行的。为了解决这个问题,你可以使用下面的结构直接从任何模块中设置一个拦截器。

    // app.module.tsJS
    
    import { Module } from '@nestjs/common';
    import { APP_INTERCEPTOR } from '@nestjs/core';
    
    @Module({
      providers: [
        {
          provide: APP_INTERCEPTOR,
          useClass: LoggingInterceptor,
        },
      ],
    })
    export class AppModule {}
    

    # 响应映射

    我们已经知道handle()返回一个Observable。这个流包含了从路由处理程序返回的值,因此我们可以很容易地使用RxJS的map()操作符对它进行改变。

    提示

    响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res() 对象)。

    让我们创建TransformInterceptor。它将使用RxJS的map()操作符将响应对象分配给一个新创建的对象的数据属性,将新对象返回给客户端。

    // 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 => ({ data })));
      }
    }
    

    调用GET /cats端点时,请求将如下所示(我们假设路由处理程序返回一个[]):

    {
        "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 ));
      }
    }
    

    # 异常映射

    另一个有趣的用例是利用RxJS的catchError()操作符来重写抛出的异常。

    // errors.interceptor.tsJS
    
    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 => throwError(() => new BadGatewayException())),
          );
      }
    }
    

    # Stream覆盖

    一个简单的缓存拦截器,它从缓存中返回它的响应

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

    我们的CacheInterceptor有一个硬编码的isCached变量和一个硬编码的响应[]。需要注意的是,我们在这里返回一个新的流,由RxJS的()操作符创建,因此路由处理程序根本不会被调用。当有人调用一个使用了CacheInterceptor的端点时,响应(一个硬编码的空数组)将被立即返回。为了创建一个通用的解决方案,你可以利用Reflector并创建一个自定义装饰器。

    # 更多操作符

    处理路由请求的超时。当你的端点在一段时间后没有返回任何东西,你想用一个错误响应来终止

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

    5秒后,请求处理将被取消。你也可以在抛出RequestTimeoutException之前添加自定义逻辑(例如,释放资源)。

    # 10. 自定义装饰器

    Nest是围绕着一种叫做装饰器 (opens new window)的语言特性而建立的。在很多常用的编程语言中,装饰器是一个众所周知的概念,但在JavaScript世界中,它们仍然相对较新。

    装饰器

    ES2016装饰器是一个表达式,它返回一个函数,并可以接受目标、名称和属性描述符作为参数。你可以通过在装饰器前加一个@字符来应用它,并将其放在你要装饰的东西的最上面。装饰器可以为一个类、一个方法或一个属性定义。

    # 参数装饰器

    Nest 提供了一组有用的参数装饰器,可以和 HTTP 路由处理程序一起使用。下面是一个所提供的装饰器和它们所代表的普通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
    

    #

    在node.js世界中,通常的做法是将属性附加到请求对象上。然后你在每个路由处理程序中手动提取它们,使用类似下面的代码。

    const user = req.user;
    

    为了使你的代码更加可读和透明,你可以创建一个@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"]
    }
    

    让我们定义一个装饰器,它以一个属性名称为键,如果存在则返回相关的值(如果不存在,或者用户对象没有被创建,则返回未定义的值)。

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

    可以使用这个相同的装饰器,用不同的键来访问不同的属性。如果用户对象很深或很复杂,这可以使请求处理程序的实现更容易和更可读。

    提示

    对于 TypeScript 用户,请注意这 createParamDecorator<T>() 是通用的。这意味着您可以显式实施类型安全性,例如 createParamDecorator<string>((data, ctx) => ...)或者,在工厂功能中指定参数类型,例如createParamDecorator((data: string, ctx) => ...) 。如果省略这两个,该类型 data 会 any。

    # 使用管道

    Nest 对待自定义的路由参数装饰器和这些内置的装饰器(@Body(),@Param() 和 @Query())一样。这意味着管道也会因为自定义注释参数(在本例中为 user 参数)而被执行。此外,你还可以直接将管道应用到自定义装饰器上:

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

    提示

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

    # 组装装饰器

    Nest提供了一个帮助方法来组合多个装饰器。例如,假设你想将所有与认证有关的装饰器组合成一个单一的装饰器。这可以通过以下结构完成。

    // auth.decorator.tsJS
    
    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函数正常工作。

    #nest
    框架对比
    基本原理

    ← 框架对比 基本原理→

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式