手记

NestJS核心概念详尽解析:从入门到精通

NestJS 是一个很好的框架,用于使用 JavaScript 或 TypeScript 来构建服务器端应用程序。

不过,在构建Nest应用时,会遇到很多概念,这些概念初看起来可能会让你觉得有些害怕。

在本文中,我们先从宏观角度了解一下框架结构。然后我们将逐一细看每个核心概念。

它们是用来做什么的,何时使用以及我们如何创造它们。

比如!

如果你还没有开始你的Nest项目,只需运行 nest new <name>。把它打开放在一边,我们开始!

概述

当用户发送请求时,通常会遵循以下流程:

应用的主模块 里,我们有包含控制器和提供者的子模块。子模块也可以包含其他模块,我为了简化图示部分,省略了这一点。

控制器(Controllers)、提供者(Providers)和模块(Modules)负责处理路由和实际请求的逻辑,其他部分则用于处理请求和响应过程中的细节。

如果你用过Express的话,你可能会发现像Guards这样的概念实际是由Middleware处理的。Nest则有所不同,我们接下来会更详细地聊聊这些。

控制器

我主要使用 Rust 和 Axum 开发后端应用,所以我认为它很像路由(Router)Router

控制器 负责处理。

  • 路由处理
  • 处理接收到的请求
  • 回复客户端

一个控制器可以包含多个子路由,每个子路由可以有各自不同的处理方式,每个处理方式都会完成特定的任务。

例如,我们有以下路由。我们将有两个 控制器,一个用于 pokemons,一个用于 types

    /pokemons  
    /pokemons/{名字}  

    /types  
    /types/{种类}

根路径(即 /)由 src/app.controller.ts 控制,在项目生成时自动生成的。如果我们运行 npm run start:dev 并访问 GET localhost:3000,我们将会看到返回 Hello World!

创建一个Controller

这些控制器基本上就是带有 @Controller 注解的类。

装饰器将元数据链接到类,使 Nest 能创建路由映射,将请求与相应控制器连接起来。

当然,我们也可以手动创建文件并写入代码,但是使用这样的[Nest CLI]会让一切变得简单得多!这里的[Nest CLI]是一个命令行工具。

创建一个叫作 pokemons(游戏名)的控制器

控制宝可梦

这将生成如下所示的文件。

    + src/宝可梦/宝可梦控制器.ts  
    + src/宝可梦/宝可梦控制器.spec.ts

并且在 src/app.module.ts 中修改代码,加入新创建的 Controller。稍后我们会更详细地讨论 Modules,所以你先别管它。

    // 源文件路径: src/app.module.ts  
    @模块({  
      导入: [],  
      控制器: [AppController, PokemonsController],  
      提供者: [AppService],  
    })  
    导出类 AppModule {}  // @模块装饰器用于定义模块,指定导入、控制器和提供者。

我们的 pokemons.controller.ts 文件应该像下面这样。

// src/pokemons/pokemons.controller.ts
import { Controller } from '@nestjs/common';

// @Controller 是 NestJS 框架中的装饰器,用于定义一个 controller,并指定其路由为 'pokemons'
@Controller('pokemons')
// PokemonsController 是一个处理与 Pokémon 相关请求的控制器
export class PokemonsController {}

Controller('pokemons') 表示我们即将在这个控制器里添加的所有子路径和方法都将位于 /pokemons 路径下,。

定义方法们

我们要再用装饰器给它添加一两个方法。

    import { Controller, Get, Post } from '@nestjs/common';
    import { baseUrl } from 'src/constants';

    @Controller('pokemons') // 基础路由
    export class PokemonsController {

        @Get() // 查询 /pokemons
        async listAll(): Promise<string> {
            return "这里列出所有的神奇宝贝!";
        }

        @Post()  // 新增 /pokemons
        async addPokemon(): Promise<string> {
            return '创建你自己的神奇宝贝!';
        }
    }

如果我们发送 GET localhost:3000/pokemons 请求,我们应该能看到返回的 string 回复。

除了常用的 @Get()@Post(),我们还有这些方法: @Put()@Delete()@Patch()@Options()@Head()@All(),来定义一个能够处理所有其他方法的端点,

嵌套路由

当然,我们可以在 PokemonController 中通过传递路径字符串 string 给装饰器来定义子路由。

    @Get("favorite") // 获取用户的最爱  
    async getFavorite(): Promise<string> {  
        return "我的最爱是皮卡丘"  
    }
带有通配符的路由:

路由:

还可以在设定路由规则时使用通配符。

