译自: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标准方法。
网友评论