Skip to main content
info

本文翻译自官网文档 v9:https://docs.nestjs.com/

大家一起交流学习Nestjs React全栈技术,QQ群:298304381

Controller 控制器

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

来自静态目录的图像

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

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

提示

为了快速创建一个内置验证的 CRUD 控制器,你可以使用 CLI 的 CRUD 生成器:nest g resource [name]

路由

在下面的例子中,我们将使用@Controller()装饰器,这是定义一个基本控制器所需要的。我们将指定一个可选的路由路径前缀为cats。在@Controller()装饰器中使用路径前缀,可以让我们轻松地将一组相关的路由分组,并尽量减少重复的代码。例如,我们可以选择将一组管理与客户实体互动的路由归入路由/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)和路由路径相对应。

什么是路由路径?处理程序的路径是通过连接控制器声明@Controller('cats')中的cats,以及方法装饰器@Get()中指定的任何路径来确定的。由于我们已经为每个路由(cats)声明了一个前缀,并且没有在装饰器@Get()中添加任何路径信息,Nest 就会将GET /cats请求映射到findAll()的Get方法上来。

如前所述,路径包括@Controller()路径前缀和请求方法findAll()装饰器@Get()中配置的路径。例如,@Controller('customers')的路径前缀与装饰器@Get('profile')相结合,将产生一个GET /customers/profile这样的路由映射请求。

在我们上面的例子中,当向该端点发出 GET 请求时,Nest 将该请求路由到我们用户定义的 findAll()方法。注意,我们在这里选择的方法名称是完全任意的。显然,我们必须声明一个方法来绑定路由,但 Nest 并不重视所选择的方法名称的任何意义。

这个方法将返回一个 200 状态代码和相关的响应,在这种情况下,它只是一个字符串。为什么会发生这种情况?为了解释,我们首先要介绍一个概念,即 Nest 采用了两种不同的选项来操作响应。

选项描述
Standard (recommended)使用这种内置方法,当请求处理程序返回一个 JavaScript 对象或数组时,它将自动被序列化为 JSON。然而,当它返回一个 JavaScript 原始类型(例如,字符串、数字、布尔值)时,Nest 将只发送值,而不尝试对其进行序列化。这使得响应处理变得简单:只需返回值,Nest 就会处理其余的事情。此外,响应的状态代码默认总是 200,除了使用 201 的 POST 请求。我们可以通过在处理程序级别添加@HttpCode(...)装饰器来轻松改变这一行为(见状态代码)。
Library-specific我们可以使用库特定的(例如 Express)响应对象,它可以使用方法处理签名中的@Res()装饰器注入(例如 findAll(@Res() response))。通过这种方法,你有能力使用该对象所暴露的本地响应处理方法。例如,在 Express 中,你可以使用 response.status(200).send()这样的代码来构造响应。
警告

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

请求对象

处理程序经常需要访问客户端的请求细节。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";
}
}
提示

为了利用表达式类型的优势(如上面的 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 平台(例如ExpressFastify)的类型兼容,Nest 提供了@Res()@Response()装饰器。@Res()@Response() 的简单别名。两者都直接暴露了底层的本地平台响应对象接口。当使用它们时,你也应该导入底层库的类型(例如,@types/express)以充分利用。请注意,当你在方法处理程序中注入@Res()@Response()时,你将 Nest 放入该处理程序的库特定模式中,并且你将负责管理响应。当这样做时,你必须通过调用响应对象(如 res.json(...)res.send(...))来发出某种响应,否则 HTTP 服务器会挂起。
tip

如何定义自己的装饰器,请学习这个章节

资源

早些时候,我们定义了一个方法来获取cats的资源(GET 路由)。我们通常也想提供一个创建新记录的方法。为此,让我们创建一个 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 方法提供装饰器。 @Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), 和 @Head()。此外,@All()定义了一个可以处理所有这些方法的端点。

路由通配符

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

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

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

状态代码

如前所述,响应状态代码默认总是 200,除了 POST 请求是 201。我们可以通过在处理程序级别添加@HttpCode(...)装饰器来轻松改变这一行为。

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

@nestjs/common包导入HttpCode

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

头信息

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

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

@nestjs/common包导入Header

重定向

要将一个响应重定向到一个特定的 URL,你可以使用@Redirect()装饰器或一个库特定的响应对象(并直接调用res.redirect())。 @Redirect()需要两个参数,urlstatusCode,都是可选的。如果省略的话,statusCode的默认值是302

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

