Skip to main content

NestJS综述目录

  1. NestJS第一步
  2. 控制器
  3. 提供者
  4. 模块
  5. 中间件
  6. 异常过滤器
  7. 管道
  8. 守卫
  9. 异常过滤器
  10. 自定义路由装饰器

第一步

在这组文章中,您将了解Nest的核心基础知识。 为了熟悉Nest应用程序的基本构建块,我们将构建一个基本的CRUD应用程序,其功能涵盖介绍性的大量基础知识。

语言

我们热爱TypeScript,但最重要的是,我们热爱Node.js。这就是Nest同时兼容TypeScript和纯JavaScript的原因。 Nest利用了最新的语法特性,因此要将其用于纯JavaScript,我们需要一个Babel编译器。

我们将在提供的示例中主要使用TypeScript,但您始终可以将代码片段切换为普通JavaScript语法(只需单击每个片段右上角的语言按钮即可切换)

先决条件

请确保您的操作系统上安装了Node.js(版本>=16)

设置

使用 Nest CLI设置新项目非常简单。安装npm后,您可以再操作系统终端中使用以下命令创建一个新的Nest项目:

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

如果要使用TypeScript更严格的功能集创建新项目,请使用--strict标志传递给nest new命令。

将创建project-name目录,安装节点模块和一些其它样板文件,并将创建src/目录并填充几个核心文件。

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

要创建Nest应用程序实例,我们需要使用核心NestFactory类。 NestFactory类提供了几个静态方法,用于创建应用程序实例。 create()方法会返回一个应用程序对象,该对象符合INestApplication接口。 该对象提供了一系列方法,这些方法将在接下来的章节中介绍。在上面的main.ts示例中, 我们只需启动HTTP监听器,让应用程序等待入站HTTP请求即可。

请注意,使用Nest CLI搭建的项目会创建一个初始化项目结构,鼓励开发人员遵循每个模块 保留在其自己的专用目录中的约定。

tip

默认情况下,如果在创建应用程序时发生任何错误, 您的应用程序将退出并显示代码1。 如果您想让它抛出错误,请禁用abortOnError选项,例如 NestFactory.create(AppModule, {abortOnError: false})

平台

Nest的目标是成为一个与平台无关的框架。 平台独立性使得创建可重复使用的逻辑部件成为可能,开发人员可以再多种不同类型 的应用程序中利用这些逻辑部件。 从技术上讲,一旦创建了适配器,Nest就能与任何Node HTTP框架协同工作。 开箱即支持两种HTTP平台:express和fastify。您可以选择最适合您需求的一种。

  • express平台

    Express是一个著名的极简网络节点框架。 它是一个经过实战检验、可用于生产的库,拥有大量由社区提供的资源。 默认使用@nestjs/platform-express包。 许多用户都能很好地使用Express,无需采取任何措施启用它。

  • fastify平台

    Fastify是一个高性能、低开销的框架,高度专注于提供最大的效率和速度。 在这里阅读如何使用它

无论使用哪个平台,它都会公开自己的应用程序结构,它们分别被视为NestExpressApplicationNestFastifyApplication

如下例所示,当你向NestFactory.create()方法传递一个类型时, 应用程序对象将拥有该特定平台专用的方法。但请注意,除非您真的想访问底层平台API,否则无需指定类型。

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

运行应用程序

安装过程完成后,您可以再操作系统命令提示符下运行以下命令来启动应用程序侦听入站HTTP请求:

npm run start
tip

为了加快开发过程(构建速度加快20倍),您可以通过将-b swc标志传递给启动脚本来使用SWC构建器, 如下所示npm run start -- -b swc

此命令将启动应用程序,HTTP服务器将监听src/main.ts文件中定义的端口。 应用程序运行后,打开浏览器并导航至http://localhost:3000。您将看到Hello World!

要查看文件中的更改,可以运行以下命令启动应用程序:

npm run start:dev

该命令将监听您的文件,自动重新编译并重新加载服务器。

语法检查和格式化

CLI尽最大努力构建可靠大规模开发工作流程。因此,生成的Nest项目预装了代码linterformatter程序(分别为eslintprettier)

info

不确定格式化程序与代码检查的作用?在这里了解差异

为了确保最大的稳定性和可扩展性,我们使用基本的eslintprettier包。 此设置允许IDE在设计上与官方扩展完美集成。

# 使用eslint进行lint和自动修复
npm run lint
# 使用prettier设置的格式进行代码格式化
npm run format

对于IDE不相关的无头环境(持续集成、Git挂钩等),Nest项目附带了现成的npm脚本

控制器

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

控制器的作用是接收应用程序的特定请求。 路由机制控制哪个控制器接收哪些请求。 通常情况下,每个控制器都有不止一个路由,不同的路由可以指定不同的操作。

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

tip

为了快速创建带有内置验证的CRUD控制器,您可以使用CLI的CRUD生成器:nest g resource [name]

路由

在下面的示例中,我们将使用@Controller()装饰器,这是定义基本控制器所必需的。 我们将指定一个可选的路径前缀:cats。在@Controller()装饰器中使用路径前缀 可以让我们轻松地将一组相关的路由分组,并最大限度地减少重复代码。

例如,我们可以选择将一组管理与猫实体交互的路由归类到路由/cats下。 在这种情况下,我们可以再@Controller()装饰器中指定路径前缀cats, 这样就不必为文件中的每个路由重复写路径前缀的这一部分。

import { Controller, Get } from '@nestjs/commom';

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

要使用CLI创建控制器,只需执行nest g controller [name]命令

findAll()方法之前的@Get() HTTP请求方法装饰器告诉Nest为HTTP请求的特定端点创建处理程序。 端点对应于HTTP请求方法(在本例中为GET)和路由路径。路由路径是什么? 处理程序的路由路径是通过连接为控制器声明的(可选)前缀和方法装饰器中指定的任何路径来确定的。 由于我们已经为每个路由(cats)声明了一个前缀,并且没有在装饰器中添加任何路径信息, 因此Nest会将GET/cats请求映射到此处理程序。 如前所述,路径包括可选的控制器路径前缀和请求方法装饰器中声明的任何路径字符串。 例如,cats的路径前缀与装饰器@Get('breed')结合将为GET /cats/breed等请求生成路由映射。

