💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### 控制器 控制器负责处理传入的**请求**并将**响应**返回给客户端。 ![](https://docs.nestjs.com/assets/Controllers_1.png) 控制器的目的是接收应用程序的特定请求。**路由**机制控制哪个控制器接收哪个请求。很多时候,每个控制器都有多个路由,不同的路由可以执行不同的动作。 为了创建一个基本的控制器,我们使用类和**装饰器**。装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)。 > **提示:** 为了快速创建内置[验证的 CRUD 控制器,您可以使用 CLI 的](https://docs.nestjs.com/techniques/validation)[CRUD 生成器](https://docs.nestjs.com/recipes/crud-generator#crud-generator):`nest g resource [name]`。 #### 路由[#](#routing) 在下面的示例中,我们将使用`@Controller()`装饰器,它是定义基本控制器所**必需的。**我们将指定一个可选的路由路径前缀`cats`。在装饰器中使用路径前缀`@Controller()`可以让我们轻松地对一组相关路由进行分组,并最大限度地减少重复代码。例如,我们可以选择将一组用于管理与`/customers`下的客户实体进行互动的路由进行分组。这样,我们可以在`@Controller()`装饰器中指定路径前缀`customers`,这样我们就不必为文件中的每个路由重复该部分路径。 /\* cats.controller.ts \*/ ~~~typescript 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 使用两种**不同**的选项来操作响应的概念: | | | | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 标准(推荐) | 使用这个内置方法,当请求处理程序返回一个 `JavaScript` 对象或数组时,它将自动序列化为 `JSON`。但是,当它返回一个 `JavaScript` 基本类型(例如`string、number、boolean`)时, Nest 将只发送该值而不尝试对其进行序列化。这使响应处理变得简单:只需要返回值,其余的由 Nest 负责。此外,响应的状态码默认始终为 200,除了使用 201 的 POST 请求。我们可以通过在处理程序级别添加装饰器来轻松更改此行为(请参阅状态码)。 | | 类库特有的 | 我们可以在函数签名处通过 `@Res()` 注入类库特定的响应对象(例如, `Express`)。使用此方法,你就能使用由该响应对象暴露的原生响应处理函数。例如,使用 `Express`,您可以使用 `response.status(200).send()` 构建响应 | > **警告**:Nest 检测处理程序何时使用`@Res()`or`@Next()`,表明您选择了特定于库的选项。如果同时使用这两种方法,则该单一路线的标准方法会**自动禁用**,并且将不再按预期工作。要同时使用这两种方法(例如,通过注入响应对象以仅设置 cookie/标头但仍将其余部分留给框架),你必须在装饰器`@Res({ passthrough: true })`中将`passthrough`选项设为`true` #### 请求对象[#](#request-object) 处理程序有时需要访问客户端的**请求**细节。Nest 提供了对底层平台(默认为`Express`)的[**请求对象**](http://expressjs.com/en/api.html#req)(`request`)的访问方式。我们可以在处理函数的签名中使用`@Req()`装饰器,指示 Nest 将请求对象注入处理程序。 /\* cats.controller.ts \*/ ~~~typescript 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`包。 `Request`对象代表`HTTP`请求,并具有请求查询字符串、参数、HTTP 标头(HTTP header) 和 正文(HTTP body)的属性([在此处](https://expressjs.com/en/api.html#req)阅读更多内容)。在大多数情况下,没有必要手动获取这些属性。我们可以使用开箱即用的专用装饰器,例如`@Body()`或者`@Query()`。下面是Nest提供的装饰器列表以及它们所代表的底层平台特定对象的对照列表。 | | | | ------------------------- | --------------------------------- | | `@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()`。两者都直接暴露了底层的原生平台`response`对象接口。使用它们时,您还应该导入底层库的类型(例如,`@types/express`)以充分利用它们。请注意,当您注入其中一个`@Res()`或`@Response()`方法处理程序时,您会将 Nest 置于该处理程序**的库特定模式**,并且您将负责管理响应。`response`这样做时,您必须通过调用对象(例如,`res.json(...)`或)来发出某种响应`res.send(...)`,否则 HTTP 服务器将挂起。 > **提示**:要了解如何创建自己的自定义装饰器,请访问[本章](https://docs.nestjs.com/custom-decorators)。 #### 资源[#](#resources) 之前,我们定义了一个端点来获取`cats`资源(**GET**路由)。我们通常还希望提供一个创建新记录的端点。为此,让我们创建**POST**处理程序: /\* cats.controller.ts \*/ ~~~typescript 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()`定义一个用于处理所有 HTTP 请求方法的处理程序。 #### 路由通配符[#](#route-wildcards) 路由也支持基于模式匹配。例如,星号用作通配符,将匹配任何字符组合。 ~~~typescript @Get('ab*cd') findAll() { return 'This route uses a wildcard'; } ~~~ `'ab*cd'`路由路径将匹配、`abcd`、`ab_cd`等`abecd`。字符`?`,`+`,`*`, 和`()`可以在路由路径中使用,并且是它们的正则表达式对应项的子集。连字符 (`-`) 和点 (`.`) 由基于字符串的路径逐字解析。 #### 状态码[#](#status-code) 如上所述,默认情况下响应**状态代码**始终为**200**,但 POST 请求(**默认响应状态码为201**)除外。`@HttpCode(...)`我们可以通过在处理程序级别添加装饰器来轻松更改此行为。 ~~~typescript @Post() @HttpCode(204) create() { return 'This action adds a new cat'; } ~~~ > **提示:**`HttpCode`从`@nestjs/common`包中导入。 通常,您的状态代码不是静态的,而是取决于各种因素。在这种情况下,您可以使用类库特有(library-specific)的**`response`**(使用 注入`@Res()`)对象(或者如果出现错误,则抛出异常)。 #### 标头[#](#headers) 要指定自定义响应标头,您可以使用`@Header()`装饰器或特定于库的响应对象(并`res.header()`直接调用)。 ~~~typescript @Post() @Header('Cache-Control', 'none') create() { return 'This action adds a new cat'; } ~~~ > **提示**:`Header`从`@nestjs/common`包中导入。 #### 重定向[#](#redirection) 要将响应重定向到特定 URL,您可以使用`@Redirect()`装饰器或类库特有的响应对象(并`res.redirect()`直接调用)。 `@Redirect()`接受两个参数,`url`和`statusCode`,两者都是可选的。如果省略,`statusCode`则默认值为`302(Found)`。 ~~~typescript @Get() @Redirect('https://nestjs.com', 301) ~~~ 有时您可能希望动态确定 HTTP 状态代码或重定向 URL。通过从路由处理方法返回一个如下格式的对象来做到这一点: ~~~json { "url": string, "statusCode": number } ~~~ 返回值将覆盖传递给`@Redirect()`装饰器的所有参数。例如: ~~~typescript @Get('docs') @Redirect('https://docs.nestjs.com', 302) getDocs(@Query('version') version) { if (version && version === '5') { return { url: 'https://docs.nestjs.com/v5/' }; } } ~~~ #### 路由参数[#](#route-parameters) 当您需要接受**动态数据**(dynamic data)作为请求的一部分时(例如,使用`GET /cats/1`来获取 id 为`1`的`cat`),具有静态路径的路由将不起作用。为了定义带参数的路由,我们可以在路由的路径中添加路由参数**标记(token)**,以捕获请求 URL 中该位置的动态值。下面装饰器示例中的路由参数标记`@Get()`演示了这种用法。以这种方式声明的路由参数可以使用`@Param()`装饰器访问,该装饰器应添加到函数签名中。 ~~~typescript @Get(':id') findOne(@Param() params): string { console.log(params.id); return `This action returns a #${params.id} cat`; } ~~~ `@Param()`用于装饰方法参数(`params`在上面的示例中),并使**路由**参数可用作方法主体内该装饰方法参数的属性。如上面的代码所示,我们可以`id`通过引用来访问参数`params.id`。您也可以将特定的参数标记传递给装饰器,然后在方法体中直接通过名称引用路由参数。 > **提示:**`Param`从`@nestjs/common`包中导入。 ~~~typescript @Get(':id') findOne(@Param('id') id: string): string { return `This action returns a #${id} cat`; } ~~~ #### 子域路由[#](#sub-domain-routing) 装饰器`@Controller`可以接受一个`host`选项,以要求传入请求的 HTTP 主机匹配某个特定值。 ~~~typescript @Controller({ host: 'admin.example.com' }) export class AdminController { @Get() index(): string { return 'Admin page'; } } ~~~ > **警告:**由于**Fastify**缺乏对嵌套路由器的支持,所以在使用子域路由时,应该使用(默认)`Express` 适配器。 与一个路由路径`path`类似,该`hosts`选项可以使用标记来捕获主机名中该位置的动态值。下面装饰器示例中的主机参数标记`@Controller()`演示了这种用法。以这种方式声明的主机参数可以使用`@HostParam()`装饰器访问,该装饰器应添加到方法签名中。 ~~~typescript @Controller({ host: ':account.example.com' }) export class AccountController { @Get() getInfo(@HostParam('account') account: string) { return account; } } ~~~ #### 作用域[#](#scopes) 对于来自不同编程语言背景的人来说,在 Nest 中得知几乎所有内容都是在传入请求之间共享的,这可能是出乎意料的。我们有一个到数据库的连接池、具有全局状态的单例服务等。请记住,Node.js 不遵循请求/响应多线程无状态模型,其中每个请求都由单独的线程处理。因此,使用单例实例对我们的应用程序来说是完全**安全**的。 但是,在某些极端情况下,控制器的基于请求的生命周期可能是期望行为的边缘情况,例如 GraphQL 应用程序中的每个请求缓存、请求跟踪或多租户。[在此处](https://docs.nestjs.com/fundamentals/injection-scopes)了解如何控制作用域。 #### 异步[#](#asynchronicity) 我们喜欢现代 JavaScript,而且我们知道数据提取(data extraction)大多数是**异步**的。这就是为什么 Nest 支持并很好地使用`async`函数的原因。 > **提示:**[在此处](https://kamilmysliwiec.com/typescript-2-1-introduction-async-await)了解有关`async / await`功能的 更多信息[](https://kamilmysliwiec.com/typescript-2-1-introduction-async-await) 每个异步函数都必须返回一个`Promise`.这意味着您可以返回 Nest 能够自行解析的延迟值。让我们看一个例子: /* cats.controller.ts */ ```typescript @Get() async findAll(): Promise<any[]> { return []; } ``` 上面的代码是完全有效的。此外,由于能够返回 RxJS[可观察流](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html),Nest 路由处理程序更加强大。Nest 将自动订阅下面的源并获取最后一个发出的值(在流完成)。 ```typescript /* cats.controller.ts */ @Get() findAll(): Observable<any[]> { return of([]); } ``` 上述的两种方法都是有效的,你可以选择你喜欢的方式。 #### 请求负载[#](#request-payloads) 我们之前的 POST 路由处理程序示例没有接受任何客户端参数,让我们通过在此处添加`@Body()`装饰器来解决此问题。 首先(如果您使用 TypeScript),我们需要确定**DTO**(数据传输对象)模式。`DTO` 是一个对象,定义数据如何通过网络发送的。我们可以使用**TypeScript**接口或简单的类来确定 DTO 模式。有趣的是,我们建议在这里使用**类**。为什么?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中被保留为真实实体。另一方面,由于 TypeScript 接口在转译过程中被移除,所以Nest 无法在运行时引用它们。这很重要,因为诸如**管道**(Pipe)之类的特性为在运行时访问变量的元类型提供更多的可能性。 /* create-cat.dto.ts */ ```typescript export class CreateCatDto { readonly name: string; readonly age: number; readonly breed: string; } ``` 它只有三个基本属性。此后,我们可以在 `CatsController`中使用新创建的 `DTO`: /* cats.controller.ts */ ```typescript @Post() async create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat'; } ``` > **提示**:我们在`ValidationPipe`中可以过滤掉不应被方法处理程序接收的属性。在这种情况下,我们可以将可接受的属性列入白名单,任何未包含在白名单中的属性都会自动从结果对象中剥离。在`CreateCatDto`示例中,我们的白名单是`name`、`age`和`breed`属性。[在这里](https://docs.nestjs.com/techniques/validation#stripping-properties)了解更多。 #### 处理错误[#](#handling-errors) [这里](https://docs.nestjs.com/exceptionfilters)有一个单独的章节介绍处理错误(即处理异常)。 #### 完整资源样本[#](https://docs.nestjs.com/controllers#full-resource-sample) 下面是一个使用几个可用装饰器来创建基本控制器的示例。这个控制器公开了几个方法来访问和操作内部数据。 ```typescript /* 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 提供了一个生成器(原理图),它会自动生成**所有样板代码**,以帮助我们避免所有这些,并使开发人员的体验更加简单。[在此处](https://docs.nestjs.com/recipes/crud-generator)阅读有关此功能的更多信息。 #### 启动并运行[#](#getting-up-and-running) 完全定义上述控制器后,Nest 仍然不知道它的`CatsController`存在,因此不会创建此类的实例。 控制器总是属于一个模块,这就是我们在装饰器中包含`controllers`数组的原因。`@Module()`由于我们还没有定义除 root 之外的任何其他模块`AppModule`,我们将使用它来介绍`CatsController`: ```typescript /* app.module.ts */ import { Module } from '@nestjs/common'; import { CatsController } from './cats/cats.controller'; @Module({ controllers: [CatsController], }) export class AppModule {} ``` 我们使用装饰器将元数据附加到模块类`@Module()`,Nest 现在可以轻松地反映必须安装哪些控制器。 #### 特定于库的方法[#](#library-specific-approach) 到目前为止,我们已经讨论了 Nest 处理响应的标准方法。操作响应的第二种方法是使用特定于库的[响应对象](https://expressjs.com/en/api.html#res)。为了注入特定的响应对象,我们需要使用`@Res()`装饰器。为了显示差异,让我们将其重写`CatsController`为以下内容: ```typescript /* cats.controller.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`,如下所示: ```typescript @Get() findAll(@Res({ passthrough: true }) res: Response) { res.status(HttpStatus.OK); return []; } ``` 现在您可以与本机响应对象进行交互(例如,根据特定条件设置 cookie 或标头),但将其余部分留给框架。