控制器

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

img

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

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

路由

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

cats.controller.ts

  1. import { Controller, Get } from '@nestjs/common';
  2. @Controller('cats')
  3. export class CatsController {
  4. @Get()
  5. findAll(): string {
  6. return 'This action returns all cats';
  7. }
  8. }

?> 要使用 CLI 创建控制器,只需执行 $ nest g controller cats 命令。

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

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

此函数将返回 200 状态代码和相关的响应,在这种情况下只返回了一个字符串。为什么会这样? 我们将首先介绍 Nest 使用两种不同的操作响应选项的概念:

标准(推荐) 使用这个内置方法,当请求处理程序返回一个 JavaScript 对象或数组时,它将自动序列化为 JSON。但是,当它返回一个 JavaScript 基本类型(例如string、number、boolean)时,Nest 将只发送值,而不尝试序列化它。这使响应处理变得简单:只需要返回值,其余的由 Nest负责。
此外,响应的状态码默认情况下始终为 200,但使用 201POST请求除外。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为 (状态代码
类库特有的 我们可以在函数签名通过 @Res() 注入类库特定的 响应对象(例如,Express),使用此函数,您具有使用该对象的响应处理函数。例如,使用 Express,您可以使用类似代码构建响应 response.status(200).send()

!> 注意! 禁止同时使用这两种方法。 Nest 检测处理程序是否正在使用 @Res()@Next(),如果两个方法都用了的话, 那么在这里的标准方式就是自动禁用此路由, 你将不会得到你想要的结果。

Request

许多端点需要访问客户端的请求细节。实际上,Nest 正使用类库特有(默认是express)的请求对象。因此,我们可以强制 Nest 使用 @Req() 装饰器将请求对象注入处理程序。

cats.controller.ts

  1. import { Controller, Get, Req } from '@nestjs/common';
  2. import { Request } from 'express';
  3. @Controller('cats')
  4. export class CatsController {
  5. @Get()
  6. findAll(@Req() request: Request): string {
  7. return 'This action returns all cats';
  8. }
  9. }

?> 为了在 express 中使用 Typescript (如 request: Request 上面的参数示例所示),请安装 @types/express

Request 对象表示 HTTP 请求,并具有 Request 查询字符串,参数,HTTP 标头 和 正文的属性(在这里阅读更多),但在大多数情况下, 不必手动获取它们。 我们可以使用专用的装饰器,比如开箱即用的 @Body()@Query() 。 下面是装饰器和 普通表达对象的比较。

@Request() 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

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

?> 想要了解如何创建自定义的装饰器,阅读这一章

资源

我们已经创建了一个端点来获取数据(GET 路由)。 我们通常还希望提供一个创建新记录的端点。为此,让我们创建 POST 处理程序:

cats.controller.ts

  1. import { Controller, Get, Post } from '@nestjs/common';
  2. @Controller('cats')
  3. export class CatsController {
  4. @Post()
  5. create(): string {
  6. return 'This action adds a new cat';
  7. }
  8. @Get()
  9. findAll(): string {
  10. return 'This action returns all cats';
  11. }
  12. }

就这么简单。Nest以相同的方式提供其余的端点装饰器- @Put()@Delete()@Patch()@Options()@Head()@All()。这些表示各自的 HTTP请求方法。

路由通配符

路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

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

以上路由地址将匹配 abcdab_cdabecd 等。字符 ?+* 以及 () 是它们的正则表达式对应项的子集。连字符 (-) 和点 (.) 按字符串路径解析。

状态码

如前面所说,默认情况下,响应的状态码总是200,除了 POST 请求外,此时它是201,我们可以通过在处理程序层添加@HttpCode(...) 装饰器来轻松更改此行为。

  1. @Post()
  2. @HttpCode(204)
  3. create() {
  4. return 'This action adds a new cat';
  5. }

?> HttpCode 需要从 @nestjs/common 包导入。

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

Headers

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

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

?> Header 需要从 @nestjs/common 包导入。

重定向

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

@Redirect() 带有必需的 url参数和可选的 statusCode参数。 如果省略,则 statusCode 默认为 302

  1. @Get()
  2. @Redirect('https://nestjs.com', 301)

有时您可能想动态确定HTTP状态代码或重定向URL。通过从路由处理程序方法返回一个形状为以下形式的对象:

  1. {
  2. "url": string,
  3. "statusCode": number
  4. }

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

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

路由参数

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

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

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

?> Param 需要从 @nestjs/common 包导入。

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

子域路由

@Controller 装饰器可以接受一个 host 选项,以要求传入请求的 HTTP 主机匹配某个特定值。

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

?> 因为 Fastify 缺乏对嵌套路由器的支持,当使用子域路由时,应该使用(默认) Express 适配器。

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

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

作用域

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

但是,存在基于请求的控制器生命周期可能是期望行为的边缘情况,例如 GraphQL 应用程序中的请求缓存,比如请求跟踪或多租户。在这里学习如何控制作用域。

Async / await

我们使用JavaScript的时候,数据读取大多是异步的。这就是为什么 Nest 支持 async 功能并与功能完美配合的原因。

?> 了解更多关于 Async / await 请点击这里

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

cats.controller.ts

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

这是完全有效的。此外,通过返回 RxJS observable 流Nest 路由处理程序更强大。Nest 将自动订阅下面的源并获取最后发出的值(在流完成后)。

cats.controller.ts

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

上面的方法都可以, 你可以选择你喜欢的方式。

请求负载

之前的 POST 路由处理程序不接受任何客户端参数。我们在这里添加 @Body() 参数来解决这个问题。

首先(如果您使用 TypeScript),我们需要确定 DTO(数据传输对象)模式。DTO是一个对象,它定义了如何通过网络发送数据。我们可以通过使用 TypeScript接口或简单的类来完成。令人惊讶的是,我们在这里推荐使用类。为什么?类是JavaScript ES6标准的一部分,因此它们在编译后的 JavaScript中保留为实际实体。另一方面,由于 TypeScript接口在转换过程中被删除,所以 Nest不能在运行时引用它们。这一点很重要,因为诸如管道之类的特性在运行时能够访问变量的元类型时提供更多的可能性。

我们来创建 CreateCatDto 类:

create-cat.dto.ts

  1. export class CreateCatDto {
  2. readonly name: string;
  3. readonly age: number;
  4. readonly breed: string;
  5. }

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

cats.controller.ts

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

处理错误

这里有一章关于处理错误(即处理异常)的单独章节。

完整示例

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

cats.controller.ts

  1. import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
  2. import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
  3. @Controller('cats')
  4. export class CatsController {
  5. @Post()
  6. create(@Body() createCatDto: CreateCatDto) {
  7. return 'This action adds a new cat';
  8. }
  9. @Get()
  10. findAll(@Query() query: ListAllEntities) {
  11. return `This action returns all cats (limit: ${query.limit} items)`;
  12. }
  13. @Get(':id')
  14. findOne(@Param('id') id: string) {
  15. return `This action returns a #${id} cat`;
  16. }
  17. @Put(':id')
  18. update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
  19. return `This action updates a #${id} cat`;
  20. }
  21. @Delete(':id')
  22. remove(@Param('id') id: string) {
  23. return `This action removes a #${id} cat`;
  24. }
  25. }

