控制器

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

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器都有不止一条路由,不同的路由可以执行不同的操作。

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

提示 为了快速创建内置 validation 的 CRUD 控制器,你可以使用 CLI 的 增删改查生成器nest g resource [name]

路由#

在下面的示例中,我们将使用 @Controller() 装饰器,这是定义基本控制器所必需的。我们将指定 cats 的可选路由路径前缀。在 @Controller() 装饰器中使用路径前缀可以让我们轻松地对一组相关路由进行分组,并最大限度地减少重复代码。例如,我们可以选择将一组路由分组,这些路由管理与路由 /cats 下的猫实体的交互。在这种情况下,我们可以在 @Controller() 装饰器中指定路径前缀 cats,这样我们就不必为文件中的每个路由重复该部分路径。

cats.controller.ts

JS

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

提示 要使用 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 基本类型(例如 stringnumberboolean)时,Nest 将仅发送该值,而不尝试对其进行序列化。这使得响应处理变得简单:只需返回值,Nest 就会处理剩下的事情。

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

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

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

请求对象#

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

cats.controller.ts

JS

1
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 类型(如上面的 request: Request 参数示例),请安装 @types/express 软件包。

请求对象表示 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() 的别名。两者都直接暴露底层原生平台 response 对象接口。使用它们时,你还应该导入底层库(例如 @types/express)的类型以充分利用它们。请注意,当你在方法处理程序中注入 @Res()@Response() 时,你会将 Nest 置于该处理程序的库特定模式,并且你将负责管理响应。这样做时,你必须通过调用 response 对象(例如,res.json(...)res.send(...))来触发某种响应,否则 HTTP 服务器将挂起。

提示 要了解如何创建你自己的自定义装饰器,请访问 this 章。

资源#

之前,我们定义了一个端点来获取 cats 资源(GET 路由)。我们通常还希望提供一个创建新记录的端点。为此,我们创建 POST 处理程序:

cats.controller.ts

JS

1
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() 定义了一个端点来处理所有这些。

路由通配符#

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

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

'ab*cd' 路由路径将匹配 abcdab_cdabecd 等。字符 ?+*() 可以在路由路径中使用,并且是它们对应的正则表达式的子集。连字符 (-) 和点 (.) 由基于字符串的路径逐字解释。

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

状态码#

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

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

提示@nestjs/common 包中导入 HttpCode

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

标头#

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

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

提示@nestjs/common 包中导入 Header

重定向#

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

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

1
@Get() @Redirect('https://nest.nodejs.cn', 301)

提示 有时你可能想要动态确定 HTTP 状态代码或重定向 URL。通过返回遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象来完成此操作。

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

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

路由参数#

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

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

JS

1
@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 给装饰器,然后在方法体中直接通过名称引用路由参数。

提示@nestjs/common 包中导入 Param

JS

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

子域路由#

@Controller 装饰器可以采用 host 选项来要求传入请求的 HTTP 主机匹配某个特定值。

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

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

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

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

作用域#

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

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

异步性#

我们喜欢现代 JavaScript,并且知道数据提取大多是异步的。这就是 Nest 支持 async 功能并与它配合使用的原因。

提示 详细了解 async / await 功能 此处

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

cats.controller.ts

JS

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

上面的代码是完全有效的。此外,Nest 路由处理程序更强大,因为它能够返回 RxJS 可观察流。Nest 将自动订阅下面的源并获取最后触发的值(一旦流完成)。

cats.controller.ts

JS

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

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

请求负载#

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

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

让我们创建 CreateCatDto 类:

create-cat.dto.ts

JS

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

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

cats.controller.ts

JS

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

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

处理错误#

有一个单独的章节是关于处理错误(即处理异常)此处

完整资源样本#

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

cats.controller.ts

JS

1
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 CLI 提供了一个生成器(原理图),可以自动生成所有样板代码,帮助我们避免执行所有这些操作,并使开发者体验更加简单。阅读有关此功能的更多信息 此处

启动并运行#

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

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

app.module.ts

JS

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

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

特定于库的方法#

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

JS

1
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 功能的兼容性,例如拦截器和 @HttpCode() / @Header() 装饰器。要解决此问题,你可以将 passthrough 选项设置为 true,如下所示:

JS

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

现在你可以与原生响应对象进行交互(例如,根据特定条件设置 cookie 或标头),但将其余部分留给框架。