在上面的示例中,当前此端点发出GET请求时,Nest将请求路由到我们用户定义的findAll()方法。 请注意,我们在这里选择的方法名称是完全任意的。显然,我们必须声明一个方法来绑定路由, 但Nest并不赋予所选方法名称任何意义。

此方法将返回200状态码和关联的响应,在本例中只是一个字符串。为什么会发生这种情况? 为了解释这一点,我们首先介绍Nest使用两种不同选项来操纵响应的概念:

  • 标准(推荐)

    使用此内置方法,当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。 然而,当它返回 JavaScript 基本类型(例如字符串、数字、布尔值)时,Nest 将仅发送该值,而不尝试对其进行序列化。 这使得响应处理变得简单:只需返回值,Nest 就会处理其余的事情。

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

  • 特定库

    我们可以使用特定于库的(例如 Express)响应对象,可以使用方法处理程序签名中的@Res()装饰器注入该对象(例如 findAll(@Res() response))。 通过这种方法,您可以使用该对象公开的本机响应处理方法。例如,使用Express,您可以使用像response.status(200).send()这样的代码构建响应。

warning

Nest 检测处理程序何时使用@Res()@Next(),表明您已选择特定库的选项。 如果同时使用两种方法,则该单一路线的标准方法将自动禁用,并且将不再按预期工作。 要同时使用这两种方法(例如,通过注入响应对象以仅设置 cookie/headers,但仍将其余部分留给框架), 您必须在@Res({ passthrough: true })装饰器设置passthroughtrue

请求对象

处理程序通常需要访问客户端请求的详细信息。Nest 提供对底层平台(默认为 Express)请求对象的访问。 我们可以在处理程序的签名中添加 @Req() 装饰器,指示 Nest 注入请求对象,从而访问请求对象

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

为了利用express的类型(如上面的request: Request请求参数示例),请安装@types/express

request对象代表HTTP请求,具有请求查询字符串、参数、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

*为了与底层HTTP平台(例如Express和Fastify)之间的类型兼容,Nest提供了@Res@Response()装饰器。 @Res()只是@Response()的别名。两者都直接公开底层平台响应对象接口。 使用它们时,您还应该导入底层库的类型(例如@types/express)以充分利用他们。 请注意,当您在方法处理程序中注入@Res()@Response()时,您会将Nest置于该处理程序的特定库的模式, 并且您将负责管理响应。 执行此操作时,您必须通过调用响应对象来发出某种类型的响应,否则HTTP服务器将挂起。 例如(res.json(...)res.send(...))

资源

之前,我们定义了一个端点来获取cats资源(GET路由)。我们还希望提供一个创建新纪录的端点。 为此,我们创建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() 其中All()定义了一个处理所有这些端点。

路由通配符

Nest还支持基于模式的路由。例如星号(asterisk)用作通配符,将匹配任意字符组合。

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

ab*cd路由路径将匹配abcd,ab_cd,abecd。 字符?+*()可以在路径中使用,它们是正则表达式的子集。 连字符(-)和点字符(.)在基于字符串的路径中按字面解释

warning

仅express支持路由中间的通配符

状态码

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

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

@nestjs/common包导入HttpCode

通常,您的状态码不是静态的,而是取决于各种因素。在这种情况下,您可以使用特定库的响应(使用@Res()注入) 对象(或者,如果发生错误,则抛出异常)。

信息头

要指定自定义响应信息头,您可以使用

  • @Header装饰器
  • 或特定库的响应对象(并直接调用res.header())
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat'
}
tip

@nestjs/common包中导入Header

重定向

要讲响应重定向到特定URL,您可以使用

  • Redirect()装饰器
  • 或特定库的响应对象(并直接调用res.redirect())

Redirect()有两个参数:

  • url
  • statusCode 两个都是可选的。 如果省略,statusCode的默认值为302(已找到)
@Get()
@Redirect('http://nestjs.com', 301)
tip

有时您可能想要动态确定HTTP状态码或重定向URL。 通过返回HttpRedirectResponse接口的对象来完成此操作。

返回值会覆盖传递给@Redirect()装饰器的任何参数。例如

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

路由参数

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

tip

带参数的路由应在任何静态路径之后声明。 这可以防止参数化路径拦截发往静态路径的流量。

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

@Param()用于装饰方法参数(上例中的params),并使用路由参数可用作方法体内该装饰方法参数的属性。 如上面的代码所示,我们可以通过引用params.id来访问id参数。 还可以向装饰器传入特定的参数token,然后在方法体中直接通过名称引用路由参数。

tip

@nestjs/common导入Param

@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';
}
}
warning

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

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

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

作用域

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

然而,在某些边缘情况下,控制器的基于请求的生命周期可能是所需的行为,例如GraphQL应用程序中的每个 请求缓存、请求跟踪或多租户。在此处了解如何控制范围。

异步性

我们喜欢现代JavaScript,并且知道数据提取大多是异步的。 这就是Nest支持异步函数并与异步(async)函数配合良好的原因。

tip

此处了解有关async/await功能。

每个异步函数都必须返回一个Promise。 这意味着您可以返回Nest能够自行解析的延迟值。让我们看一个例子:

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

上述代码完全有效。此外,Nest路由处理程序还能返回RxJS可观察流,因此功能更加强大。 Nest会自动订阅下方的源,并获取最后发射的值(一旦流完成)。

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

上述两种方法都有效,您可以使用适合您要求的任何方法。

请求负载

我们之前的POST路由处理程序示例不接受任何客户端参数。 让我们通过在此处添加@Body()装饰器来解决此问题。

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

让我们创建CreateCatDto类:

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

它只有三个基本属性。此后我们可以再CatsController中使用新创建的DTO:

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

我们的ValidationPipe可以过滤掉方法处理程序不应接收的属性。 在这种情况下,我们可以将可接受的属性列入白名单,并且任何未包含在白名单中的属性都会自动从结果对象中删除。 在CreateCatDto示例中,我们的白名单是name,age,breed属性。