最后一步

控制器已经准备就绪,可以使用,但是 Nest 不知道 CatsController 是否存在,所以它不会创建这个类的一个实例。

控制器总是属于模块,这就是为什么我们将 controllers 数组保存在 @module() 装饰器中。 由于除了根 ApplicationModule,我们没有其他模块,所以我们将使用它来介绍 CatsController

app.module.ts

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

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

类库特有方式

到目前为止,我们已经讨论了 Nest 操作响应的标准方式。操作响应的第二种方法是使用类库特有的响应对象(Response)。为了注入特定的响应对象,我们需要使用 @Res() 装饰器。为了对比差异,我们重写 CatsController

  1. import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
  2. import { Response } from 'express';
  3. @Controller('cats')
  4. export class CatsController {
  5. @Post()
  6. create(@Res() res: Response) {
  7. res.status(HttpStatus.CREATED).send();
  8. }
  9. @Get()
  10. findAll(@Res() res: Response) {
  11. res.status(HttpStatus.OK).json([]);
  12. }
  13. }

虽然这种方法有效,并且事实上通过提供响应对象的完全控制(标准操作,库特定的功能等)在某些方面允许更多的灵活性,但应谨慎使用。这种方式非常不清晰,并且有一些缺点。 主要是失去了与依赖于 Nest 标准响应处理的 Nest 功能的兼容性,例如拦截器和 @HttpCode() 装饰器。此外,您的代码可能变得依赖于平台(因为底层库可能在响应对象上有不同的 API),并且更难测试(您必须模拟响应对象等)。

因此,在可能的情况下,应始终首选 Nest 标准方法。

译者署名

用户名 头像 职能 签名
@zuohuadong 控制器 - 图2 翻译 专注于 caddy 和 nest,@zuohuadong at Github
@Drixn 控制器 - 图3 翻译 专注于 nginx 和 C++,@Drixn
@Armor 控制器 - 图4 翻译 专注于 Java 和 Nest,@Armor
@tangkai 控制器 - 图5 翻译 专注于 React,@tangkai
@havef 控制器 - 图6 校正 数据分析、机器学习、TS/JS技术栈 @havef