控制器 Controllers

导读

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

img

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

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

提示

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

路由

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

ts
ts
ts
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,但 POST 请求除外,它们使用 201。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为(请参阅状态代码)。
特定于库 我们可以使用特定于库(例如 Express)的 响应对象,可以使用方法处理程序签名中的 @Res() 装饰器注入该对象(例如 findAll(@Res() response))。通过这种方法,您可以使用该对象公开的本机响应处理方法。例如,使用 Express,您可以使用 response.status(200).send() 之类的代码构建响应。

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

请求对象

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

ts
ts
ts
// cats.controller
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 处理程序:

ts
ts
ts
// cats.controller
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() 定义了一个处理所有这些方法的端点。

路由通配符

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

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

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

警告

只有 express 支持在路由中间使用通配符。

状态代码

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

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

Import HttpCode from the @nestjs/common package.

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

标头

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

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

@nestjs/common 包导入 Header

重定向

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

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

ts
@Get()
@Redirect('https://nestjs.com', 301)
提示

有时您可能希望动态确定 HTTP 状态代码或重定向 URL。通过返回遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象来执行此操作。

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

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

路由参数

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

提示

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

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

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

提示

@nestjs/common 包导入 Param

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

子域路由

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

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

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

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

ts
@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 能够自行解析的延迟值。让我们看一个例子:

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

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

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

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

请求有效负载

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

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

让我们创建 CreateCatDto 类:

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

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

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

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

处理错误

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

完整资源示例

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

ts
ts
ts
// cats.controller
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'
import { CreateCatDto, ListAllEntities, UpdateCatDto } 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
import { Module } from '@nestjs/common'
import { CatsController } from './cats/cats.controller'

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

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

特定于库的方法

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

ts
ts
ts
import { Controller, Get, HttpStatus, Post, Res } 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,如下所示:

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

现在您可以与本机响应对象进行交互(例如,根据某些条件设置 cookie 或标头),但将其余部分留给框架。