处理错误

将在处理错误(即处理异常)章节介绍

完整资源样本

下面是一个使用几个可用装饰器来创建基本控制器的示例。 该控制器公开了几种访问和操作内部数据的方法。

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

Nest CLI提供了一个生成器(原理图),可以自动生成所有的样板代码,帮助我们避免执行所有这些操作, 并使开发人员体验更加简单。

启动并运行

完全定义上述控制器后,Nest仍然不知道CatsController存在,因此不会创建此类的实例。

控制器始终属于一个模块,这就是为什么我们在@Module装饰器中包含控制器数组的原因。 由于除了根AppModule之外我们还没有定义任何其他模块, 因此我们将使用它来引入CatsController:

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

@Module({
controllers: [CatsController],
})

export class AppModule {}

我们使用@Module()装饰器将元素附加到模块类,Nest现在可以轻松反映必须安装哪些控制器。

库特定方法

到目前为止,我们已经讨论了Nest操作响应的标准方法。 操作响应的第二种方法是使用库特定的响应对象(response object)。 为了注入特定的响应对象,我们需要使用@Res()装饰器。 为了显示差异,我们将CatsController重写为以下内容:

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()装饰器。 要解决此问题,您可以将passthrough选项设置为true,如下所示:

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

现在您可以与本机响应对象交互(例如,根据某些条件设置cookieheaders), 但将其余的留给框架。

提供者

提供者是Nest中的一个基本概念。许多基本的Nest类可以被视为提供者:

  • 服务(services)
  • 存储库(repositories)
  • 工厂(factories)
  • 助手(helpers)

许多基本的Nest类可以被视为提供者-服务、存储库、工厂、助手等等。 提供者的主要思想是它可以作为依赖项注入。这意味着对象之间可以创建各种关系, 并且"连接"这些对象的功能很大程度上可以委托给Nest运行时系统。

在上一章节中,我们构建了一个简单的CatsController控制器应该处理HTTP请求并将更复杂的任务委托给提供者。 提供者在模块中声明为providers的纯JavaScript类。

tip

由于Nest能够以更加面向对象的方式设计和组织依赖关系,因此我们强烈建议遵循SOLID原则

服务

让我们创建一个简单的CatsService开始。 这个服务将负责数据存储和检索,并且设计为由CatsController使用, 因此它是定义为提供者的良好候选者。

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

要使用CLI创建服务,只虚执行nest g service cats命令

我们的CatsService事一个基本类,具有一个属性和两个方法。 唯一的新功能是它使用@Injectable装饰器。 @Injectable()装饰器附加元数据,它声明CatsService是一个可以由Nest IoC容器管理的类。 顺便说一句,这个例子也使用了Cat接口,它可能看起来像这样。

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

现在我们有了一个用于检索猫的服务类,让我们在CatsController中使用它:

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是围绕通常称为依赖注入的强大设计模式构建的。 我们建议阅读Angular 官方文档中有关此概念的精彩文章。

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

constructor(private catsService: CatsService) {}

作用域

提供者通常具有与应用程序生命周期同步的生命周期("scope")。 当应用程序启动时,必须解决每个依赖项,因此必须实例化每个提供者程序。 同样,当应用程序关闭时,每个提供者程序都将被销毁。 但是,也有一些方法可以使您的提供者程序生命周期限定在请求范围内。

自定义提供者

Nest有一个内置的控制反转(IoC)容器,可以解析提供者之间的关系此功能是上述依赖注入功能的基础,但实际上比我们迄今为止所描述的功能要强大得多。 有多种方法可以定义提供者程序:您可以使用普通值、类以及异步或同步工厂。

可选提供者

有时,您可能存在不一定需要解决的依赖关系。 例如,您的类可能依赖于配置对象,但如果没有传递任何内容,则应使用默认值。 在这种情况下,依赖关系变得可选,因为缺少配置提供者程序不会导致错误。

要指明提供者程序时可选的,请在构造函数的签名中使用@Optional()装饰器。

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

@Injectable()
export class HtpService<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;
}
warning

如果您的类没有扩展另一个类,那么您应该始终更喜欢使用基于构造函数的注入。

提供者注册

现在我们已经定义了一个提供者(CatsService),并且有了该服务的使用者(CatsController), 我们需要向Nest注册该服务,以便它可以执行注入。 我们通过编辑模块文件(app.module.ts)并将服务添加到@Module()装饰器的提供者数组中来实现此目的。

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

Nest现在能够解析CatsController类的依赖关系。 这就是我们的目录结构现在的样子:

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

手动实例化

到目前为止,我们已经讨论了Nest如何自动处理解决依赖关系的大部分细节。 在某些情况下,您可能需要跳出内置依赖注入系统并手动检索或实例化提供者程序。

要获取现有实例或动态实例化提供者程序,您可以使用模块引用。

bootstrap()函数中获取提供者程序(例如,对于没有控制器的独立应用程序, 或在引导期间使用配置服务)。

模块

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

每个应用程序至少有一个模块,即根模块。 根模块是Nest用于构建应用程序图的起点-Nest用于解析模块和提供者关系及依赖关系的内部数据结构。 虽然理论上非常小的应用程序可能只有根模块,但这不是典型情况。 我们要强调的是,强烈建议将模块作为组织组件的有效方式。 因此,对于大多数应用程序来说,最终的架构将采用多个模块, 每个模块封装一组密切相关的功能。

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

providers将由Nest注入器实例化的提供者程序,并且至少可以再该模块中共享
controllers该模块中定义的必须实例化的控制器集
imports导入模块的列表,导出该模块所需的提供者程序
exports此模块提供的提供程序子集,并且应该在导入此模块的其他模块中可用

该模块默认封装了提供者。 这意味着不可能注入即不直接属于当前模块不从导入模块导出的提供者程序。 因此,您可以将从模块导出的提供者程序视为模块的公共接口或API。

功能模块