比如说,如果我们把 listAll 方法改为如下,所有针对 /pokemon/*GET 请求都将被映射到这里。

    @Get("*") // 获取 /pokemons/* 路径
    async listAll(): Promise<any[]> {  
        return "这里就是所有的宝可梦信息!"  
    }

然而,在这种情况下,我们的 getFavorite 永远无法被调用,并且 GET /pokemon 将不再映射到任何处理函数。

单一处理者,多条路径

我们可以用一个处理器(handler)处理多个路径,只需将字符串数组传递给装饰器(decorator)即可。

    @Get(["", "*"]) // 获取比如 /pokemons 和 /pokemons/*  
    async listAll(): Promise<string[]> {  
        return this.pokemonService.listAll()  
    }

这样,这将会将 GET /pokemon 以及 GET /pokemon/* 都映射到 listAll 函数

参数提取器

生活其实比仅仅提一个没有任何具体要求的请求要复杂得多啦!

要从HTTP请求中提取参数,我们可以使用类似 @Body()@Query() 的装饰器。这里是一些常用的装饰器及其对应的平台特定对象,例如:@Body() 对应于请求体,@Query() 对应于查询参数。

需要注意的是,使用 Express 类型定义时,确保从 @types/express 导入这些类型。

    import { Request, Response } from 'express'; // 引入express中的Request和Response对象

这里有几个例子来展示我们如何使用这些东西。

查询

     @Get() // 获取 /pokemons 接口
    async listAll(@Query('version') version: string | null): Promise<string> {
        return `这是 ${version ?? '所有'} 版本的神奇宝贝。`
    }

     @Get(":name")  
    async getSingle(@Param() params: {name: string}): Promise<string> {  
        return `获取名叫 ${params.name} 的宝可梦`  
    }

你有什么请求吗?

import { Request } from 'express'

@Get("agent")
async getWithAgent(@Req() request: Request): Promise<string> {
    return "返回为用户代理 " + request.headers['user-agent'] + " 获取的宝可梦"
}
静态状态代码

默认情况下,响应的状态码通常是200,不过如果是POST请求,状态码会是201

为了提供自定义的静态HTTP状态码,我们可以在处理函数级别使用 @HttpCode(...) 注解来自定义HTTP状态码。

    @Post()  // post /pokemons  
    @HttpCode(204)  
    async addPokemon(): Promise<string> {  
      return '创建你自己的神奇宝贝吧,';  
    }

可以试试用上面的 @Res() 提取器来返回动态响应代码。

import { Response } from 'express';  

// ...  
@Get("response")  // 获取 /response 捕获  
async getWithResponse(@Res() response: Response): Promise<响应> {  
    return response.status(200).json("使用响应注入获取宝可梦");  
}

这种方法有几个缺点。

  1. 平台相关的
  2. 失去与Nest特性兼容性,这些特性依赖于标准响应处理,比如拦截器功能

为了解决这个问题,启用 passthrough(直通选项)

    @Get("response")  // 获取 /pokemons/response 接口  
    async getWithResponse(@Res({ passthrough: true }) response: Response): Promise<string> {  
        response.status(200);  
        return "使用响应注入获取 Pokémon。"  
    }

采用这种方法,我们可以与原生响应对象进行交互(例如,根据特定条件设置 cookie 或自定义 header),而 Nest 框架仍可继续处理其他部分。

这里有一个代码片段

这里有一段代码用于这部分。

    import { Controller, Get, HttpCode, HttpStatus, Param, Post, Query, Req, Res } from '@nestjs/common';  
    import { Request, Response } from 'express'  

    @Controller('pokemons') // 基础路由  
    export class PokemonsController {  

        @Get() // 获取 /pokemons  
        async listAll(@Query('version') version: string | null): Promise<string> {  
            return `这里是版本为 ${version ?? '所有'} 的宝可梦列表!`  
        }  

        @Get("response")  // 获取 /pokemons/response  
        async getWithResponse(@Res({ passthrough: true }) response: Response): Promise<string> {  
            response.status(200);  
            return "注入响应的宝可梦获取。"  
        }  

        @Get("favorite") // 获取 /pokemons/favorite  
        async getFavorite(): Promise<string> {  
            return "我最喜欢的宝可梦是皮卡丘"  
        }  

        @Get(":name") // 获取 /pokemons/:name  
        async getSingle(@Param() params: {name: string}): Promise<string> {  
            return `获取名为 ${params.name} 的宝可梦`  
        }  

        @Get("agent")  // 获取 /pokemons/agent  
        async getWithAgent(@Req() request: Request): Promise<string> {  
            return `为用户代理字符串 ${request.headers['user-agent']} 获取宝可梦`  
        }  

        @Post()  // POST /pokemons  
        @HttpCode(204)  
        async addPokemon(): Promise<string> {  
          return '快来创建你自己的宝可梦吧!';  
        }  
    }

如果你想的话,也可以跳过这篇文章的其余部分,直接开始搭建你的后端应用,完全没有问题!

看看就知道,我们就能使用不同的方法添加路由和子路由,处理这些请求,提取参数这项操作,用我们自己喜欢的方式来返回响应!

不过!

没有问题 ≠ 最好的选择
或者可以改为:没有问题并不意味着这是最好的选择。

咱们来瞧瞧那些其他不太小的概念,会让我们的应用更棒!

一个服务商

提供者是Nest的核心概念。许多基本的Nest类也被归类为提供者,例如服务、仓库、工厂和辅助类。提供者背后的关键概念是,它可以被注入为依赖项,比如它可以被注入为依赖项,从而让对象之间可以建立各种关系。

我主要用它作为管理者角色,将处理相关的逻辑从“Controllers”中分离,以将与处理相关的逻辑从Controllers中分离出来。

(Note: The output is slightly adjusted to better fit the suggestions while maintaining the source context. The repetition of "从...中分离" is kept to match the source's emphasis but might sound redundant in fluent Chinese. A more natural way would be:
我主要用它作为管理者角色,将处理相关的逻辑从“Controllers”中分离出来。)

Final Adjusted Translation:
我主要用它作为管理者角色,将处理相关的逻辑从“Controllers”中分离出来。

我们将主要关注服务,这是最具代表性的一种服务。

创建提供方

这里我们又一次会用到命令行。

提供 g 服务,管理宝可梦

将创建以下文件

    + src/宝可梦/宝可梦服务.service.ts  
    + src/宝可梦/宝可梦服务.spec.ts

并修改 src/app.module.ts 以包含新的提供器。

    // 注释:src/app.module.ts  
    @模块({  
      导入: [],  
      控制器: [AppController, PokemonsController],  
    - 提供者: [AppService],  
    + 提供者: [AppService, PokemonsService],  // 添加了PokemonsService  
    })  
    export class AppModule {}

让我们为我们的 PokemonsService 添加几个功能点,看看我们怎么在控制器里用它们。

    // 文件路径: src/pokemons/pokemons.service.ts  
    import { Injectable } from '@nestjs/common';  

    @Injectable()  
    export class Pokemon服务 {  
        private readonly pokemons: string[] = [];  

        创建(pokemon: string) {  
            this.pokemons.push(pokemon);  
        }  

        列出所有(): string[] {  
            return this.pokemons;  
        }  
    }
依赖注入

要将服务注入我们的控制器构造函数,这很简单,添加 constructor(private pokemonService: PokemonsService) {},即可。

我们就能在处理器中使用该服务。

    import { Body, Controller, Get, HttpCode, Post } from '@nestjs/common';  
    import { PokemonsService } from './pokemons.service';  

    @Controller('pokemons') // 路由基础  
    export class PokemonsController {  
        constructor(private pokemonService: PokemonsService) {}  

        @Get() // 查询 /pokemons  
        async listAll(): Promise<string[]> {  
            return this.pokemonService.listAll()  
        }  

        @Post()  // POST /pokemons 表示添加操作  
        @HttpCode(204)  
        async addPokemon(@Body() params: {pokemon: string}): Promise<string> {  
            this.pokemonService.add(params.pokemon)  
            return `宝可梦 ${params.pokemon} 已成功添加!`;  
        }  
    }

当然,我们可以根据需要进一步添加其他关键字到服务中。例如,如果我们不想让属性被修改,我们可以将它标记为 readonly。例如:constructor(private readonly pokemonService: PokemonsService) {}

模块

我们已经看到我们的根模块 ModuleAppModulesrc/app.module.ts 中被修改过几次了,终于到了仔细看看它的时候了!

首先,根模块(root模块)。它作为Nest应用图构建的起点。这个图是Nest用来解析模块和提供者之间关系及它们的依赖的内部结构。

@Module() 装饰器定义的模块类具有以下编程属性。

  • import:列出导入的模块,这些模块导出了此模块所需要的一些提供者
  • providers:可以在这个模块中至少共享的提供者
  • controllers:在此模块中定义的控制器列表
  • exports:此模块提供的providers子集,可以在其他导入此模块的模块中使用

正如我们所见,我们并不一定需要除根模块(root模块)之外的其他模块,但确实建议这样做,以便我们能更好地根据组件的功能和职责来组织它们。

没有模块

只是为了演示一下,我们创建几个其他的控制器和服务。

    nest g co types // 创建 TypesController: 创建控制器  
    nest g s types // 创建 TypesService: 创建服务  
    nest g co abilities // 创建 AbilitiesController: 创建控制器  
    nest g s abilities // 创建 AbilitiesService: 创建服务

以下是我们最近修改的 app.module.ts 文件。

// 源文件路径: src/app.module.ts
import { 模块 } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PokemonsController } from './pokemons/pokemons.controller';
import { PokemonsService } from './pokemons/pokemons.service';
import { TypesController } from './types/types.controller';
import { TypesService } from './types/types.service';
import { AbilitiesController } from './abilities/abilities.controller';
import { AbilitiesService } from './abilities/abilities.service';

@模块({
  导入: [],
  控制器: [AppController, PokemonsController, TypesController, AbilitiesController],
  提供者: [AppService, PokemonsService, TypesService, AbilitiesService],
})
export class AppModule {}

正如你看到的(或想象的),当我们不断增加控制器和服务时,controllersproviders 属性将会变得很长且混乱不堪……

带有模块的

为了处理这个问题,我们可以为每个能力模块创建一个模块。

    生成 pokemons 模块  
    生成 types 模块  
    生成 abilities 模块

然后使用新创建的模块来包含相应的控制器和服务提供者。

宝可梦插件

    // src/pokemons/pokemons.module.ts
    // 这是 Pokemons 模块的定义文件, 定义了 PokemonsModule 类。
    // 这里导入了 @nestjs/common 的 Module 类, 并定义了 PokemonsModule 类, 在其中配置了控制器和提供者。
    import { Module } from '@nestjs/common';
    import { PokemonsController } from './pokemons.controller';
    import { PokemonsService } from './pokemons.service';

    @Module({
        imports: [],
        controllers: [PokemonsController],
        providers: [PokemonsService],
    })
    export class PokemonsModule {}

TypesModule (类型模块)

// 源文件位置: src/types/pokemons.module.ts
import { Module } from '@nestjs/common';  // 导入 Module 类 from @nestjs/common
import { TypesController } from './types.controller';  // 导入 TypesController 类 from 当前文件夹下的 types.controller
import { TypesService } from './types.service';  // 导入 TypesService 类 from 当前文件夹下的 types.service

@Module({  // @Module({...}) 注: 模块装饰器
    imports: [],  
    controllers: [TypesController],  
    providers: [TypesService],  
})  
export class TypesModule { }  // 导出 类 TypesModule

能力模块

// src/能力/abilities.module.ts
import { Module } from '@nestjs/common';
import { AbilitiesController } from './abilities.controller';
import { AbilitiesService } from './abilities.service';

@Module({
    imports: [],
    controllers: [AbilitiesController],
    providers: [AbilitiesService],
})
export class AbilitiesModule { }

现在我们可以将我们的AppModule重构如下。

    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { PokemonsModule } from './pokemons/pokemons.module';
    import { TypesModule } from './types/types.module';
    import { AbilitiesModule } from './abilities/abilities.module';

    @Module({
      imports: [PokemonsModule, TypesModule, AbilitiesModule], // 导入需要的模块
      controllers: [AppController], // 注册控制器
      providers: [AppService], // 注册服务提供者
    })
    export class AppModule { } // 导出应用模块类

当然,就像我们在应用模块中导入这些模块一样,如果我们的服务是提供各种动画的信息,我们也可以创建一个模块,将所有与宝可梦相关的模块集合在一起。我们还可以进一步细分这些模块。

// src/宝可梦信息/宝可梦信息模块.ts
import { Module } from '@nestjs/common';
import { AbilitiesModule } from 'src/宝可梦能力/宝可梦能力模块';
import { PokemonsModule } from 'src/宝可梦/宝可梦模块';
import { TypesModule } from 'src/宝可梦类型/宝可梦类型模块';

@Module({
    imports: [PokemonsModule, TypesModule, AbilitiesModule]
})
export class PokemonInfoModule { }
这定义了一个名为宝可梦信息模块的类,它导入了宝可梦模块、类型模块和能力模块。

然后把这东西导入到我们这里的 AppModule 里。

    // src/app.module.ts  
    import { 模块 } from '@nestjs/common';  
    import { AppController } from './app.controller';  
    import { AppService } from './app.service';  
    import { PokemonInfoModule } from './pokemon-info/pokemon-info.module';  

    @模块({  
      imports: [PokemonInfoModule],  // 导入PokemonInfo模块  
      controllers: [AppController],  // 注册AppController控制器  
      providers: [AppService],       // 注册AppService服务  
    })  
    export class AppModule { }       // 定义AppModule类
更改事项:

我们应用的图表在非常高层次上是怎么变化的?

就这样。

这样。

不仅如此,还有更多!

当然,我们还可以继续。

中间件

这基本上与Express中的中间件相同,可以访问请求和响应对象,以及使用next()函数,所以这里我就不多说了。

这篇文章变得越来越长,我发现每个字都打得慢!

我们可以自己创建中间件或使用第三方中间件。关于常用的第三方中间件,可参考:第三方中间件

创建中件层

我们来创建一个简单的日志工具。

nest g middleware middleware/日志记录

再加一些代码里的 console.log

    // src/middleware/logger/logger.middleware.ts
    import { Injectable, NestMiddleware } from '@nestjs/common';

    @Injectable()
    export class LoggerMiddleware implements NestMiddleware {
      use(req: any, res: any, next: () => void) {
        console.log('请求: ', req);  // 请求
        console.log('响应: ', res);  // 响应
        next();
      }
    }
全球适用

如果我们想为所有已注册的路由绑定中间件,我们可以使用 INestApplication 实例上的 use() 方法来绑定中间件。

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

    // 异步函数启动应用
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.use(LoggerMiddleware); // 使用LoggerMiddleware
      // 监听端口,使用环境变量中的PORT或者默认端口3000
      await app.listen(process.env.PORT ?? 3000);
    }
    bootstrap();
适用于特定路线

比如,我们要把中间件应用到所有定义在 PokemonsControllerTypesController 的路由上。

// src/app.module.ts
// ...
export class AppModule implements NestModule {
  /**

* 配置中间件,将日志中间件应用于PokemonsController和TypesController。

* @param consumer 中间件消费者
   */
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) // 应用日志中间件
      .forRoutes(PokemonsController, TypesController); // 配置路由
  }
}