有时你可能想动态地确定 HTTP 状态代码或重定向 URL。通过从路由处理方法中返回一个对象来做到这一点,类似如下。

{
"url": string,
"statusCode": number
}

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

@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 的猫)。为了定义带参数的路由,我们可以在路由的路径中添加路由参数令牌,以捕获请求 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参数。你也可以向装饰器传递一个特定的参数标记,然后在方法主体中直接引用路由参数的名称。

提示

@nestjs/common包导入Param

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

子域路由

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

@Controller({ host: "admin.example.com" })
export class AdminController {
@Get()
index(): string {
return "Admin page";
}
}
caution

由于 Fastify 缺乏对嵌套路由器的支持,当使用子域路由时,应该使用(默认)Express 适配器来代替

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

@Controller({ host: ":account.example.com" })
export class AccountController {
@Get()
getInfo(@HostParam("account") account: string) {
return account;
}
}

范畴

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

但是,在极端情况下,基于请求的控制器生存周祁可能是所需的行为(However, there are edge-cases when request-based lifetime of the controller may be the desired behavior),例如 GraphQL 应用程序中的每个请求缓存,请求跟踪或多租户。在这里了解如何控制作用域

异步性

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

提示

学习async / await请点击这里

每个异步函数都必须返回一个 Promise。这意味着你可以返回一个延迟值,Nest 将能够自己解决。让我们来看看这个例子。

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

上述代码是完全有效的。此外,Nest 路由处理程序通过能够返回 RxJS 可观察流而变得更加强大。Nest 将自动订阅下面的源并获取最后一个发出的值(一旦流完成)--( Nest will automatically subscribe to the source underneath and take the last emitted value (once the stream is completed))

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

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

请求的有效载荷

我们之前的 POST 路由处理器的例子没有接受任何客户端参数。让我们通过在这里添加@Body()装饰器来解决这个问题。

但首先(如果你使用 TypeScript),我们需要确定 DTO(数据传输对象)模式。DTO 是一个定义了数据如何在网络上发送的对象。我们可以通过使用 TypeScript 接口,或通过简单的类来确定 DTO 模式。有趣的是,我们在这里推荐使用类。为什么呢?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中被保留为真实的实体。另一方面,由于 TypeScript 接口在转译过程中被移除,Nest 不能在运行时引用它们。这一点很重要,因为像管道这样的功能在运行时可以访问变量的元类型,从而实现更多的可能性。

我们来创建CreateCatDto类。

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

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

提示

我们的 Validation Pipe 可以过滤掉那些不应该被方法处理程序接收的属性。在这种情况下,我们可以将可接受的属性列入白名单,任何不包括在白名单中的属性都会自动从结果对象中剥离。在CreateCatDto的例子中,我们的白名单是名称、年龄和品种属性在这里了解更多。

处理错误

这里有一个关于处理错误的单独章节(即,与异常一起工作)。

完整例子

下面是一个例子,利用几个可用的装饰器来创建一个基本的控制器。这个控制器暴露了一些方法来访问和操作内部数据。

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`;
}
}
提示

Nest CLI 提供了一个自动生成器(示意图),自动生成所有的模板代码,以帮助我们避免做这些事情,并使开发人员的体验更加简单。在这里阅读更多关于这个功能的信息

开始运行

随着上述控制器的完全定义,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 现在可以很容易地反映出哪些控制器必须被安装。

库的特定方法

到目前为止,我们已经讨论了操作响应的 Nest 标准方式。操作响应的第二种方式是使用库特定的响应对象。为了注入一个特定的响应对象,我们需要使用 @Res() 装饰器。为了显示差异,让我们把CatsController重写成以下内容。

CatsController.ts
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([]);
}
}

虽然这种方法是可行的,而且事实上通过提供对响应对象的完全控制,在某些方面确实有更大的灵活性(头文件的操作、库的特定功能等等),但是应该谨慎使用。一般来说,这种方法不那么明确,而且确实有一些缺点。主要的缺点是,你的代码变得依赖于平台(因为底层库在响应对象上可能有不同的 API),而且更难测试(你必须模拟响应对象,等等)。

另外,在上面的例子中,你失去了与依赖 Nest 标准响应处理的 Nest 功能的兼容性,如拦截器和@HttpCode() / @Header() 装饰器。为了解决这个问题,你可以将 passthrough 选项设置为 true,如下所示。

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

现在,你可以与本地响应对象进行交互(例如,根据某些条件设置 cookies 或头信息),但把其余的事情留给框架。