Nest(三):Controllers

译自:https://docs.nestjs.com/controllers

控制器负责处理传入的请求并将响应返回给客户机。


image.png

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

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

Routing

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

// cats.controller.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 cats命令。

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

在上面的示例中,当向该端点发出GET请求时,Nest将该请求路由到用户定义的findAll()方法中。注意,我们在这里选择的方法名完全是任意的。显然,我们必须声明一个方法来绑定路由,但是Nest并不为所选择的方法名赋予任何意义。这个方法将返回一个200状态码和相关的响应,在本例中,它只是一个字符串。为什么会这样?为了解释这个问题,我们首先介绍一下Nest使用两种不同的选项来处理响应的概念:

Standard (推荐) 使用这个内置方法,当请求处理程序返回JavaScript对象或数组时,它将自动序列化为JSON。但是,当它返回一个字符串时,Nest将只发送一个字符串,而不尝试序列化它。这使得响应处理非常简单:只返回值,其余的由Nest负责。此外,除了使用201的POST请求外,默认情况下响应的状态代码总是200。通过在handler级别添加@HttpCode(…)装饰器,我们可以很容易地改变这种行为(请参阅状态代码)。
Library-specific 我们可以使用特定于库的响应对象(例如,Express),它可以使用方法处理程序签名中的@Res()装饰器注入(例如,findAll(@Res() response))。使用这种方法,您有能力(和责任)使用该对象公开的本机响应处理方法。例如,使用Express,可以使用response.status(200).send()这样的代码构造响应。

警告
您不能同时使用两种方法。Nest检测处理程序何时使用@Res()或@Next(),指示您选择了特定于库的选项。如果同时使用这两种方法,则此单一路由将自动禁用标准方法,并且不再按预期工作。

Request object

处理程序通常需要访问客户机请求的详细信息。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';
  }
}

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

请求对象表示HTTP请求,并具有请求查询字符串、参数、HTTP头和主体的属性(请参阅此处的详细信息)。在大多数情况下,不需要手动获取这些属性。我们可以使用专用的装饰器,比如@Body()或@Query(),这些都是现成的。下面是所提供的装饰器和它们所表示的特定于平台的普通对象的列表。

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

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

Resources

前面,我们定义了一个端点来获取cats资源(GET route)。我们通常还希望提供一个创建新记录的端点。为此,我们创建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请求端点装饰器——@Put()@Delete()@Patch()@Options()@Head()@All()。每个都表示其各自的HTTP请求方法。

Route wildcards

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

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

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

Status code

如前所述,默认情况下响应状态代码总是200,除了POST请求是201之外。通过在处理程序级别添加@HttpCode(…)装饰器,我们可以很容易地更改此行为。

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

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

Headers

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

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

Route parameters

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

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

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

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

Routes order

注意路由注册顺序(每个路由的方法出现在类中的顺序)很重要。假设您有一个通过标识符(cats/:id)返回cats的路由。如果您在类定义中注册了它下面的另一个端点,该类定义将一次返回所有的cats(cats),那么GET /cats请求将永远不会按预期命中第二个处理程序,因为所有路径参数都是可选的。请看下面的例子:

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

  @Get()
  findAll() {
    // This endpoint will never get called
    // because the "/cats" request is going
    // to be captured by the "/cats/:id" route handler
  }
}

为了避免这些副作用,只需将findAll()声明(包括它的装饰器)移动到findOne()之上。

Scopes

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

然而,当控制器的基于请求的生存期可能是所期望的行为时,也存在一些边缘情况,例如GraphQL应用程序中的每个请求缓存、请求跟踪或多租户。在这里学习如何控制范围。

Asynchronicity

我们热爱现代JavaScript,我们知道数据提取大部分是异步的。这就是为什么Nest支持async函数并能很好地工作。

提示
了解更多关于async / await 特性。

每个async函数都必须返回一个Promise。这意味着您可以返回一个deferred值,Nest将能够自行解析该值。让我们来看一个例子:

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

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

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

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

Request payloads

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

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

让我们创建CreateCatDto类:

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

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

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

Handling errors

有一个单独的章节是关于处理错误的。

Full resource sample

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

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

Getting up and running

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

控制器总是属于一个模块,这就是为什么我们将控制器数组包含在@Module()装饰器中。由于我们还没有定义任何其他模块,除了根应用程序模块,我们将使用它来介绍CatsController:

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

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

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

Appendix: Library-specific approach

到目前为止,我们已经讨论了操作响应的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([]);
  }
}

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335