这就相当于。

consumer  
  .apply(LoggerMiddleware)  
  .forRoutes("pokemons", "types")
  // 这段代码使用consumer应用LoggerMiddleware中间件,并为其路由“pokemons”和“types”设置。
应用到具体方法
导出一个名为 AppModule 的类,实现 NestModule 接口。{
  配置一个中间件消费者 (consumer: MiddlewareConsumer) {  
    consumer  
      .应用日志中间件 (LoggerMiddleware)  
      .适用于路由 ({ 路径: "pokemons", 方法: RequestMethod.GET });  
  }  
}
排除特定的技巧或方法

    export class AppModule implements NestModule {  
      // 导出 AppModule 类,实现 NestModule 接口
      configure(consumer: MiddlewareConsumer) {  
        // 配置中间件消费者
        consumer  
          .apply(LoggerMiddleware) // 应用日志中间件
          .exclude({ path: "pokemons", method: RequestMethod.GET }) // 排除路径 "pokemons" 和 GET 请求方法
          .forRoutes(PokemonsController); // 为精灵控制器设置路由
      }  
    }
使用通配符 (WildCard)

就像我们在控制器中定义路由一样,我们同样可以在路径中使用通配符来匹配路径中任意字符的组合。

例如,下面这种情况差不多等于在整个app上全局生效。