CatsControllerCatsService属于同一应用程序域。由于它们密切相关,因此将它们移至 功能模块中是有意义的。 功能模块只是组织与特定功能相关的代码,保持代码组织并建立清晰的边界。 这有助于我们管理复杂性并按照可靠的原则进行开发,特别是随着应用程序或团队规模的增长。

为了演示这一点,我们将创建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 {}
tip

要使用CLI创建模块,只需执行nest g module cats

上面,我们在cats.module.ts文件中定义了CatsModule,并将与该模块相关的所有内容移至cats目录中。 我们需要做的最后一件事是将此模块导入到根模块(AppModule,在app.module.ts文件中定义)。

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

共享模块

在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,并将于导入它的所有其他模块共享同一个实例

模块再次导出

如上所示,模块可以导出其内部提供者程序。此外,他们还可以重新导出导入的模块。 在下面的示例中,CommonModule被导入到CoreModule中导出,使其可用于导入此模块的其他模块。

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

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

全局模块

如果您必须在各处导入相同的模块集,这可能会变得乏味。 与Nest不同,Angular providers是在全局范围内注册的。 一旦定义,它们就随处可用。然而,Nest将提供者程序封装在模块范围内。 如果不先导入封装模块,您就无法再其他地方使用模块的提供者程序。

当您想要提供一组开箱即用的提供者程序(例如帮助程序、数据库连接等)时, 请使用@Global()装饰器使模块全局化。

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

@Global()
@Module({
controllers: [CatsController],
providers: [CatsServicer],
exports: [CatsServicer],
})

export class CatsModule {}

@Global()装饰器使模块具有全局范围。全局模块应该只注册一次,通常由根模块或核心模块注册。 在上面的示例中,CatsService提供者程序将无处不在,并且希望注入服务的模块不需要再其导入数组 中导入CatsModule

tip

让一切全局化并不是一个好的设计决策。全局模块可用于减少必要的样板数量。 imports数组通常是使模块的API可供消费者使用的首选方式。

动态模块

Nest模块系统包含一个称为动态模块的强大功能。 此功能使您能够轻松创建可动态注册和配置提供者程序的可定制模块。 以下是DatabaseModule的动态模块定义的示例:

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

forRoot()方法可以同步或异步(即通过Promise)返回动态模块。

该模块默认定义了连接提供者程序(在@Module()类装饰器元数据中),但还会根据传入forRoot()方法的实体 和可选对象,公开一系列提供者程序,例如存储库。请注意,动态模块返回的属性扩展(而不是覆盖)@Module() 装饰器中定义的基本模块元数据。这就是从模块导出静态声明的Connection提供者程序和动态生成的存储库提供者程序的方式。

如果要在全局范围内注册动态模块,请将global属性设置为true

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

如上所述,使素有内容全局化并不是一个好的设计决策。

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()中间件函数通常用名为next的变量来表示。

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

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

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

您可以在函数或带有@Injectable()装饰器的类中实现自定义Nest中间件。该类应该实现NestMiddleware接口, 而函数没有任何特殊要求。让我们首先使用类方法实现一个简单的中间件功能。

warning

Express和fastify以不同的方式处理中间件并提供不同的方法签名。

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接口。让我们在AppModule级别设置LoggerMiddleware

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

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

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

可以使用async/await使configure()方法异步(例如,您可以等待configure()方法体内异步操作的完成)

warning

当使用express适配器时,NestJS应用程序将默认从包body-parser注册jsonurlencoded。 这意味着如果您想通过MiddlewareConsumer自定义该中间件,则需要在使用NestFactory.create() 创建应用程序时将bodyParser标志设置为false来关闭全局中间件。

路由通配符

还支持基于模式的路由。例如,星号(asterisk)用作通配符,将匹配任意字符组合:

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

ab*cd路由路径将匹配abcdab_cdabecd等。字符?,+,*,()可以再路由路径中使用, 并且是其正则表达式对应项的子集。连字符(-)和点(.)按字面意思解释为基于字符串的路径。

warning

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

中间件消费者

MiddlewareConsumer是一个辅助类。它提供了几种内置方法来管理中间件。所有这些都可以简单地以流畅的方式链接起来。 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);
}
}
tip

apply()方法可以采用单个中间件,也可以采用多个参数来指定多个中间件。

排除路由

有时,我们希望将某些路由排除在中间件应用之外。我们可以使用exclude()方法轻松排除某些路由。 该方法可以使用单个字符串、多个字符串或RouteInfo对象来标识要排除的路由,如下所示:

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

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

在上面的示例中,LoggerMiddleware将绑定到CatsController中定义的所有路由,除了传递给exclude()方法 排除了三个路由路径通过。

函数式中间件

我们一直使用的LoggerMiddleware类非常简单。它没有成员,没有附加方法,也没有依赖项。 为什么我们不能再一个简单的函数而不是一个类中定义它?事实上,我们可以。 这种类型的中间件称为函数式中间件。让我们将日志中间件从基于类的中间件转换为函数式中间件来说明区别:

logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
}

并在AppModule中使用:

consumer
.apply(logger)
.forRoutes(CatsController);
tip

当您的中间件不需要任何依赖项时,请考虑使用更简单的函数式中间件替代方案。

多中间件

如上所述,为了绑定多个顺序执行的中间件,只需在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);
tip

无法访问全局中间件中的DI容器。使用app.use()时,您可以使用函数式中间件。 或者,您可以使用类中间件并在AppModule(或任何其他模块)中使用.forRoutes('*')来使用它。

异常过滤器

Nest带有一个内置的异常层,负责处理应用程序中所有未处理的异常。 当应用程序代码未处理异常时,该层会捕获异常,然后自动发送适当的用户友好响应。

开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理HttpException类型(及其子类)的异常。 当异常无法识别时,(既不是HttpException也不是继承自HttpException的类),内置异常过滤器会生成 以下默认JSON响应:

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

全局异常过滤器部分支持http-errors库。基本上,任何包含statusCodemessage属性的抛出异常都将 被正确填充并作为响应发回(而不是针对无法识别的异常的默认InternalServerErrorException)。

