Nest —— Controllers

2,907 阅读10分钟

前言

       之前匆匆写了一篇关于Nest官文的翻译,然而写的时候比较着急,所以很多地方的翻译比较马虎,甚至直接丢进翻译器在丢出来....(有的时候我会被丢进翻译器之后被丢出来的译文吓到,原因是...译文比自己翻译的好/(ㄒoㄒ)/~~)但还是有很多部分是翻译器解决不了的,语句通顺固然优雅锦上添花,但一点小的错误却是致命的。
       现在手头比较悠闲,也打算重新修改一份比较优雅的中文文档。有人会问花这么多时间写这个东西,是不是真的有用。百度上面也有一些关于Nest的文档,完全也不会有人来看你写的翻译。我的感受是,可能这就是我的学习方式吧。其实平时阅读文档,大多数情况下都是脑子说会了,手说不会。一边翻译英文文档,一遍理解框架的含义,还有助于提高阅读英文文档的能力。手敲过一遍和眼睛看过一遍真的不太一样,而且这种方式会增加使用时的自信。之后打算将每一章节分开书写,最后再通过链接汇总到一篇Nest妲己大记中去。就算没有人看,自己想要翻阅文档的时候,也可以拿出来看看,还能改改。这是一种乐趣,就好像养成游戏一样。



正文 Controllers

controllers(控制器)负责处理请求并返回响应数据给客户端

  • controller的目的是为应用 接收特定的路由请求。 路由策略控制哪一个controller接受哪些请求。通常每个controller拥有多个路由,并且不同的路由可以执行不的动作。
  • 创建一个基础的controller可以使用 class(类) 和 decorators(装饰器)。装饰器将类和需要的 metadata(元数据)结合,Nest以此来创建一个路由映射(将请求和对应的controller联系起来)。


Routing 路由

如下例,定义一个基础的controller需要使用装饰器 @Controller()。 指定一个可配置的路由路径前缀 cats 将一系列相关的路由组合起来,最大程度减少重复代码。
🌰 我们将一组路由分组,使用decorator @Controller() 在里面指定路由的前缀 customers 用来管理所有 /customers 请求下的交互,如此便不必在整个文件中重复得去书写相同的路径。

// cats.controller.ts
imoprt { Controller, Get } from '@nestjs/common'

@controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return '这个动作会返回所有的cats'
  }
}

Hints
可以使用CLI创建controller nest g controller cats


findAll() 方法之前添加HTTP请求方法的装饰器 @Get() 用来告诉Nest为HTTP请求特定的端点创建一个处理程序。这个端点对应了 HTTP请求方法 (这里是 GET)和 路由的路径。路径由 控制器中声明的前缀在请求装饰器中指定的路径 组合组成。 我们为每个路由声明了 cats 的前缀,若没有在装饰器中添加任何信息,Nest会将 GET /cats 请求映射到这个处理函数中。
🌰 路由前缀为 customers 加上装饰器 @Get('profile') 会将请求映射到路由 GET /customers/profile

在上例中,当一个 GET 请求到这个端点,Nest将这个请求路由到用户定义的 findAll() 函数中。注意这个方法的名字是随意的,我们很显然必须为路由绑定一个函数,但是这个函数的名字Nest并不在意~

这个方法只会返回一个200的状态码和对应的响应数据(上例中只是一个字符串)。解释这个问题,需要先介绍Nest,它使用两种不同的选项来处理响应。

Standard <标准的>(推荐) 使用内置的方法,当一个请求的处理函数返回了一个js对象或数组,将自动序列化为JSON格式。当返回的是一个基础js类型,Nest会直接将值返回,不会进行序列化。这响应的处理变得简单:只返回值,Nest会关心剩下的处理
Library-specific <指定库> 我们可以使用指定库(例如Express)的响应对象,通过在处理函数签名中使用装饰器@Res()(例如findAll(@Res() response))注入。如此你就拥有能力(和责任)来使用原生响应处理函数所暴露的对象。例如:使用Express,你可以用response.status(200).send()构建响应数据

Warning
不可以同时使用以上两种方法。Nest会检查你是否使用@Res()或者@Next(),这表明了你选择了Library-specific选项。如果同时使用了两种方法,在当前路由中Standard会自动禁用,也不会起效果哦~



Request Object 请求对象

处理函数通常需要接受来自客户端的请求细节。Nest提供了底层请求对象的接口(默认是Express)。我们可以通过在函数签名中添加 @Req() 装饰器来获取请求对象

// cats.controller.ts
import { Controller, Get, Req } from '@nestjs/common'
import { Request } from 'express'

@Controller('cats')
export class CatsController {
  @Get('cats')
  findAll(@Req() request: Request): string {
    return '这个动作返回所有的cats'
  }
}

该请求对象代表了HTTP请求,并且拥有查询字符串,HTTP请求头,和请求体等属性。大多数情况下不需要手动获取这些属性,而是直接使用装饰器比如 @Body() 或者 @Query() 。下面是提供的装饰器以及他们所代表的相应的对象

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

* 为了兼容底层HTTP类型,Nest同时提供了 @Res()@Response() 装饰器, @Res() 只是 @Response() 的别名。两者都暴露了底层响应对象的接口,使用的时候需要引入这些底层库的类型。当你同时注入二者在同一个处理函数中,表示你为该函数选择了Library-specific模式,你就有义务处理响应的对象。你必须在响应对象上调用一些方法来发出响应(例如:res.json())或者@res.send(),否则HTTP服务将会处于挂起状态。