export class AppModule implements NestModule {  
  configure(consumer: MiddlewareConsumer) {  
    consumer  
      .apply(日志中间件)  
      .forRoutes("为所有路由应用");  
  }  
}
多个中间层

为了将多个顺序执行的中间件绑定在一起,只需在 apply() 方法中传入一个用逗号分隔的中间件列表。

export class AppModule implements NestModule {  
  configure(consumer: MiddlewareConsumer) {  
    consumer  
      .apply(LoggerMiddleware, cookieParser())  
      .forRoutes("*");  
  }  
}

// 以下是代码的功能解释:
// AppModule 类实现了 NestModule 接口,该接口用于配置 NestJS 应用程序中间件。
// 在 configure 方法中,我们应用了 LoggerMiddleware 和 cookieParser 中间件,并将其应用于所有路由。

异常过滤

异常过滤器 是一个层用于处理应用程序中所有未处理异常。

使用内置的筛选功能

让我们先来查看一下内置的全局异常过滤器,该过滤器处理HttpException及其所有子类。当异常未被识别时,也就是说既不是HttpException也不是继承自HttpException的类,此过滤器会生成一个默认的JSON响应,如下所示。

{
  "statusCode": 500,
  "message": "服务器发生内部错误"
}
抛出标准的异常

我们可以使用来自 @nestjs/common 包的内置 HttpException 类来抛出标准异常。

    @Get("抛出")  
    async 异常抛出(): Promise<string[]> {  
        throw new HttpException('Some Message', HttpStatus.FORBIDDEN);  
    }