抛出标准异常

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

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

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

我们在这里使用了HttpStatus。这是从@nestjs/common包导入的帮助器枚举

当客户端调用此端点时,响应如下所示:

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

HttpException构造函数采用两个必需参数来确定响应:

  • 响应参数定义JSON响应正文。它可以是stringobject,如下所述。
  • status参数定义HTTP状态代码

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

  • statusCode: 默认为状态参数中提供的HTTP状态码
  • message: 基于状态的HTTP错误的简短描述

要仅覆盖JSON响应正文的消息部分,请在response参数中提供一个字符串。 要覆盖整个JSON响应正文,请在响应参数中传递一个对象。 Nest将序列化该对象并将其作为JSON响应正文返回。

第二个构造函数参数status应该是有效的HTTP状态码。 最佳实践是使用从@nestjs/common导入的HttpStatus枚举。

第三个构造函数参数(可选)options可用于提供错误原因。 这个cause对象不会序列化到响应对象中,但它对日志记录很有用,可以提供有关导致HttpException被 抛出的内部错误的有价值信息。

这是一个覆盖整个响应正文并提供错误原因的示例:

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

自定义异常

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

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

由于ForbiddenException扩展了基类HttpException,因此它将与内置异常处理程序无缝协作, 因此我们可以再findAll()方法中使用它。

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参数提供错误cause和错误描述。

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

使用上面的内容,响应将如下所示:

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

异常过滤器

虽然基本(内置)异常过滤器可以自动为您处理许多情况,但您可能希望完全控制异常层。 例如,您可能想要添加日志记录或根据某些动态因素使用不同的JSON架构。 异常过滤器正是为此目的而设计的。它们使您可以控制确切的控制流程以及发送回客户端的响应内容。

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

http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } '@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,
})
}
}
tip

所有异常过滤器都应实现通用ExceptionFilter<T>接口。 这要求您提供catch(exception: T, host: ArgumentsHost)方法及其指示的签名。 T表示异常的类型

warning

如果您使用@nestjs/platform-fastity,您可以使用response.send()而不是response.json(). 不要忘记从fastify导入正确的类型。

@Catch(HttpException)装饰器将所需的元数据绑定到异常过滤器,告诉Nest这个特定的过滤器 正在寻找HttpException类型的异常,而不是其他任何东西。 @Catch()装饰器可以采用单个参数或逗号分隔的列表。这使得您可以同时为多种类型的异常设置过滤器。

参数主机

我们来看看catch()方法的参数。exception参数时当前正在处理的异常对象。 host参数是一个ArgumentsHost对象。ArgumentsHost是一个功能强大的实用程序对象, 我们将在执行上下文章节中进一步研究它。在此代码示例中,我们使用它来获取对传递给原始请求处理程序 (在引发异常的控制器中)的RequestResponse对象的引用。 在此代码示例中,我们在ArgumentsHost上使用了一些辅助方法来获取所需的RequestResponse对象。 *这种抽象级别的原因是ArgumentsHost在所有上下文中都起作用(例如,我们现在正在使用的HTTP服务器 上下文,还有微服务和WebSocket)。在执行上下文一章中,我们将了解如何利用ArgumentsHost及其辅助函数 的功能来访问任何执行上下文的适当底层参数。这将使我们能够编写跨所有上下文运行的通用异常过滤器。

绑定过滤器

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

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

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

我们在这里使用@UseFilter()装饰器。与@Catch()装饰器类似,它可以采用单个过滤器实例, 或以逗号分隔的过滤器实例列表。在这里,我们就地创建了HttpExceptionFilter的实例。 或者,您可以传递类(而不是实例),将实例化的责任留给框架,并启动依赖项注入

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

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

在上面的示例中,HttpExceptionFilter仅应用于单个create()路由处理程序,使其具有方法范围。 异常过滤器的范围可以再不同级别:控制器(controller)/解析器(resolver)/网关的方法范围、控制器范围或全局范围。 例如,要将过滤器设置为控制器范围,您可以执行以下操作:

cats.controller.ts
@UseFilter(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();
warning

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

全局范围的过滤器用于整个应用程序、每个控制器和每个路由处理程序。 就依赖注入而言,从任何模块外部注册的全局过滤器(使用useGlobalFilters()如上例所示) 无法注入依赖项,因为这是在任何模块的上下文之外完成的。 为了解决此问题,您可以使用以下结构直接从任何模块注册全局范围的过滤器:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

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

在使用这种方法为过滤器执行依赖注入时,请注意,无论在哪个模块中使用此构造方式,过滤器实际上是全局的。 这应该在哪里完成?选择定义过滤器(在上面的例子中为HttpExceptionFilter)的模块。 另外,useClass并不是处理自定义提供者注册的唯一方式。

您可以根据需要使用此技术添加任意数量的过滤器;只需将每个添加到提供者数组中。

捕获一切

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

在下面的示例中,我们有一个与平台无关的代码,因为它使用HTTP适配器来传递响应,并且不直接使用任何 特定于平台的对象(ResponseRequest)。

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

当将捕获所有内容的异常过滤器与绑定到特定类型的过滤器组合时, 应首先声明"捕获任何内容"过滤器,以允许特定过滤器正确处理绑定类型。

继承

通常情况下,您会创建完全自定义的异常过滤器,以满足您的应用程序需求。 不过,在某些情况下,您可能只想扩展内置的默认全局异常过滤器,并根据某些因素重写其行为。

为了将异常处理委托给基本过滤器,您需要扩展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);
}
}
warning

扩展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();

管道

管道是一个用@Injectable()装饰器注解的类,它实现了PipeTransform接口。

管道有两个典型用例:

  • transformation(转换): 将输入数据转换为所需形式(例如,从字符串到整数)
  • validation(验证): 验证数据,如果有效,则只需按将其原样传递;否则,抛出异常

在这两种情况下,管道对控制器路由处理程序正在处理的参数进行操作Nest在调用方法之前插入一个管道,该管道接收指定给该方法的参数并对它们进行操作。 任何转换或验证操作都会在此时发生,之后使用任何(可能)转换的参数调用路由处理程序。