Hints
学习创建自定义装饰器



Resource 资源

前面定义了一个端点来获取cats的资源(GET 路由),我们也需要提供一个端点来创建新的资源。

// 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'
  }
}

Nest为剩余的HTTP请求标准提供了相同风格的装饰器—— @Put(), @Delete(), @Patch(), @Options(), @Head(), @All()



Route wildcard 路由通配符

也支持基于路由的样式,*作为通配符可以匹配到所有字符组合。

@Get('ab*cd')
findAll() {
  return '这个路由使用了通配符'
}

'ab*cd'会匹配abcdab_cdabcde等等。字符?,+,*,()以及对应正则表达式的自己也可以在路由中使用。连字符-和点.则是字符串路径的字面意思。



Status code 状态码

响应状态码默认是200,除了POST请求是201。可以通过在处理函数层添加装饰器 @HttpCode() 去改变这种习惯。

@Post()
@HttpCode(204)
create() {
  return '这个动作会添加一个新的cat'
}

Hints
HttpCode@nestjs/common包引入

通常状态码不是静态的,会依赖于各种不同的情况。既然这样,你可以使用Library-specific的响应对象。



Headers 请求头

指定自定义的请求头,可以使用装饰器 @Header()或者Library-specific的响应对象(res.header()

@Post()
@Header('Cache-Control', 'none')
create() {
  return '这个动作会添加一个新的cat'
}

Hints
import { Header } from '@nestjs/common



Redirection 重定向

将响应重定向到特定的url,可以使用 @Redirect()或者Library-specific的响应对象(res.redirect()

@Redirect()有一个必传入参url,和可选入参statusCodestatusCode不传默认是302(Found)

@Get()
@Redirect('http://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/' }  
  }
}


Route parameters 路由参数

当需要接受请求路径中一部分作为动态数据时,静态路径做不到。(例如GET /cats/1获取到id为1的cat)。定义带有参数的路由,只需要添加参数标识就可以在请求url中捕获动态的值。然后就可以在函数签名中使用@Param()访问到路由参数。

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `这个动作会返回id为${params.id}的cat`
}

我们可以接受一个id作为参数来获取到params.id

@Get('id')
findOne(@Param('id') id): string {
  return `这个动作会返回id为${id}的cat`
}


Asynchronicity 异步

数据的获取是异步的,Nest支持async函数 每个异步的函数返回一个Promise,这意味着你可以返回一个延迟的值,Nest可以自己处理。

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

上面的代码完全有效,Nest路由处理函数甚至可以返回RxJs(可观测的数据流)。Nest将自动订阅资源,并流完成时获取最后一次发出的值。

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

两种方法都可以,根据需要来使用吧~



Request payloads 请求负载(数据)

前面的栗子,POST路由处理函数没有接受任何来自客户端的参数,现在来添加装饰器 @Body() 但是首先我们需要定义 DTO(Data Transfer Object)。DTO对象定义了数据将以何种形式通过网络发送,可以通过ts的interface来定义DTO,或者class类。有趣的是我们推荐用class类。类是ES6标准的一部分,他们在编译之后会保存为一个真实的实体。然而ts的接口在编译期间会删除,Nest运行时并获取不到他们。这很重要,因为它为像Pipes管道这样的特性在运行时访问变量的元类型,提供了更多的可能性。

创建CreateDto

// create-cast.dto.ts
export class CreateDto {
  readonly name: string;
  readonly age: number;
  readonly bread: string;
}

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

@Post()
async create(@Body() createCatDto: CreateCatDto) {
    return '这个动作会添加一个新的cat'
}


完整案例

// cats.controlller.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 `这个动作会返回所有的cat(limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `这个动作会返回一个id为${id}的cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `这个动作为更新id为${id}的cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `这个动作会删除id为${id}的cat`;
  }
}


运行

控制器定义好之后,Nest并不知道这个控制器的存在,并且也不会创建实例。 控制器通常属于一个模块Module,需要以数组的形式传进装饰器@Module()

import { Module } from 'nestjs/common';
import { CatsController } from './cats/cats.controller';

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


附录:Library-specific的用法

import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express'

@Controller('cats')
export class CatsController {
  @Post()
  craete(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }
  
  @Get()
  findAll(@Res() res: Response) {
    res.status(HttpStatus.OK).json([]);
  }
}

Hints
尽可能使用Nest的Standard标准方法





后记

原文地址: docs.nestjs.com/controllers


关于本文

  • 文章非复制黏贴,经浏览文档,以自己的理解进行,代码测试,手打书写。本篇为翻译+意译。
  • 用作记录自己曾经学习、思考过的问题的一种笔记。
  • 用作前端技术交流分享。
  • 阅读本文时欢迎随时质疑本文的准确性,将错误的地方告诉我。本人会积极修改,避免文章对读者的误导。

关于我

  • 是一只有梦想的肥柴。
  • 觉得算法、数据结构、函数式编程、js底层原理等十分有趣的小前端。
  • 志同道合的朋友请关注我,一起交流技术,在前端之路上共同成长。
  • 如对本人有任何意见建议尽管告诉我哦~ 初为肥柴,请多多关照~
  • 前端路漫漫,技术学不完。今天也是美(diao)好(fa)的一天( 跪了...orz