当调用该端点时,响应将如下。

{
    "statusCode": 403,  
    "message": "某些消息"
}

在这个示例中,我们仅重写了 JSON 响应体中的 message 部分,而没有提供错误的原因 cause,。

但我们也完全可以覆盖整个响应体内容,并提供类似的错误信息。

    @Get("throw")  
    async throwOnList(): Promise<string[]> {  
        throw new HttpException({  
            status: HttpStatus.FORBIDDEN,  
            error: '我会抛出任何东西来!',  
        }, HttpStatus.FORBIDDEN, {  
            cause: "原因: 这条路由是用于抛出异常的!",  
            status: "状态: 禁止",  
            error: "错误: 我会抛出任何东西来!"  
        });  
    }
创建自定义异常筛选器

直到现在,我们一直在使用内置的异常筛选器,但也可以创建自己的异常筛选器,以实现对异常处理层的完全控制。

例如,这可能在我们需要添加日志记录或根据某些动态因素选择不同的 JSON 模式时很有用。

我们来造一个能搞定所有异常的,不管是Http还是其他类型的异常都行。

    nest g 过滤器 异常过滤器/自定义异常过滤器

并将我们的 CustomExceptionFilter 修改如下。

    // 自定义异常过滤器
    import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
    import { Request, Response } from 'express';

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

        response
          .status(400)
          .json({
            timestamp: new Date().toISOString(),
            path: request.url,
            message: "这是一个被过滤的消息"
          });

      }
    }