Nest附带了许多内置管道,您可以开箱即用。您还可以构建自己的自定义管道。 在本章中,我们将介绍内置管道并展示如何将它们绑定到路由处理程序。 然后,我们将检查几个定制的管道,以展示如何从头开始构建一个管道。

tip

管道在例外区域内运行。这意味着当Pipe抛出异常时, 它将由异常层处理(全局异常过滤器和应用于当前上下文的任何异常过滤器)。 鉴于上述情况,应该清楚的是,当Pipe中引发异常时,随后不会执行任何控制器方法。 这为您提供了一种最佳实践技术,用于验证从系统边界的外部源进入应用程序的数据。

内建管道

Nest附带九个开箱即用的管道:

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

它们是从@nestjs/common包导出的。

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

绑定管道

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

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

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

例如,假设该路线被称为:

GET localhost:3000/abc

Nest将抛出一个异常:

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

当使用ParseUUIDPipe()时,您正在解析版本3,4或5中的UUID,如果您只需要特定版本的UUID, 您可以再管道选项中传递版本。

上面我们看到了绑定各种Parse*系列内置管道的示例。 绑定验证管道有点不同;我们将在下一节中讨论这一点。

自定义管道

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

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

validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

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

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(),查询Query(),参数@Param()还是自定义参数
  • metatype 提供参数的元数据,例如String。注意:如果您在路由处理程序方法签名中省略类型声明,或者使用普通JavaScript,则该值未定义。
  • data 传递给装饰器的字符串,例如@Body('string')。如果将装饰器括号留空,则它是未定义(undefined)的。
warning

TypeScript接口在转译过程中消失。因此,如果方法参数的类型声明为接口而不是类, 则metatype值将为Object

基于模式的验证

让我们让验证管道变得更有用一点。仔细看看CatsControllercreate()方法,我们可能希望在 尝试运行我们的服务方法之前确保帖子主题对象有效。

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

让我们关注createCatDtobody参数。它的类型是CreateCatDto:

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

我们希望确保对create方法的任何传入请求都包含有效的主体。 因此,我们必须验证createCatDto对象的三个成员。 我们可以在路由处理方法内部执行此操作,但这样做并不理想,因为它会违反单一职责原则(SRP)。

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

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

当然,这正是管道设计的用例。因此,让我们继续完善我们的验证管道。

对象模式验证

有几种可以以清晰、不重复的方式进行对象验证的方法。一种常见的方法是使用基于模式的验证。让我们尝试这种方法。

Zod库允许您以一种简单易读的API方式创建模式。让我们构建一个使用基于Zod的模式的验证管道。

首先,安装所需的包。

npm install --save zod

在下面的代码示例中,我们创建一个简单的类,该类以模式作为构造函数参数。 然后,我们应用schema.parse()方法,该方法根据提供的模式验证我们传入的参数。

正如前面提到的,验证管道要么返回不变的值,要么抛出异常。

在下一节中,您将看到我们如何使用@UsePipes()装饰器为给定的控制器方法提供适当的模式。 这样做使我们的验证管道在不同上下文中可重用,正如我们所设想的那样。

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

export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodObject<any>) {}

transform(value: unknown, metadata: ArgumentMetadata) {
try {
this.schema.parse(value);
} catch (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}

绑定验证管道

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

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

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

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

Zod模式示例:

import { z } from 'zod';

export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();

export type CreateCatDto = z.infer<typeof createCatSchema>;

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

cats.controller.ts
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
tip

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

warning

zod库需要再tsconfig.json文件中启用strictNullChecks配置

类验证器

warning

本节中的技术需要TypeScript,如果您的应用程序是使用普通JavaScript编写的,则这些技术不可用

Nest与class-validator库搭配使用效果很好。这个强大的库允许您使用基于装饰器的验证。 基于装饰器的验证非常强大,特别是与Nest的管道功能结合使用时,因为我们可以访问已处理属性的元类型(metatype)。 在开始之前,我们需要安装所需的包:

npm i --save class-validator class-transformer

安装完成后,我们可以在CreateCatDto类中添加一些装饰器。 这里我们看到了这种技术的一个重要优势:CreateCatDto类仍然是我们Post主体对象的唯一真实来源 (而不是必须创建一个单独的验证类)。

create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';

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

@IsInt()
age: number;

@IsString()
breed: string;
}

现在我们可以创建一个使用这些注解的ValidationPipe

validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-validator';

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

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

作为提醒,您无需自己构建通用验证管道,因为Nest已经提供了ValidationPipe。 内置的ValidationPipe提供了比本章中构建的示例更多的选项,为了说明自定义管道的机制,本章将其保持基本。 您可以在这里找到完整的详细信息,以及许多示例。

warning

我们在上面使用了由与class-validator库相同作者制作的class-transformer库, 因此它们之间协同运作得非常好。

让我们浏览一下这段代码。首先,请注意transform()方法被标记为异步。 这是因为Nest支持同步和异步管道。我们使这个方法异步,因为class-validator的一些验证可能是异步的(使用Promises)。

接下来请注意,我们使用解构来提取metatype字段(从ArgumentMetadata中提取此成员)到我们的metatype参数中。 这只是一种简写,用于获取完整的ArgumentMetadata,然后有一个额外的语句来分配metatype变量。

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

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

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

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

cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) creteCatDto: 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();
tip

在混合应用程序的情况下,useGlobalPipes() 方法不会为网关和微服务设置管道。 对于“标准”(非混合)微服务应用程序,useGlobalPipes()确实会在全局范围内挂载管道。

全局管道在整个应用程序中使用,对于每个控制器和每个路由处理程序都起作用。

请注意,在依赖注入方面,从任何模块外部注册的全局管道(如上例中的 useGlobalPipes())无法注入依赖项, 因为绑定是在任何模块的上下文之外完成的。为了解决这个问题,您可以使用以下结构直接从任何模块设置全局管道:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

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

