Skip to main content

控制器

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

Nest.js 控制器

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

为了创建基本控制器,我们使用类和装饰器。 装饰器将类与所需的元数据关联起来,并使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 注入请求对象,从而访问请求对象

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

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

为了利用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(...))

note

要了解如何创建您自己的自定义装饰器,请访问本章

资源

之前,我们定义了一个端点(endpoint)获取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)
note

有时您可能想要动态确定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能够自行解析的延迟值。让我们看一个例子:

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

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

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

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

请求负载

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

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

让我们创建CreateCatDto类:

create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}

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

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

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

处理错误

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

完整资源样本

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

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

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

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

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

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

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

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

启动并运行

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

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

app.module.ts
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), 但将其余的留给框架。