@Catch(...) 装饰器将所需的元数据绑定到异常过滤器功能。它告诉系统此过滤器处理的异常类型,比如 @Catch(HttpException) 表示此过滤器仅会捕获 HttpException。若要为多种类型设置过滤器,只需传入逗号分隔的列表即可。如果我们不传入任何参数,我们的过滤器会捕获所有类型的异常。

catch() 方法内部,我们首先捕获到的是 exception 异常。这里我们使用泛型 T,因为我们是在捕获所有类型的异常。例如,如果我们使用 @Catch(HttpException),这里的类型将会是 HttpException

然后我们有一个超级强大的ArgumentsHost对象。我们已经使用它来获取上面代码中的RequestResponse对象,但它还能做更多的事情!如果你对此感兴趣,请查看执行上下文页面!

过滤器绑定

就像处理中间件一样,我们也可以为特定方法、控制器,甚至整个应用设置异常过滤器。

特定方法:

    // src/宝可梦/宝可梦控制器.ts
    @Get("throw")
    @UseFilters(CustomExceptionFilter)
    async 投掷列表(): Promise<string[]> {
        // ...
    }

这和用实例代替相同。

// 使用自定义异常过滤器
@UseFilters(new CustomExceptionFilter())

然而,当我们通过实例应用时,我们可以传递额外的参数,例如 HttpAdapterHost,如这里所述 此处

和我们上面用过的其他装饰器一样,@UseFilters() 装饰器可以接受一个过滤器实例,或多个用逗号分隔的过滤器实例,用于应用多个异常过滤器实例。

给特定控制器

    // src/pokemons/pokemons.controller.ts
    @Controller('pokemons') // 基础路径
    @UseFilters(CustomExceptionFilter)
    export class PokemonsController {}

全球适用

    // 创建应用实例并设置全局异常过滤器
    const app = await NestFactory.create(AppModule);
    // 自定义异常过滤器
    app.useGlobalFilters(CustomExceptionFilter);

注意哦!

当将一个通配过滤器与一个特定过滤器结合时,应先声明通配过滤器,以便特定过滤器能正确处理绑定类型。

我们还可以创建抛出异常,但是由于在大多数情况下HttpException已经足够,这里就不再讲解了!

管子

另一个 Injectable

管道 处理控制器路由处理程序中的 参数,主要用于以下两种情况。

  • 转换:将输入数据进行转换,例如将字符串转换为整数。
  • 验证:如果输入数据有效,则直接通过而无需更改;否则,抛出异常。由于管道位于异常处理区域中,异常将由异常处理层处理,不会执行控制器方法。
使用内置管道功能

Nest 自带了几种管道配件。

  • ValidationPipe (验证管道 yǎnzhèng guǎndào)
  • ParseIntPipe (整数解析管道 zhěngshù jiěxī guǎndào)
  • ParseFloatPipe (浮点数解析管道 fúdiǎnshù jiěxī guǎndào)
  • ParseBoolPipe (布尔值解析管道 bù’ěr zhí jiěxī guǎndào)
  • ParseArrayPipe (数组解析管道 shùzhù jiěxī guǎndào)
  • ParseUUIDPipe (UUID解析管道 UUID jiěxī guǎndào)
  • ParseEnumPipe (枚举值解析管道 méijǔ zhí jiěxī guǎndào)
  • DefaultValuePipe (默认值解析管道 mòrúnzhí jiěxī guǎndào)
  • ParseFilePipe (文件解析管道 wénjiàn jiěxī guǎndào)
  • ParseDatePipe (日期解析管道 rìqī jiěxī guǎndào)