在使用此方法执行管道的依赖注入时,请注意,无论在哪个模块中使用此结构,管道实际上都是全局的。 这应该在哪里完成?选择定义管道(上面的例子中是ValidationPipe)的模块。 另外,useClass不是处理自定义提供者注册的唯一方法。

内置的验证管道

作为提醒,您无需自己构建通用的验证管道,因为Nest内置了ValidationPipe。 内置的ValidationPipe提供了比本章中构建的示例更多的选项,本章保持基本只是为了说明自定义管道的机制。 您可以在这里找到完整的详细信息以及大量的示例。

转换用例

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

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

下面是一个简单的ParseIntPipe,负责将字符串解析为整数值。 (正如上面提到的,Nest有一个内置的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对象)。通过将样板代码从处理程序中抽象出来并放入一个通用管道, 这可以使您的代码更具声明性和DRY。

提供默认值

Parse管道期望参数的值已定义。如果收到nullundefined值,它们将抛出异常。 为了允许端点处理缺少的查询字符串参数值,我们必须在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})
}

守卫

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

守卫(Guards)具有单一职责。它们根据运行时存在的特定条件(如权限、角色、ACL等)确定是否处理给定的请求, 这通常被称为授权。授权(以及通常与之协作的身份验证)通常在传统的Express应用程序中由中间件处理。 中间件对于身份验证来说是一个很好的选择,因为诸如令牌验证和将属性附加到请求对象之类的操作与特定的路由上下文(及其元数据)并不紧密相关。

但中间件本质上是愚蠢的。它不知道在调用next()函数后将执行哪个处理程序。 另一方面,守卫具有对ExecutionContext实例的访问权,因此确切知道接下来将执行什么。 它们的设计,很像异常过滤器、管道和拦截器,是为了让您在请求/响应周期中的准确位置插入处理逻辑, 并以声明性的方式执行。这有助于保持代码DRY和声明性。

tip

守卫(guard)在所有中间件之后、任何拦截器(interceptor)或管道(pipe)之前执行。

授权守卫

正如前面提到的,授权是守卫的一个很好的用例,因为只有在调用方(通常是特定的经过身份验证的用户) 具有足够权限时,特定路由才应该可用。我们将构建的AuthGuard假设存在一个经过身份验证的用户(因此, 请求标头中附加了一个令牌)。它将提取和验证令牌,并使用提取的信息来确定是否可以继续处理请求。

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()函数。该函数应返回一个布尔值,指示当前请求是否允许。 它可以同步或异步地返回响应(通过PromiseObservable)。Nest使用返回值来控制下一步动作:

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

执行上下文

canActivate()函数接受一个参数,即ExecutionContext实例。ExecutionContext继承自ArgumentsHost, 我们在异常过滤器章节中已经看到过ArgumentsHost。在上面的示例中,我们只是使用了在ArgumentsHost上定义的 与之前相同的辅助方法,以获取对Request对象的引用。您可以回顾一下异常过滤器章节的Arguments host部分, 了解更多关于这个主题的信息。

通过扩展ArgumentsHostExecutionContext还添加了几个新的辅助方法,提供有关当前执行过程的额外详细信息。 这些详细信息对于构建更通用的守卫,可以在广泛的控制器、方法和执行上下文中工作,是有帮助的。 在这里了解有关ExecutionContext 的 更多信息。

基于角色的身份验证

让我们构建一个更实用的守卫,仅允许具有特定角色的用户访问。我们将从一个基本的守卫模板开始,并在接下来的部分中 进行扩展。暂时允许所有请求继续进行:

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

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

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

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

上面的构造将守卫附加到此控制器声明的每个处理程序。如果我们希望守卫仅应用于单个方法, 我们可以在方法级别应用@UseGuards()装饰器。

为了设置全局守卫,可以使用Nest应用程序实例的 useGlobalGuards() 方法:

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

在混合应用程序的情况下,默认情况下,useGlobalGuards()方法不会为网关和微服务设置全局守卫 (有关如何更改此行为的信息,请参见混合应用程序)。对于“标准”(非混合)微服务应用程序, useGlobalGuards()确实会在全局范围内挂载守卫。

全局守卫在整个应用程序中使用,适用于每个控制器和每个路由处理程序。 在依赖注入方面,从任何模块外部注册的全局守卫(如上面的示例中使用useGlobalGuards())无法注入依赖项, 因为这是在任何模块的上下文之外完成的。为了解决这个问题,您可以使用以下结构直接从任何模块设置守卫:

app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/common';

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

在使用这种方法进行守卫的依赖注入时,请注意无论在何处使用这种结构,守卫实际上都是全局的。这应该在哪里完成?选择定义守卫(如上例中的 RolesGuard)的模块。另外,useClass 不是处理自定义提供者注册的唯一方法。在这里了解更多信息。

为每个处理程序设置角色

我们的RolesGuard正在工作,但它还不够智能。我们尚未充分利用最重要的守卫功能 - 执行上下文。它还不知道角色, 或者每个处理程序允许哪些角色。例如,CatsController可能对不同的路由有不同的权限方案。 有些可能仅对管理员用户可用,而其他可能对所有人都开放。我们如何以一种灵活且可重复使用的方式将角色匹配到路由?

这就是自定义元数据发挥作用的地方 (在这里了解更多)。 Nest提供了通过Reflector#createDecorator静态方法或内置的@SetMetadata()装饰器, 将自定义元数据附加到路由处理程序的能力。

例如,让我们使用Reflector#createDecorator方法创建一个@Roles()装饰器,将元数据附加到处理程序。 Reflector是由框架内置提供的,并从@nestjs/core软件包中公开。

roles.decorator.ts
import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

这里的Roles装饰器是一个接受string[]类型的单个参数的函数。 现在,要使用这个装饰器,我们只需用它注解处理程序:

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

在这里,我们将Roles装饰器元数据附加到create()方法,表示只允许拥有管理员角色的用户访问此路由

另外,与其使用Reflector#createDecorator方法,我们也可以使用内置的@SetMetadata()装饰器。 在这里了解更多。

将所有内容组合在一起

