Nest.js 控制器和路由
控制器
Nest.js 控制器负责处理传入的请求并将响应返回给客户端。
控制器的作用是接收应用程序的特定请求。 路由机制控制哪个控制器接收哪个请求。 很多时候,每个控制器都有多个路由,不同的路由可以执行不同的动作。
为了创建一个基本的控制器,我们使用类和装饰器。 装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)。
提示
: 为了快速创建内置验证的 CRUD 控制器,我们可以使用 CLI 的 CRUD 生成器:$ nest g resource [name]
路由
在下面的示例中,我们将使用 @Controller()
装饰器,对于定义基本控制器它是必需的。 我们将指定一个可选的路由路径前缀 cats
。 在 @Controller()
装饰器中使用路径前缀可以让我们轻松地对一组相关路由进行分组,并最大限度地减少重复代码。 例如,我们可以选择在路由 /customers
下对一组管理与 customer 实体的交互的路由进行分组。 在这种情况下,我们可以在 @Controller() 装饰器中指定路径前缀 customers
,这样我们就不必为文件中的每个路由重复该部分路径。
cats.controller.ts
import { Controller, Get } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() findAll(): string { return '该控制器返回所有的 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。 然而,当它返回一个 JavaScript 原始类型(例如,字符串、数字、布尔值)时,Nest 将只发送该值而不会对其进行序列化。 这使得响应处理变得简单:我们只需返回值,其余的由 Nest 处理。 此外,响应的状态码默认始终为 200,除了使用 201 的 POST 请求。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器轻松更改此行为(请参阅 状态码)。 |
Library-specific | 我们可以使用特定于库(例如 Express)的响应对象,它可以使用方法处理程序签名中的 @Res() 装饰器注入(例如,findAll(@Res() 响应))。 通过这种方法,我们可以使用该对象公开的本机响应处理方法。 例如,使用 Express,可以使用 response.status(200).send() 之类的代码构建响应。 |
警告
:Nest 检测处理程序何时使用@Res() 或@Next(),是通过我们选择的特定于库的选项来决定的。 如果同时使用这两种方法,则该单一路径的标准方法会自动禁用,并且将不再按预期工作。 要同时使用这两种方法(例如,通过注入响应对象来仅设置 cookie/headers 但仍将其余部分留给框架),必须在@Res({ passthrough: true })
装饰器中将passthrough
设置为true
。
路由通配符
也支持基于正则表达式的路由。 例如,星号 *
用作通配符,将匹配任何字符组合。
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
'ab*cd'
路由路径将匹配 abcd、ab_cd、abecd 等。 字符 ?
、+
、*
和 ()
可以在路由路径中使用,并且是它们的正则表达式对应物的子集。 连字符 -
和点 .
由基于字符串的路径逐个字符进行解析。
提示
:在路由中正则表达式表现异常突出,可以点此查看正则表达式更多内容
路由参数
当我们需要接受动态数据作为请求的一部分时(例如,GET /cats/1 以获取 id 为 1 的 cat),具有静态路径的路由将不起作用。 为了定义带参数的路由,我们可以在路由的路径中添加路由参数标记,来捕获请求 URL 中该位置的动态值。 下面的 @Get()
装饰器示例中的路由参数标记说明了这种用法。 可以使用 @Param()
装饰器访问以这种方式声明的路由参数,该装饰器应添加到方法签名中。
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `该方法返回编号为 #${params.id} 的 cat`;
}
@Param()
用于装饰方法参数(上面示例中的参数),并使路由参数可用作方法主体内该装饰方法参数的属性。 如上面代码所示,我们可以通过引用 params.id
来访问 id 参数。 也可以将特定的参数标记传递给装饰器,然后在方法体中直接通过名称引用路由参数。
提示
:从 @nestjs/common 包中导入 Param。
@Get(':id')
findOne(@Param('id') id: string): string {
return `该方法返回编号为 #${id} 的 cat`;
}
子域路由
@Controller
装饰器可以采用 host
选项来要求传入请求的 HTTP 主机匹配某个特定值。
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return '管理页面';
}
}
警告
:由于 Fastify 缺乏对嵌套路由器的支持,所以在使用子域路由时,应该使用(默认)Express 适配器。
与路由路径类似,hosts 选项可以使用标记来捕获主机名中该位置的动态值。 下面的 @Controller() 装饰器示例中的主机参数标记演示了这种用法。 可以使用 @HostParam()
装饰器访问以这种方式声明的主机参数,该装饰器应添加到方法签名中。
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
Request 对象
处理程序通常需要访问客户端请求的详细信息。 Nest 提供对底层平台的 Request 对象的访问(默认使用 Express)。 我们可以通过将 @Req()
装饰器添加到处理程序的签名中来指示 Nest 注入它来访问 Request 对象。
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 '该控制器返回所有的 cats!'; } }
提示
:为了利用快速类型(如上面的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 置于该处理程序的 Library-specific 模式,并且将负责管理响应。 这样做时,则必须通过调用 response
对象(例如 res.json(...)
或 res.send(...)
)来发出某种响应,否则 HTTP 服务器将挂起。
Resources 资源
之前,我们定义了一个端点来获取 cats 资源(GET 路由)。 我们通常还希望提供一个创建新记录的端点。 为此,让我们创建 POST 处理程序:
cats.controller.ts
import { Controller, Get, Post } from '@nestjs/common'; @Controller('cats') export class CatsController { @Post() create(): string { return '该方法添加一个 cat'; } @Get() findAll(): string { return '该方法返回所有的 cats'; } }
我们使用postman 发起一个 post 请求。
状态码
如前所述,默认情况下响应状态代码始终为 200,除了 201 的 POST 请求。我们可以通过在处理程序级别添加 @HttpCode(...)
装饰器轻松更改此行为。
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
提示
: 从 @nestjs/common 包中导入 HttpCode。
通常,状态代码不是静态的,而是取决于各种因素。 在这种情况下,我们可以使用特定于库的 response
(使用 @Res() 注入)对象(或者,如果出现错误,则抛出异常)。
Headers 标头
要指定自定义响应标头,可以使用 @Header()
装饰器或特定于库的 response 对象(并直接调用 res.header()
)。
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
提示
:从 @nestjs/common 包中导入 Header。
重定向
要将响应重定向到特定 URL,可以使用 @Redirect()
装饰器或特定于库的 response 对象(并直接调用 res.redirect()
)。
@Redirect() 有两个参数,url 和 statusCode,都是可选的。 如果省略,statusCode 的默认值为 302(已找到)。
@Get()
@Redirect('https://nestjs.com', 301)
有时我们可能希望动态确定 HTTP 状态码或重定向 URL。 通过从具有以下形式的路由处理程序方法返回一个对象来做到这一点:
{
"url": string,
"statusCode": number
}
返回值将覆盖传递给 @Redirect()
装饰器的任何参数。 例如:
@Get('docs')
@Redirect('https://www.jiyik.com', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://www.jiyik.com/w/' };
}
}
作用域
对于来自不同编程语言背景的人来说,在 Nest 中得知几乎所有内容都在传入请求之间共享可能是出乎意料的。 我们有一个到数据库的连接池、具有全局状态的单例服务等。请记住,Node.js 不遵循 请求/响应多线程无状态模型,其中每个请求都由单独的线程处理。 因此,使用单例实例对我们的应用程序来说是完全安全的。
然而,当控制器的基于请求的生命周期可能是期望的行为时,存在边缘情况,例如 GraphQL 应用程序中的每个请求缓存、请求跟踪或多租户。
异步性
我们喜欢现代 JavaScript,而且我们知道数据提取大多是异步的。 这就是为什么 Nest 支持并与异步函数配合良好的原因。
每个异步函数都必须返回一个 Promise
。 这意味着我们可以返回 Nest 能够自行解析的延迟值。 让我们看一个例子:
@Get()
async findAll(): Promise<any[]> {
return [];
}
上面的代码是完全有效的。 此外,由于能够返回 RxJS 可观察流,Nest 路由处理程序更加强大。 Nest 将自动订阅下面的源并获取最后一个发出的值(一旦流完成)。
@Get()
findAll(): Observable<any[]> {
return of([]);
}
上述两种方法都有效,我们可以使用任何适合自己要求的方法。
Request 有效载荷
我们之前的 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:
cats.controller.ts
@Post() async create(@Body() createCatDto: CreateCatDto) { return '该方法添加一个新的 cat'; }
提示
: 我们的 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 '该方法添加一个新的 cat'; } @Get() findAll(@Query() query: ListAllEntities) { return `该方法返回所有的 cats (limit: ${query.limit} items)`; } @Get(':id') findOne(@Param('id') id: string) { return `该方法返回编号为 #${id} 的 cat`; } @Put(':id') update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) { return `该方法更新编号为 #${id} 的 cat`; } @Delete(':id') remove(@Param('id') id: string) { return `该方法移除编号为 #${id} 的 cat`; } }
提示
: Nest CLI 提供了一个生成器(schematic),它会自动生成所有模板代码,以帮助我们避免所有无效的代码编写,并使开发人员的体验更加简单。
运行示例
完全定义上述控制器后,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 现在可以轻松地反映必须安装哪些控制器。