我们先来看看如何使用它们,比如以 ParseIntPipe 为例。

    @Get(":id")  
    async getByIndex(@Param('id', ParseIntPipe) id: number): Promise<string> {  
        return `查询索引编号为${id}的宝可梦!`  
    }

咱们试试看。

    curl http://127.0.0.1:3000/pokemons/1  
    获取宝可梦:编号1号!  

    curl http://127.0.0.1:3000/pokemons/1.5  
    {  
     "timestamp": "2025-02-18T09:03:53.877Z",  
     "path": "/pokemons/1.5",  
     "message": "这被筛选了!"  
    }

哎哟!

我们刚刚确认,上面提到的异常捕获器确实捕获了管道抛出的异常!

咱们先去掉我们自定义的异常过滤器,再试试。

    curl http://127.0.0.1:3000/pokemons/1.5  

    {  
     "message": "验证失败,应该是数字格式",  
     "error": "无效请求",  
     "statusCode": 400  
    }
自定义内置管道组件的消息和状态码

如你所见,上面的信息其实并没有提供什么有用的讯息……我们确实传入了一个数字串!问题是,它并不是一个整数!

要自定义行为,我们可以这样传入 ParseIntPipe 的一个实例,而不是直接使用类。

    @Get(":id")  
    async getByIndex(@Param('id', new ParseIntPipe({exceptionFactory: (e) => {  
        throw new HttpException({  
            status: HttpStatus.FORBIDDEN,  
            error: '错误:无效的索引号',  
          }, HttpStatus.FORBIDDEN, {  
            cause: e  
          });  
          }})) id: number): Promise<string> {  
        return `获取宝可梦:索引号${id}`  
    }

这次访问这个接口,结果如下。

{  
 "status": 403,  
 "error": "你知道有索引不是整数的宝可emon吗?"  
}

另外,如果你只需要改错误代码,可以直接用 errorHttpStatusCode 参数。

@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) // 可以解析并处理 'id' 参数,将其转换为整数类型,若不符合要求则返回 NOT_ACCEPTABLE 状态码
自定义管道组件

现在我们已经看了内置的管道后,接下来就让我们来看看如何创建我们自己的管道吧!

    nest g pi pipe/pikachu-validation

让我们用一个简单的例子来说明,我们只会接受 25 作为 id 路径参数,以此来了解 transform 函数的基本用法。

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

    @Injectable()  
    export class PikachuValidationPipe implements PipeTransform {  
      transform(value: any, metadata: ArgumentMetadata) {  
        if (metadata.type == "param" && metadata.data == "id") {  
          if (value != 25) {  
            throw new HttpException({  
              status: HttpStatus.FORBIDDEN,  
              error: '获取非皮卡丘的宝可梦是不允许的。',  
            }, HttpStatus.FORBIDDEN, {  
              cause: "非25的索引"  
            });  
          }  
        }  
        return value;  
      }  
    }

首先,我们有 value,这是在传给路由处理方法前正在处理的参数。

然后我们有以下的 metadata,它包含以下属性。

export interface ArgumentMetadata {  
  type: 'body' | 'query' | 'param' | '自定义参数';  
  metatype?: Type<unknown>;  
  data?: string;  
}
  • type:表示参数是实体参数 @Body()、查询 @Query()、路径参数 @Param() 还是自定义参数。
  • metatype:参数的元类型。如果我们没在路由处理方法签名中声明类型,则为 undefined
  • data:传递给装饰器的字符串参数。比如 @Param('id'),那么我们的 data 就是 id。如果没在括号里填内容,则为 undefined

我们就可以像使用内置管道那样来使用它。

    @Get(":id")  
    async getByIndex(@Param('id', PikachuValidationPipe) id: number): Promise<string> {  
        return `获取索引编号为${id}的宝可梦`  
    }

我们也可以用 @UsePipes() 装饰器,将其应用到处理程序或控制器。

    // 控制器  
    @Controller('pokemons') // 基础路由  
    @UsePipes(PikachuValidationPipe)  
    export class PokemonsController {} // 导出的Pokemons控制器类  

    // 方法  
    @Get(":id") // 获取指定ID的记录  
    @UsePipes(PikachuValidationPipe)  
    async getByIndex(...){} // 异步获取指定索引的记录

我们还可以设置一个全局管道,使其适用于应用中的所有路由处理程序。