现在,让我们回过头将这与我们的RolesGuard结合在一起。 目前,它在所有情况下都只返回true,允许每个请求继续进行。 我们希望使返回值有条件地基于比较当前用户分配的角色与当前正在处理的路由所需的实际角色。 为了访问路由的角色(自定义元数据),我们将再次使用Reflector辅助类,如下所示:

roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
tip

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

warning

matchRoles() 函数内的逻辑可以根据需要简单或复杂。 这个例子的主要目的是展示守卫如何适应请求/响应周期。

有关在上下文敏感方式中使用Reflector的更多详细信息,请参阅执行上下文章节的反射和元数据部分。

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

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

请注意,在幕后,当守卫返回false时,框架会抛出ForbiddenException。 如果您想返回不同的错误响应,您应该抛出您自己特定的异常。例如:

throw new UnauthorizedException();

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

tip

如果您正在寻找如何实现授权的真实示例,请查看本章

拦截器

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

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

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

基础知识

每个拦截器都实现了 intercept() 方法,该方法接受两个参数。 第一个是 ExecutionContext 实例(与守卫的对象完全相同)。 ExecutionContext 继承自 ArgumentsHost。我们在异常过滤器章节中已经看到过 ArgumentsHost。 在那里,我们看到它是对传递给原始处理程序的参数的包装,根据应用程序类型包含不同的参数数组。 您可以回到异常过滤器章节了解更多关于这个主题的信息。

执行上下文

通过扩展 ArgumentsHostExecutionContext 还添加了几个新的辅助方法,提供有关当前执行过程的额外详细信息。 这些详细信息对于构建更通用的拦截器,在广泛的控制器、方法和执行上下文中工作,是有帮助的。 在这里了解有关ExecutionContext的更多信息。

调用处理程序

第二个参数是 CallHandlerCallHandler 接口实现了 handle() 方法, 您可以在拦截器的某个点使用它来调用路由处理程序方法。 如果在 intercept() 方法的实现中没有调用 handle() 方法,路由处理程序方法将根本不会被执行。

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

例如,考虑一下传入的 POST /cats 请求。 此请求将发送到 CatsController 内定义的 create() 处理程序。 如果在任何地方调用了不调用 handle() 方法的拦截器,create() 方法将不会被执行。 一旦调用了 handle()(并且其 Observable 已经返回),将触发 create() 处理程序。 并且一旦通过 Observable 收到响应流,就可以在流上执行其他操作,并将最终结果返回给调用方。

切面拦截

我们首先将看一下使用拦截器记录用户交互的用例(例如,存储用户调用、异步调度事件或计算时间戳)。 下面展示了一个简单的 LoggingInterceptor

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

NestInterceptor<T, R>是一个泛型接口,其中T表示Observable<T>(支持响应流)的类型, 而R是由Observable<R>包装的值的类型。

tip

拦截器,就像控制器、提供者、守卫等一样,可以通过其构造函数注入依赖项

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

绑定拦截器

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

cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
tip

@nestjs/common导入@UseInterceptors()

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

Before...
After... 1ms

请注意,我们传递了 LoggingInterceptor 类型(而不是一个实例),将实例化的责任留给框架并启用依赖注入。 与管道、守卫和异常过滤器一样,我们也可以传递一个现场实例:

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

如上所述,上述构造将拦截器附加到此控制器声明的每个处理程序。 如果我们想将拦截器的范围限制为单个方法,我们只需在方法级别应用装饰器

为了设置全局拦截器,我们使用 Nest 应用程序实例的 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 {}
tip

使用这种方法进行拦截器的依赖注入时,请注意无论在哪个模块中使用此结构,拦截器实际上都是全局的。 应该在哪里进行这个操作?选择定义拦截器的模块(如上例中的LoggingInterceptor)。 此外,useClass不是处理自定义提供程序注册的唯一方式。在这里了解更多信息。

响应映射

我们已经知道handle()返回一个Observable。 该流包含从路由处理程序返回的值,因此我们可以使用 RxJS 的map()操作符轻松地对其进行变异。

warning

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

让我们创建TransformInterceptor,它将以一种微不足道的方式修改每个响应,以演示这个过程。 它将使用 RxJS 的map()操作符将响应对象分配给新创建的对象的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 => ({ data })));
}
}
tip

Nest 拦截器支持同步和异步的 intercept() 方法。如果需要,您可以简单地将方法切换为异步。

使用上述结构,当有人调用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.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

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

流覆盖

有几个原因可能导致我们有时希望完全阻止调用处理程序并返回不同的值。 一个明显的例子是实现缓存以提高响应时间。让我们来看一个简单的缓存拦截器,它从缓存返回响应。 在实际示例中,我们可能要考虑其他因素,如 TTL、缓存失效、缓存大小等,但这超出了本讨论的范围。 这里我们提供一个演示主要概念的基本示例。

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 的 of() 操作符创建,因此根本不会调用路由处理程序。 当有人调用使用 CacheInterceptor 的端点时,响应(一个硬编码的空数组)将立即返回。 为了创建一个通用的解决方案,您可以利用Reflector并创建一个自定义装饰器。 在守卫章节中对Reflector进行了详细描述。

更多操作符

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

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

自定义路由装饰器

Nest 是建立在一种称为装饰器的语言特性周围的。 装饰器在许多常用的编程语言中是一个众所周知的概念,但在 JavaScript 的世界中,它们仍然相对较新。 为了更好地理解装饰器的工作原理,我们建议阅读这篇文章。以下是一个简单的定义:

tip

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"]
}

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

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

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

tip

对于 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);
}
tip

请注意,validateCustomDecorators 选项必须设置为 true。 默认情况下,ValidationPipe 不会验证使用自定义装饰器注释的参数。

装饰器组合

Nest 提供了一个帮助方法来组合多个装饰器。 例如,假设您想将所有与身份验证相关的装饰器组合成一个单独的装饰器,可以使用以下构造:

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

这具有通过单个声明应用所有四个装饰器的效果。

tip

@nestjs/swagger包中的@ApiHideProperty()装饰器不可组合, 并且无法与applyDecorators函数一起正常工作。