// 在主文件中,我们创建了一个Nest应用实例,并设置了全局验证管道,用于验证数据输入的正确性。
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(PikachuValidationPipe);
守卫

正如你可能从名字中猜到的,这个组件负责处理授权和认证。

它在所有 middleware (中间件)之后执行,但在执行任何 interceptor (拦截器)或 pipe (管道)之前。

创建守护

我们来创建一个简单的授权,将检查授权头信息来判断是否应该处理或拒绝这个请求。

巢 gg 护卫或宝可梦爱好者

在我们的 PokemonLoverGuard 里,我们将实现 canActivate

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

    @Injectable()  
    export class PokemonLoverGuard implements CanActivate {  
      canActivate(  
        context: ExecutionContext,  
      ): boolean | Promise<boolean> | Observable<boolean> {  
        const ctx = context.switchToHttp();  
        const request = ctx.getRequest<Request>();  
        if (request.headers.authorization !== "pokemon-lover") {  
          return false;  
        }  
        return true;  
      }  
    }

(显然,在实际环境中,请求头中的授权信息很可能不是一个简单的字符串,我们不能直接进行相等性比较,不过我们现在先忽略这一点……)

context 参数,类型为 [ExecutionContext](https://docs.nestjs.com/fundamentals/execution-context),继承自强大的 ArgumentsHost,提供了当前执行的上下文,我们可以利用它来获取关于当前 请求流程 的详细信息。

返回值表示当前请求是否允许继续处理,true 表示允许继续,false 表示拒绝继续

应用保护

就像上面提到的所有 injectable 一样,我们可以在方法、控制器或全局范围内应用它。

关于方法

    @Get()
    @UseGuards(PokemonLoverGuard) // 使用PokemonLoverGuard守护, 该守护程序确保只有热爱宝可梦的用户能够访问此功能
    async listAll(): Promise<string[]> {}

控制员

@Controller('神奇宝贝') // base route  
@UseGuards(神奇宝贝爱好者守卫) // Guard  
export class 神奇宝贝控制器 {}

全球到应用

const app = await NestFactory.create(AppModule);  
app.useGlobalGuards(宠物爱好者守护者); // 使用全局守护者,确保只有宠物爱好者可以访问
拦截器(拦截器)

我们快完成了!本文的最后一部分内容。

一个拦截器也是一个使用了 @Injectable() 的装饰器,并实现了 NestInterceptor 接口。

我们用它来怎么样?

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

拦截器既支持同步也支持异步的 intercept() 方法。如果需要,只需将方法改为 async 即可。

新建

我们先用一个简单的日志记录器记录访问日志试试。

    nest g itc interceptors/访问记录

在我们的AccessHistoryInterceptor中,我们将实现拦截函数,该函数用于封装请求和响应流。

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

    @Injectable()  
    export class AccessHistoryInterceptor implements NestInterceptor {  
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {  
        const ctx = context.switchToHttp();  
        const request = ctx.getRequest<Request>();  
        console.log('收到请求:', request);  
        return next.handle();  
      }  
    }

类似于守卫中的 canActivate 方法,我们同样拥有 ExecutionContext 对象,提供了获取即将调用的路由处理器和类的方法。

然后我们有名为 nextCallHandler,它提供了对表示路由处理程序响应流的 Observable 对象的访问。通过调用 handle() 方法,我们在拦截器中通过调用路由处理方法来调用 handle()。这样,如果从未调用 handle,处理程序将永远不会被执行,请求就会一直挂在那里得不到响应!

使用拦截器

我不知道我重复了多少次了,但是再说一遍,拦截器也可以是方法处理程序作用域、控制器作用域或全局作用域。

应用到方法

    @Get()  
    @UseInterceptors(AccessHistoryInterceptor)  
    async listAll(): Promise<string[]> {}

获取所有记录的异步方法,使用AccessHistoryInterceptor拦截器,并返回字符串数组的Promise。

申请控制岗位

     @Controller('精灵们') // 基本路由  
    @UseInterceptors(访问历史记录拦截器)  
    export class PokemonsController {}

全球适用

const app = await NestFactory.create(AppModule);
// 使用全局拦截器来记录访问历史
app.useGlobalInterceptors(AccessHistoryInterceptor);
其他用途:

我们也可以使用它来进行响应映射,例如,将简单的对象(如字符串、数组等)转换为 JSON,或将 null 值转换为空串,异常映射流覆盖 等等。

谢谢大家的阅读!

就这样,这篇文章就到这里吧!

它变得相当长了!但希望你觉得它(至少在某种程度上)有帮助,也值得你花时间!

祝你生活美满!

0人推荐
随时随地看视频
慕课网APP