创建项目基本内容
创建项目时将根据项目名称创建目录,安装 node 模块和一些其他样板文件,将创建 src/ 目录并填充几个核心文件
核心文件的简要概述:
| app.controller.ts | 具有单一路由的基本控制器 |
|---|---|
| app.controller.spec.ts | 控制器的单元测试 |
| app.module.ts | 应用的根模块 |
| app.service.ts | 具有单一方法的基本服务 |
| main.ts | 使用核心函数 NestFactory 创建 Nest 应用实例的应用入口文件 |
main.ts 包含一个异步函数,它将引导应用
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
prettier问题
换行处爆红
解决:在.eslintrc.js文件中添加下面内容,关闭换行处警告
"prettier/prettier": [
"error",
{
"endOfLine": false
}
]
Controllers(控制器)
控制器负责处理传入请求并向客户端返回响应
控制器的目的是接收请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器都有不止一条路由,不同的路由可以执行不同的操作
为了创建基本控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)
提示:为了快速创建内置 validation 的 CRUD 控制器,可以使用 CLI 的 增删改查生成器:nest g resource [name]
路由(Routing)
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('breed')
findAll(): string {
return 'This action returns all cats';
}
}
使用 @Controller() 装饰器来修饰路由控制器,这个装饰器接收一个参数作为请求路径,如传入cats 则客户端访问时需要在请求地址后面加上 /cats 路径就能请求到这个路由上
也可以在方法上添加请求方法装饰器,如例子中的 @Get('breed') 表示请求到 /cats/breed,并且请求方式为 get,触发的处理方法是 findAll
提示:要使用 CLI 创建控制器,只需执行nest g controller [name] 命令即可
关于响应,nest 中使用两种不同的选项来操作响应:
| 标准(推荐) | 使用此内置方法,当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。然而,当它返回 JavaScript 基本类型(例如 string、 number、 boolean)时,Nest 将仅发送该值,而不尝试对其进行序列化。这使得响应处理变得简单:只需返回值,Nest 就会处理剩下的事情。 此外,默认情况下,响应的状态代码始终为 200,但使用 201 的 POST 请求除外。可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为 |
|---|---|
| 特定库 | 可以使用特定于库的(例如 Express) 响应对象,它可以使用方法处理程序签名中的 @Res() 装饰器注入(例如 findAll(@Res() response))。通过这种方法,你可以使用该对象公开的原生响应处理方法。例如,使用 Express,你可以使用 response.status(200).send() 等代码构建响应 |
注意:如果想同时使用上述两种响应(例如,通过注入响应对象来仅设置 cookies/headers,但仍将其余部分留给框架),必须在 @Res({ passthrough: true }) 装饰器中将 passthrough 选项设置为 true
请求对象(req)
以通过将 @Req() 装饰器添加到处理程序的签名来指示 Nest 注入它来访问请求对象
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 软件包(ts版默认安装)
请求对象表示 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() 的别名。两者都直接暴露底层原生平台 response 对象接口。使用它们时,你还应该导入底层库(例如 @types/express)的类型以充分利用它们
注意: 当在方法中注入 @Res() 或 @Response() 时,此时会将 Nest 置于该处理程序的特定库模式,并且你将负责管理响应。这样做时,必须通过调用 response 对象(例如,res.json(...) 或 res.send(...))来触发某种响应,否则 HTTP 服务器将挂起
import {
Controller,
Get,
HostParam,
Ip,
Req,
Res,
Session,
} from '@nestjs/common';
import { AppService } from './app.service';
@Controller('cats')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('breed')
getHello(
@Req() req: Request,
@Res() res: any,
@Session() session: any,
@Ip() ip: string,
@HostParam() host: string,
): string {
console.log('请求头内容:', req);
console.log('响应头内容:', res);
console.log('session', session);
console.log('IP:', ip);
console.log('host:', host);
// return this.appService.getHello();
return res.send(this.appService.getHello());
}
}
资源(Resources)
之前定义了一个接口来获取 cats 资源(GET 路由)。通常还希望提供一个创建新记录的接口。为此,创建 POST 处理程序:
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等。字符?、+、*和()可以在路由路径中使用,并且是它们对应的正则表达式的子集。连字符 -)和点 .)由基于字符串的路径逐字解释
警告:仅 express 支持路由中间的通配符
状态码(Status code)
@HttpCode(...) :修改响应状态码
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
提示:从 @nestjs/common 包中导入 HttpCode
通常,状态代码不是静态的,而是取决于各种因素。在这种情况下,可以使用指定库的响应(使用 @Res() 注入)对象(或者,如果发生错误,则抛出异常)
响应标头(Headers)
修改响应标头有两种方式:
@Header():自定义响应标头- 指定库的响应对象(调用
res.header())
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
提示:从 @nestjs/common 包中导入 HttpCode
重定向(Redirection)
重定向有两种方式:
- @Redirect() 装饰器
-
- 有两个可选参数:
-
-
- url
- statusCode
- 如果省略,statusCode 的默认值为 302 (Found)
-
- 指定库的响应对象(直接调用
res.redirect())
@Get()
@Redirect('https://nest.nodejs.cn', 301)
提示:有时可能想要动态确定 HTTP 状态代码或重定向 URL。通过返回遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象来完成此操作
返回值将覆盖传递给 @Redirect() 装饰器的任何参数。例如:
@Get('docs')
@Redirect('https://nest.nodejs.cn', 302)
getDocs(@Query('version') version) {
if (version && version === '5') {
return { url: 'https://nest.nodejs.cn/v5/' };
}
}
路由参数
当需要接受动态数据作为请求的一部分时(例如,GET /cats/1 获取 ID 为 1 的 cat),具有静态路径的路由将不起作用。为了定义带参数的路由,可以在路由的路径中添加路由参数标记,以捕获请求 URL 中该位置的动态值。下面 @Get() 装饰器示例中的路由参数 token 演示了这种用法。可以使用 @Param() 装饰器访问以这种方式声明的路由参数,应将其添加到方法签名中
提示:带参数的路由应在任何静态路径之后声明。这可以防止参数化路径拦截发往静态路径的流量
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
@Param() 用于修饰方法参数(上例中的 params),并使路由参数可用作方法体内该修饰方法参数的属性。如上面的代码所示,可以通过引用 params.id 来访问 id 参数。也可以传入一个特定的参数 token 给装饰器,然后在方法体中直接通过名称引用路由参数
提示:从 @nestjs/common 包中导入 Param
import { Controller, Get, Param, Query } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get('find')
findAll(): string {
return this.catsService.findAll();
}
@Get('findById')
findById(@Query() query: { id: number }) {
console.log('当前的参数:', query);
return this.catsService.findOne(query.id);
}
/**
*需要放到最后,否则会顶掉 findById 方法
**/
@Get(':id')
findOne(@Param() params: { id: number }): string {
return this.catsService.findOne(params.id);
}
}
子域路由
警告:由于 Fastify 缺乏对嵌套路由的支持,因此在使用子域路由时,应使用(默认)Express 适配器
@Controller 装饰器可以采用 host 选项来要求传入请求的 HTTP 主机匹配某个特定值
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
与路由 path 类似,hosts 选项可以使用标记来捕获主机名中该位置的动态值。下面 @Controller() 装饰器示例中的主机参数 token 演示了这种用法。可以使用 @HostParam() 装饰器访问以这种方式声明的主机参数,应将其添加到方法签名中
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
作用域(Scopes)
在 Nest 中几乎所有内容都是在传入请求之间共享的,Node.js 不遵循请求/响应多线程无状态模型,在该模型中每个请求都由单独的线程处理。因此,使用单例实例对于 Node.js 应用来说是完全安全的
异步性(Asynchronicity)
每个异步函数都必须返回 Promise。这意味着可以返回一个 Nest 能够自行解析的延迟值。例子:
@Get()
async findAll(): Promise<any[]> {
return [];
}
上面的代码是完全有效的。此外,Nest 路由处理程序更强大,因为它能够返回 RxJS 可观察流。Nest 将自动订阅下面的源并获取最后触发的值(一旦流完成)
@Get()
findAll(): Observable<any[]> {
return of([]);
}
请求负载/请求体(Request payloads)
@Body() :接收请求体参数
首先(如果使用 TypeScript),需要确定 DTO(数据传输对象)架构。DTO 是定义数据如何通过网络发送的对象。可以通过使用 TypeScript 接口或简单的类来确定 DTO 模式。建议在这里使用类,类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中作为真实实体保存。另一方面,由于 TypeScript 接口在转换过程中被删除,Nest 无法在运行时引用它们。这很重要,因为管道等功能在运行时可以访问变量的元类型时可以提供额外的可能性
创建 CreateCatDto 类:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
在 CatsController 中使用新创建的 DTO:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
提示:ValidationPipe 管道 可以过滤掉方法处理程序不应接收的属性。在这种情况下,可以将可接受的属性列入白名单,并且白名单中未包含的任何属性都会自动从生成的对象中删除。在 CreateCatDto 示例中,白名单是 name、age 和 breed 属性
完整示例
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`;
}
}
启动并运行
在完全定义了上述 controller 的情况下,Nest 仍然不知道 CatsController 存在,因此不会创建此类的实例
controller 始终属于一个 module,这就是在 @Module() 装饰器中包含 controllers 数组的原因。由于还没有定义除根 AppModule 之外的任何其他 module,使用它来引入 CatsController:
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
使用 @Module() 装饰器将元数据附加到模块类,Nest 现在可以轻松反映必须安装哪些 controller
处理响应
处理响应的第二种方法是使用库特定的响应对象(如 express 中的响应)。使用 @Res() 装饰器注入特定的响应对象:
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 [];
}
Provider(提供器)
Provider是 Nest 中的一个基本概念。许多基本的 Nest 类可以被视为提供器 - 服务、存储库、工厂、助手等等。Provider 的主要思想是它可以作为依赖注入;这意味着对象之间可以创建各种关系,并且 "接线" 这些对象的功能很大程度上可以委托给 Nest 运行时系统
Controller 应该处理 HTTP 请求并将更复杂的任务委托给 Provider。Provider 是在 module 中声明为 Provider 的纯 JavaScript 类
由于 Nest 能够以更加面向对象的方式设计和组织依赖,因此强烈建议遵循 SOLID 原则
服务(Services)
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
export interface Cat {
name: string;
age: number;
breed: string;
}
快捷生成:nest g service xxxxxx
@Injectable() 装饰器附加元数据,该元数据声明 CatsService 是可由 Nest IoC 容器管理的类
在 CatsController 中使用:
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
CatsService 通过类构造函数注入。请注意 private 语法的使用,这种简写允许立即在同一位置声明和初始化 catsService 成员
依赖注入(构造方法注入)
在 Nest 中,由于 TypeScript 的功能,管理依赖非常容易,因为它们只是按类型解析。在下面的示例中,Nest 将通过创建并返回 CatsService 的实例来解析 catsService(或者,在单例的正常情况下,如果已在其他地方请求过,则返回现有实例)。此依赖已解析并传递给控制器的构造函数(或分配给指示的属性):
constructor(private catsService: CatsService) {}
作用域
提供程序通常具有与应用生命周期同步的生命周期(scope)。启动应用时,必须解析每个依赖,因此必须实例化每个提供程序。同样,当应用关闭时,每个 Provider 都将被销毁。但是,也有一些方法可以使提供程序生命周期限定在请求范围内
自定义Provider
Nest 有一个内置的控制反转 ("IoC") 容器,可以解决 Provider 之间的关系。此功能是上述依赖注入功能的基础,但实际上比目前所描述的功能强大得多。有几种定义Provider的方法:可以使用普通值、类以及异步或同步工厂
可选Provider
有时,有些依赖可能有不一定要解决。例如,类可能依赖于配置对象,但如果没有传递任何内容,则应该使用默认值。在这种情况下,依赖变为可选,这样即使缺少配置提供的情况下程序也不会导致错误
要指示 Provider 是可选的,在构造函数的签名中使用 @Optional() 装饰器
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
属性注入
到目前为止,使用的技术称为基于构造函数的注入,因为 Provider 是通过构造函数方法注入的。在某些非常特殊的情况下,基于属性的注入可能很有用。例如,如果顶层类依赖于一个或多个 Provider,则通过从构造函数在子类中调用 super() 将它们一路向上传递可能会是代码非常冗余。为了避免这种情况,可以在属性级别使用 @Inject() 装饰器
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
警告:如果类没有扩展另一个类,那么应该始终更喜欢使用基于构造函数的注入
注册Provider
需要向 Nest 注册该服务,以便它可以执行注入,通过编辑模块文件(app.module.ts)并将服务添加到 @Module() 装饰器的 providers 数组来完成此操作
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
目录结构:
Modules(模块)
Module 是用 @Module() 装饰器注释的类。 @Module() 装饰器提供 Nest 用于组织应用结构的元数据
- 每个应用至少有一个模块,即根模块
- 根模块是 Nest 用于构建应用图的起点
- Nest 用根模块解析 module 和 provider 关系及依赖的内部数据结构
- 强烈建议将 module 作为组织组件的有效方式
- 对于大多数应用来说,最终的架构将采用多个模块,每个模块封装一组密切相关的功能
@Module() 装饰器采用单个对象,参数如下:
| providers | 将由 Nest 注入器实例化并且至少可以在该模块中共享的 provider | 引入 provider |
|---|---|---|
| controllers | 此模块中定义的必须实例化的 controller | 引入 controller |
| imports | 导出此模块所需的 provider 的导入模块列表 | 导入 Moudle |
| exports | 这个模块提供的 providers 的子集应该在导入这个模块的其他模块中可用。可以使用提供器本身或仅使用其 token (provide值) | 引入 provider 后,可以用过 exports 暴露此 provider 给其他的 Moudle 使用 |
功能模块
将相关的 Controller 和 Service 放到同一个 Module 中
CatsController 和 CatsService 属于同一个应用域。由于它们密切相关,移动到一个 module 中有助于管理复杂性并使用 SOLID 原则进行开发,尤其是随着应用/团队规模的增长
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
提示:要使用 CLI 创建模块,只需执行 nest g module xxxxxx 命令即可
上面在 cats.module.ts 文件中定义了 CatsModule,并将与该模块相关的所有内容都移动到了 cats 目录下。此时需要做的最后一件事是将此模块导入根模块(AppModule,在 app.module.ts 文件中定义)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
目录结构:
共享模块
将其他模块的 Service 注入的其他的 Module 中使用
在 Nest 中,默认情况下模块是单例,因此可以在多个 module 之间共享任何 provider 的同一实例
每个 module 自动成为共享模块。一旦创建,它就可以被任何 module 重用。假设想要在其他几个 module 之间共享 CatsService 的一个实例。为此,首先需要将 CatsService 这个 provider 添加到 module 的 exports 数组中来导出它,如下所示:
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
现在,任何导入 CatsModule 的模块都可以访问 CatsService,并将与导入它的所有其他模块共享同一个实例
示例:注入其他 service
cats 模块导出 CatsService 给 dogs 模块使用
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
dogs 模块中先引入 cats 模块
import { Module } from '@nestjs/common';
import { DogsService } from './dogs.service';
import { DogsController } from './dogs.controller';
import { CatsModule } from 'src/cats/cats.module';
@Module({
controllers: [DogsController],
providers: [DogsService],
imports: [CatsModule]
})
export class DogsModule {}
然后,在 dogs 的 controller 中使用,有两种方法注入这个 service
- 构造方法注入
- 属性注入
import { Controller, Get, Inject } from '@nestjs/common';
import { CatsService } from 'src/cats/cats.service';
@Controller('dog')
export class DogsController {
// 在构造方法中注入这个 catsService
// constructor(private readonly catsService: CatsService) {}
// 通过属性注入这个 catsService
@Inject()
private readonly catsService: CatsService;
@Get('cat')
findAll(): string {
return this.catsService.helloCat();
}
}
模块重新导出
module 可以导出其内部的 provider。此外,他们可以重新导出他们导入的 module。在下面的示例中,CommonModule 既被导入到 CoreModule 中,又被从 CoreModule 中导出,从而使其可用于导入该module 的其他 module
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
依赖注入
模块类也可以注入 provider(例如,出于配置目的):
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
但是,由于循环依赖,模块类本身不能作为 provider 注入
全局module
当想要提供一组开箱即用的 provider(例如辅助程序、数据库连接等)时,可以使用 @Global() 装饰器使模块全局化
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
@Global() 装饰器使模块具有全局作用域。全局模块应该只注册一次,通常由根模块或核心模块注册。在上面的示例中,CatsService provider 将无处不在,注入服务的 module 不需要再将 CatsModule 导入到当前 module 的 imports 数组中
动态模块
Nest 模块系统包含一个称为动态模块的强大功能。此功能能够轻松创建可自定义的 module ,这些 module 可以动态注册和配置 provider
以下是 DatabaseModule 的动态模块定义示例:
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
exports: [Connection], // 这里导出静态声明
})
export class DatabaseModule {
// 这里定义一个静态方法(返回值声明为一个动态模块)
// 这里意味着导入这个动态模块的时候可以调用这个静态方法往这个动态模块添加参数
static forRoot(entities = [], options?): DynamicModule {
// 这里内部创建了一个 providers,并且这个方法接收了
// 在外部调用模块的静态方法时传入的参数
const providers = createDatabaseProviders(options, entities);
// 返回一个对象,这个对象严格遵循装饰器 @Module 的参数,
// 也就是可以有 providers,controllers,imports,exports这四个可选的属性;
// 注意,这个返回对象不会覆盖掉元数据中的内容,而是会合并到元数据中的内容;
// module,providers 和 exports 可以返回其他内容
return {
module: DatabaseModule, // 返回模块自己本身(可以返回其他 module)
providers: providers, // 返回调用 forRoot 时候生成的 providers
exports: providers, // 允许这个 providers 被其他 module 引用
};
}
}
提示:forRoot() 方法可以同步或异步(即通过 Promise)返回动态模块
这个 module 默认定义了 Connection provider(在 @Module() 装饰器元数据中),但另外——根据传递给forRoot()方法的实体和选项对象——公开了一个 provider 集合,例如存储库。注意,动态模块返回的是属性扩展(不是覆盖)—— @Module() 装饰器中定义的基本模块元数据。这就是从 module 中导出静态声明的 Connection provider 和动态生成的存储库 provider 的方式
如果要在全局作用域内注册动态模块,请将 global 属性设置为 true
{
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
}
DatabaseModule 可以通过以下方式导入和配置:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
// 这里就是调用上面定义的动态模块的forRoot静态方法,往这个 module 中传入参数
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
如果想反过来重新导出一个动态模块,可以省略 exports 数组中的 forRoot() 方法调用:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [DatabaseModule.forRoot([User])], // 传入参数,初始化完成
exports: [DatabaseModule], // 初始化完成后再导出去给其他 module 使用
})
export class AppModule {}
中间件(Middleware)
中间件是在路由处理程序之前调用的函数。中间件函数可以访问 request 和 response 对象,以及应用请求-响应周期中的 next() 中间件函数。下一个中间件函数通常由名为 next 的变量表示
默认情况下,Nest 中间件等同于 express 中间件,官方 express 文档中的以下描述描述了中间件的功能:
Middleware functions can perform the following tasks:
- 执行任何代码
- 更改请求和响应对象
- 结束请求-响应循环
- 调用堆栈中的下一个中间件函数
- if the current middleware function does not end the request-response cycle, it must call next() to pass control to the next middleware function. Otherwise, the request will be left hanging
可以在方法中或在具有 @Injectable() 装饰器的类中实现自定义 Nest 中间件。类应实现 NestMiddleware 接口,功能无特殊要求
警告:Express 和 fastify 以不同方式处理中间件并提供不同的方法签名
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
依赖注入
Nest 中间件完全支持依赖注入。就像 providers 和 controllers 一样,它们能够注入同一模块中可用的依赖。像往常一样,这是通过 constructor 完成的
使用中间件
@Module() 装饰器中没有中间件的位置,需要使用 module 类的 configure() 方法设置它们。包含中间件的模块必须实现 NestModule 接口。例如下面中在 AppModule 级别设置 LoggerMiddleware
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
在上面的示例中,为之前在 CatsController 中定义的 /cats 路由处理程序设置了 LoggerMiddleware。还可以通过在配置中间件时将包含路由 path 和请求 method 的对象传递给 forRoutes() 方法,进一步将中间件限制为特定的请求方法。在下面的示例中,请注意此处导入了 RequestMethod 枚举以引用所需的请求方法类型
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
提示:可以使用 async/await 使 configure() 方法异步(例如,可以在 configure() 方法主体内 await 完成异步操作)
警告:当使用 express 适配器时,NestJS 应用将默认从 body-parser 包中注册 json 和 urlencoded。这意味着如果想通过 Middleware Consumer (中间件消费者)自定义该中间件,则需要在使用 NestFactory.create() 创建应用时通过将 bodyParser 标志设置为 false 来关闭全局中间件
路由通配符
也支持基于模式的路由。例如,星号用作通配符,将匹配任意字符组合:
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
ab*cd'路由路径将匹配 abcd、ab_cd、abecd 等。字符?、+、*和()可以在路由路径中使用,并且是它们对应的正则表达式的子集。连字符-和点.由基于字符串的路径逐字解释
警告:fastify 包使用最新版本的 path-to-regexp 包,不再支持通配符星号 *。相反,必须使用参数(例如,(.*)、:splat*)
中间件消费者(Middleware consumer)
MiddlewareConsumer 是一个辅助类。它提供了几种内置的方法来管理中间件。所有这些都可以简单地链接在 流畅的风格 中。forRoutes() 方法可以接受单个字符串、多个字符串、一个 RouteInfo 对象、一个控制器类甚至多个控制器类。在大多数情况下,可能只会传递以逗号分隔的 controller 列表。以下是单个 controller 的示例:
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}
提示:apply() 方法可以采用单个中间件或多个参数来指定多个中间件
排除路由
有时希望排除某些路由应用中间件。可以使用 exclude() 方法轻松排除某些路由。此方法可以采用单个字符串、多个字符串或 RouteInfo 对象标识要排除的路由,如下所示:
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
提示:exclude() 方法使用 path-to-regexp 包支持通配符参数
上面的示例中,LoggerMiddleware 将绑定到 CatsController 内部定义的所有路由,除了传递给 exclude() 方法的三个路由之外
函数式中间件(Functional middleware)
对于没有成员,没有额外的方法,也没有依赖的类,可以用一个简单的函数而不是一个类来定义。这种类型的中间件称为函数式中间件
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};
在 AppModule 中使用:
consumer
.apply(logger)
.forRoutes(CatsController);
多个中间件
为了绑定顺序执行的多个中间件,只需在 apply() 方法中提供一个逗号分隔的列表:
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
全局中间件
如果想一次将中间件绑定到每个已注册的路由,可以使用 INestApplication 实例提供的 use() 方法:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
提示:无法访问全局中间件中的 DI 容器。使用 app.use() 时可以使用函数式中间件。或者,可以使用类中间件并在 AppModule(或任何其他 module )中将其与 .forRoutes('*') 一起使用
异常过滤器(Exception filters)
Nest 带有一个内置的异常层,负责处理应用中所有未处理的异常。当应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应
开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理 HttpException 类型(及其子类)的异常。当异常无法识别时(既不是 HttpException 也不是继承自 HttpException 的类),内置异常过滤器会生成以下默认 JSON 响应:
{
"statusCode": 500,
"message": "Internal server error"
}
提示:全局异常过滤器部分支持 http-errors 库。基本上,任何包含 statusCode 和 message 属性的抛出异常都将被正确填充并作为响应发回(而不是用于无法识别的异常的默认 InternalServerErrorException)
抛出标准异常
Nest 提供了一个内置的 HttpException 类,从 @nestjs/common 包中暴露出来。对于典型的基于 HTTP REST/GraphQL API 的应用,最佳做法是在发生某些错误情况时发送标准 HTTP 响应对象。
例如,在 CatsController 中,有一个 findAll() 方法(一个 GET 路由处理程序)。假设此路由处理程序出于某种原因抛出异常:
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
客户端调用此接口时,响应如下所示:
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException 构造函数需要传入两个必需的参数和一个可选参数:
- response:定义 JSON 响应主体。它可以是 string 或 object
-
- 默认情况下,JSON 响应主体包含两个属性:
-
-
- statusCode:默认为 status 参数中提供的 HTTP 状态代码
- message:基于 status 的 HTTP 错误的简短描述
-
-
- 要仅覆盖 JSON 响应正文的消息部分,请在 response 参数中提供一个字符串。要覆盖整个 JSON 响应主体,请在 response 参数中传递一个对象。Nest 将序列化该对象并将其作为 JSON 响应主体返回
- status:参数定义了 HTTP 状态代码
-
- 应该是有效的 HTTP 状态代码。最佳做法是使用从 @nestjs/common 导入的 HttpStatus 枚举
- options(可选) :可用于提供错误的 cause(原因) 。此 cause 对象未序列化到响应对象中,但它可用于记录目的,提供有关导致 HttpException 被抛出的内部错误的有价值信息
一个覆盖整个响应主体并提供错误原因的示例:
@Get()
async findAll() {
try {
await this.service.findAll()
} catch (error) {
throw new HttpException({
status: HttpStatus.FORBIDDEN, // 状态码
error: 'This is a custom message', // 错误的信息
}, HttpStatus.FORBIDDEN, {
cause: error // 错误原因,这里的内容不会返回给客户端
});
}
}
{
"status": 403,
"error": "This is a custom message"
}
列举一部分常用的 HttpStatus 中定义的枚举异常:
| OK | 200 |
|---|---|
| CREATED | 201 |
| ACCEPTED | 202 |
| NO_CONTENT | 204 |
| MOVED_PERMANENTLY | 301 |
| FOUND | 302 |
| NOT_MODIFIED | 304 |
| BAD_REQUEST | 400 |
| UNAUTHORIZED | 401 |
| NOT_FOUND | 404 |
| METHOD_NOT_ALLOWED | 405 |
| INTERNAL_SERVER_ERROR | 500 |
| BAD_GATEWAY | 502 |
| SERVICE_UNAVAILABLE | 503 |
自定义异常
在许多情况下,不需要编写自定义异常,并且可以使用内置的 Nest HTTP 异常。如果确实需要创建自定义异常,那么最好创建异常层次结构,其中自定义异常继承自 HttpException 基类。通过这种方法,Nest 将识别自定义的异常,并自动处理错误响应。实现这样一个自定义异常:
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
由于 ForbiddenException 扩展了基 HttpException,它将与内置异常处理程序无缝协作,因此可以在 findAll()方法中使用它:
@Get()
async findAll() {
throw new ForbiddenException();
}
内置 HTTP 异常
Nest 提供了一组继承自基 HttpException 的标准异常。这些是从 @nestjs/common 包中公开的,代表了许多最常见的 HTTP 异常:
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- HttpVersionNotSupportedException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableEntityException
- InternalServerErrorException
- NotImplementedException
- ImATeapotException
- MethodNotAllowedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
- PreconditionFailedException
所有内置异常也可以使用 options 参数提供错误 cause 和错误描述:throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
异常过滤器(Exception filters)
虽然基本(内置)异常过滤器可以自动处理许多情况,但你可能希望完全控制异常层。例如,希望根据某些动态因素添加日志记录或使用不同的 JSON 模式。异常过滤器正是为此目的而设计的。它们可以控制准确的控制流和发送回客户端的响应内容
创建一个异常过滤器,负责捕获作为 HttpException 类实例的异常,并为它们实现自定义响应逻辑。为此,需要访问底层平台 Request 和 Response 对象。访问 Request 对象,以便提取原始 url 并将其包含在日志信息中;使用 Response 对象直接控制发送的响应,使用 response.json() 方法
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
提示:所有异常过滤器都应实现通用 ExceptionFilter 接口。这要求你提供 catch(exception: T, host: ArgumentsHost) 方法及其指示的签名。T 表示异常的类型
警告:如果使用 @nestjs/platform-fastify,则可以使用 response.send() 而不是 response.json()。不要忘记从 fastify 导入正确的类型
@Catch(HttpException) 装饰器将所需的元数据绑定到异常过滤器,告诉 Nest 这个特定的过滤器正在寻找 HttpException 类型的异常。 @Catch() 装饰器可以采用单个参数或逗号分隔的列表,可以一次为多种类型的异常设置过滤器
参数主机(Arguments host)
看一下 catch() 方法的参数:
- exception:当前正在处理的异常对象
- host:一个 ArgumentsHost 对象。ArgumentsHost 是一个强大的实用程序对象。在此代码示例中,使用它来获取对传递给原始请求处理程序(在异常产生的控制器中)的 Request 和 Response 对象的引用。在此代码示例中,在 ArgumentsHost 上使用了一些辅助方法来获取所需的 Request 和 Response 对象
这种抽象级别的原因是 ArgumentsHost 在所有上下文中都起作用(例如,现在使用的 HTTP 服务器上下文,还有微服务和 WebSockets)
绑定过滤器
将新的 HttpExceptionFilter 与 CatsController 的 create() 方法联系起来:
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
提示: @UseFilters() 装饰器是从 @nestjs/common 包导入的
这里使用了 @UseFilters() 装饰器,类似于 @Catch() 装饰器,它可以采用单个过滤器实例,或以逗号分隔的过滤器实例列表。在这里就地创建了 HttpExceptionFilter 的实例。或者可以传入类,通过依赖注入绑定过滤器:
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
提示:尽可能使用类而不是实例来应用过滤器。它减少了内存使用量,因为 Nest 可以轻松地在整个模块中重用同一类的实例
上面的示例中,HttpExceptionFilter 仅应用于单个 create() 路由处理程序,使其具有方法作用域。异常过滤器可以在不同级别作用域内:controller/resolver/gateway(控制器/解析器/网关)的方法作用域、控制器作用域(controller-scoped)或全局作用域(global-scoped)。例如,要将过滤器设置为控制器作用域,可以执行以下操作:
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
要创建全局作用域的过滤器,可以执行以下操作:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
警告:useGlobalFilters() 方法不为网关或混合应用设置过滤器
全局作用域的过滤器用于整个应用,用于每个控制器和每个路由处理程序。由于这是在任何 Module 外(不在上下文中)注册的全局过滤器,所以无法进行依赖注入
为了解决此问题,可以使用以下结构直接从任何模块注册全局作用域的过滤器:
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
提示:当使用此方法对过滤器执行依赖注入时,请注意,无论采用此构造的模块如何,过滤器实际上都是全局的。这应该在哪里完成?选择定义过滤器(上例中的 HttpExceptionFilter)的模块。此外,useClass 不是处理自定义提供程序注册的唯一方法
可以根据需要使用此技术添加任意数量的过滤器;只需将每个添加到 providers 数组即可
捕获所有
为了捕获每个未处理的异常(无论异常类型如何),可以将 @Catch() 装饰器的参数列表留空,例如 @Catch()
在下面的示例中,有一个与平台无关的代码,因为它使用HTTP 适配器来传递响应,并且不直接使用任何特定于平台的对象(Request 和 Response):
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
// In certain situations `httpAdapter` might not be available in the
// constructor method, thus we should resolve it here.
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
};
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}
警告:当将捕获所有内容的异常过滤器与绑定到特定类型的过滤器组合时,应首先声明 "捕获任何异常" 过滤器,以允许特定过滤器正确处理绑定类型
继承
通常,可以创建完全定制的异常过滤器来满足应用需求。但是,在某些用例中,开发者可能只想扩展内置的默认全局异常过滤器,并根据某些因素覆盖行为
为了将异常处理委托给基本过滤器,需要扩展 BaseExceptionFilter 并调用继承的 catch() 方法
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
super.catch(exception, host);
}
}
警告:扩展 BaseExceptionFilter 的方法范围和控制器范围的过滤器不应使用 new 实例化。相反,让框架自动实例化它们
上面的实现只是一个演示方法的 shell。扩展异常过滤器的实现将包括定制的业务逻辑(例如,处理各种条件)
全局过滤器可以扩展基本过滤器。这可以通过两种方式之一完成:
- 第一种方法是在实例化自定义全局过滤器时注入 HttpAdapter 引用:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(3000);
}
bootstrap();
- 第二种方法是使用 APP_FILTER token
管道(Pipes)
管道是用 @Injectable() 装饰器注释的类,它实现了 PipeTransform 接口
管道有两个典型的用例:
- 转型:将输入数据转换为所需的形式(例如,从字符串到整数)
- 验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常
在这两种情况下,管道都在由 controller route handler (控制器路由处理器)处理的 arguments 上运行。Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生,之后会使用任何(可能)转换的参数调用路由处理程序
Nest 附带了许多内置管道,可以开箱即用。还可以构建自己的自定义管道
提示:管道在例外区域内运行。这意味着当 Pipe 抛出异常时,它由异常层处理(全局异常过滤器和应用于当前上下文的任何异常过滤器)。综上所述,应该很清楚,当在 Pipe 中抛出异常时,将不会执行后续任何 controller 方法。这提供了一种最佳实践技术,用于在系统边界验证从外部源进入应用的数据
内置管道
Nest 附带九个开箱即用的管道:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
它们是从 @nestjs/common 包中导出的
绑定管道
要使用管道,需要将管道类的实例绑定到适当的上下文,以下示例称为在方法参数级别绑定管道:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
如果传入的 id 不为数字或者转换不成数字,则会抛出异常:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
与管道和守卫一样,可以传递一个就地实例。如果想通过传递选项来自定义内置管道的行为,那么就可以传递就地实例:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
绑定其他转换管道(所有 Parse* 管道)的工作原理类似。这些管道都在验证路由参数、查询字符串参数和请求正文值的上下文中工作
例如,使用查询字符串参数:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
使用 ParseUUIDPipe 解析字符串参数并验证它是否为 UUID:
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
提示:当使用 ParseUUIDPipe() 时,正在解析版本 3、4 或 5 中的 UUID,如果只需要特定版本的 UUID,则可以在管道选项中传递版本
自定义管道
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
提示:PipeTransform<T, R> 是一个通用接口,任何管道都必须实现。泛型接口用 T 表示输入 value 的类型,用 R 表示 transform() 方法的返回类型
每个管道都必须实现 transform() 方法来实现 PipeTransform 接口。这个方法有两个参数:
- value:当前处理的方法参数(在被路由处理方法接收之前)
- metadata:当前处理的方法参数的元数据。元数据对象具有以下属性:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
这些属性描述了当前处理的参数:
| type | 指示参数是主体 @Body() 、査询 @Query() 、参数 @Param() 还是自定义参数 |
|---|---|
| metatype | 提供参数的元类型,例如 String。注意:如果在路由处理程序方法签名中省略类型声明或使用普通 JavaScript,则该值为 undefined |
| data | 传递给装饰器的字符串,例如 @Body('string') 。如果将装饰器括号留空,则为 undefined |
警告:TypeScript 接口在转译过程中消失。因此,如果方法参数的类型声明为接口而不是类,则 metatype 值将为 Object
基于模式的验证
在尝试运行服务方法之前确保请求体对象正文是有效的
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
要确保对 create 方法的任何传入请求都包含有效的正文。所以要验证 createCatDto 对象的三个成员。可以在路由处理程序方法内执行此操作,但这样做并不理想,因为它会破坏单一职责原则(SRP)
另一种方法可能是创建一个验证器类并在那里委派任务。这样做的缺点是必须记住在每个方法的开头调用此验证器
创建验证中间件可能有效,但不幸的是,不可能创建可在整个应用的所有上下文中使用的通用中间件。这是因为中间件不知道执行上下文,包括将被调用的处理程序及其任何参数
当然,这正是设计管道的原因。因此,继续改进验证管道
对象模式验证
有几种方法可用于以干净的 DRY 方式进行对象验证。一种常见的方法是使用基于模式的验证
Zod 库允许使用可读的 API 以直接的方式创建模式。现在来构建一个使用基于 Zod 的模式的验证管道
首先安装所需的包:
npm install --save zod
在下面的代码示例中,创建了一个将模式作为 constructor 参数的简单类。然后应用 schema.parse() 方法,该方法根据提供的模式验证传入的参数
如前所述,验证管道要么返回不变的值,要么引发异常
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
绑定验证管道
绑定验证管道也非常简单
在这种情况下,希望在方法调用级别绑定管道。在当前的示例中,需要执行以下操作才能使用ZodValidationPipe:
- 创建 ZodValidationPipe 的实例
- 在管道的类构造函数中传递上下文特定的 Zod 架构
- 将管道绑定到方法
Zod 架构示例:
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
使用 @UsePipes 绑定到 controller 上
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
提示: @UsePipes() 装饰器是从 @nestjs/common 包导入的
警告:zod 库需要在 tsconfig.json 文件中启用 strictNullChecks 配置
类验证器
需要 TypeScript
Nest 与 class-validator 库配合良好。这个强大的库允许使用基于装饰器的验证。基于装饰器的验证非常强大,特别是与 Nest 的 Pipe 功能结合使用时,因为可以访问已处理属性的 metatype。需要安装所需的包:
npm i --save class-validator class-transformer
安装了之后就可以向 CreateCatDto 类添加一些装饰器。在这里可以看到这种技术的一个显着优势:CreateCatDto 类仍然是 Post 主体对象的唯一真实来源(而不是必须创建一个单独的验证类)
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
创建一个使用这些注释的 ValidationPipe 类
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
提示:不必自己构建通用验证管道,因为 ValidationPipe 对于 Nest 开箱即用。内置的 ValidationPipe 提供更多的选项
注意:上面使用的 class-transformer 库,它与类验证器库由同一作者制作,因此它们可以很好地协同工作
代码分析:
首先,注意 transform() 方法被标记为 async。这是可能的,因为 Nest 支持同步和异步管道。将此方法设为 async,因为某些类验证器验证 可以是异步的(利用 Promises)
接下来,使用解构将元类型字段(仅从 ArgumentMetadata 中提取此成员)提取到我们的 metatype 参数。这只是获取完整 ArgumentMetadata 的简写,然后有一个附加语句来分配元类型变量
接下来,注意辅助函数 toValidate()。当正在处理的当前参数是原生 JavaScript 类型时,它负责绕过验证步骤(它们不能附加验证装饰器,因此没有理由让它们通过验证步骤)
接下来,使用类转换器函数 plainToInstance() 将纯 JavaScript 参数对象转换为类型化对象,以便应用验证。这样做的原因是,当从网络请求反序列化时,传入的 post body 对象没有任何类型信息(这是底层平台(例如 Express)的工作方式)。类验证器需要使用之前为 DTO 定义的验证装饰器,因此需要执行此转换以将传入主体视为经过适当装饰的对象,而不仅仅是普通对象
最后,如前所述,由于这是一个验证管道,它要么返回不变的值,要么引发异常
最后一步是绑定 ValidationPipe。管道可以是参数作用域的、方法作用域的、控制器作用域的或全局作用域的。之前,通过基于 Zod 的验证管道,我们看到了在方法级别绑定管道的示例。在下面的示例中,我们将管道实例绑定到路由处理程序 @Body() 装饰器,以便调用我们的管道来验证请求体正文
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
当验证逻辑只涉及一个指定参数时,参数作用域的管道很有用
全局作用域管道
由于 ValidationPipe 被创建为尽可能通用,因此可以通过将其设置为全局作用域的管道来实现其完整实用性,以便将其应用于整个应用中的每个路由处理程序
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
注意:在 混合应用 的情况下,useGlobalPipes() 方法不会为网关和微服务设置管道。对于 standard (非混合)微服务应用,useGlobalPipes() 会全局安装管道
全局管道用于整个应用,用于每个 controller 和每个路由处理程序
请注意,就依赖注入而言,从任何 module 外部注册的全局管道(如上例中的 useGlobalPipes())无法注入依赖,因为绑定是在任何 module 的上下文之外完成的。为了解决这个问题,可以使用以下结构直接从任何 module 设置全局管道:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
提示:当使用此方法对管道执行依赖注入时,请注意,无论采用此构造的模块如何,管道实际上都是全局的。这应该在选择自定义管道(上例中的 ValidationPipe)的模块。此外,useClass 不是处理自定义 provider 注册的唯一方法
内置的 ValidationPipe
不一定要自己构建通用验证管道,因为 ValidationPipe 由 Nest 开箱即用。内置的 ValidationPipe 提供了很多选项
转换用例
验证不是自定义管道的唯一用例。管道还可以将输入数据转换为所需的格式。这是可能的,因为从 transform 函数返回的值完全覆盖了参数的先前值
考虑到有时候从客户端传过来的数据需要进行一些改变 - 例如将字符串转换为整数 - 在它可以被路由处理程序方法正确处理之前。此外,一些必需的数据字段可能会丢失时可以使用自定义的默认值。转换管道可以通过在客户端请求和请求处理程序之间插入处理方法来执行这些功能
这是一个简单的 ParseIntPipe,它负责将字符串解析为整数值。(Nest 有一个更复杂的内置 ParseIntPipe;这里是自定义了一个同名转换管道的简单演示)
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
将此管道绑定到选定的参数:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
使用请求中提供的 ID 从数据库中选择现有的用户实体:
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
提供默认值
Parse*管道需要定义参数值。他们在收到 null 或 undefined 值时抛出异常。为了允许接口处理丢失的查询字符串参数值,必须提供一个默认值,以便在 Parse* 管道对这些值进行操作之前注入
DefaultValuePipe 就是为这个目的服务的。只需在相关的 Parse* 管道之前的 @Query() 装饰器中实例化一个 DefaultValuePipe,如下所示:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
守卫(Guards)
守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate 接口
守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的中间件处理。中间件是身份验证的不错选择,因为诸如 token 验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系
但是中间件,就其本质而言,是愚蠢的。它不知道调用 next() 函数后将执行哪个处理程序。另一方面,Guards 可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,可以在请求/响应周期的正确位置插入处理逻辑,并以声明方式进行。这有助于代码保持干爽和声明式
提示:守卫在所有中间件之后、任何拦截器或管道之前执行
授权守卫
如前所述,授权对于 Guards 来说是一个很好的用例,因为只有当调用者(通常是特定的经过身份验证的用户)拥有足够的权限时,特定的路由才应该可用。假设现在要构建的 AuthGuard 是一个经过身份验证的用户(token 已经附加到请求标头)。它将提取并验证令牌,并使用提取的信息来确定请求是否可以继续
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
validateRequest() 函数内的逻辑可以根据需要变得简单或复杂。这个例子的要点是展示守卫如何适应请求/响应周期
每个守卫都必须实现一个 canActivate() 函数。此函数应返回一个布尔值,指示是否允许当前请求。它可以同步或异步(通过 Promise 或 Observable)返回响应。Nest 使用返回值来控制下一步的动作:
- 如果它返回 true,请求将被处理
- 如果它返回 false,Nest 将拒绝该请求
执行上下文
canActivate() 函数采用单个参数,即 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHost。之前在异常过滤器章节中看到了 ArgumentsHost。在上面的示例中,只是使用之前在 ArgumentsHost 上定义的相同辅助方法来获取对 Request 对象的引用
通过扩展 ArgumentsHost,ExecutionContext 还添加了几个新的辅助方法,这些方法提供有关当前执行过程的更多详细信息。这些细节有助于构建更通用的守卫,这些守卫可以在广泛的控制器、方法和执行上下文中工作
基于角色的身份验证
构建一个功能更强大的守卫,只允许具有特定角色的用户访问。从一个基本的守卫模板开始,并在接下来的部分中构建它。目前,它允许所有请求继续:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
绑定守卫
与管道和异常过滤器一样,守卫可以是可以设置控制器范围、方法范围或全局作用域。下面,使用 @UseGuards() 装饰器设置了一个控制器作用域的守卫。这个装饰器可以接受一个参数,或者一个逗号分隔的参数列表。这可以通过一个声明轻松应用一组适当的保护
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
提示: @UseGuards() 装饰器是从 @nestjs/common 包导入的
上面传递了 RolesGuard 类(而不是实例),将实例化的责任留给框架并启用依赖注入。与管道和异常过滤器一样,也可以传递一个就地实例:
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
上面的构造将守卫附加到此控制器声明的每个处理程序。如果希望守卫仅应用于单个方法,也可以在方法级别应用 @UseGuards() 装饰器
为了设置全局守卫,使用 Nest 应用实例的 useGlobalGuards() 方法:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
警告:useGlobalGuards() 方法不为网关或混合应用设置守卫
全局守卫用于整个应用,用于每个控制器和每个路由处理程序。在依赖注入方面,从任何 module 外部注册的全局守卫(如上例中的 useGlobalGuards())不能注入依赖,因为这是在任何 module 的上下文之外完成的。为了解决这个问题,可以使用以下结构直接从任何 module 设置守卫:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
提示:当使用此方法对守卫执行依赖注入时,请注意,无论采用此构造的模块如何,守卫实际上都是全局的。这应该在选择自定义守卫(上例中的 RolesGuard)的模块。此外,useClass 不是处理自定义提供程序注册的唯一方法
为每个处理程序设置角色
利用最重要的防护功能 - 执行上下文。它还不知道角色,或者每个处理程序允许哪些角色。例如,CatsController 可以针对不同的路由使用不同的权限方案。有些可能只对管理员用户可用,而另一些可能对所有人开放
这就是自定义元数据发挥作用的地方。Nest 提供了通过 Reflector#createDecorator 静态方法创建的装饰器或内置 @SetMetadata() 装饰器将自定义元数据附加到路由处理程序的功能
例如,使用 Reflector#createDecorator 方法创建一个 @Roles() 装饰器,该方法将元数据附加到处理程序。Reflector 由框架开箱即用地提供,并从 @nestjs/core 包中公开
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
这里的 Roles 装饰器是一个接受 string[] 类型的单个参数的数组
现在,要使用这个装饰器,只需用它注释处理程序:
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
这里将 Roles 装饰器元数据附加到 create() 方法,表明只有具有 admin 角色的用户才可以访问此路由
或者,也可以使用内置的 @SetMetadata() 装饰器,而不是使用 Reflector#createDecorator 方法
结合守卫与角色
现在回过头来将它与 RolesGuard 联系起来。目前,它在所有情况下都只返回 true,允许每个请求继续进行。现在希望通过将分配给当前用户的角色与当前正在处理的路由所需的实际角色进行比较来使返回值成为有条件的。为了访问路由的角色(自定义元数据),将再次使用 Reflector 辅助程序类,如下所示:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
提示:在 Node.js 世界中,通常的做法是将授权用户附加到 request 对象。因此,在上面的示例代码中,假设 request.user 包含用户实例和允许的角色。在你的应用中,你可能会在自定义身份验证防护(或中间件)中进行这种关联
警告:matchRoles() 函数内的逻辑可以根据需要简单或复杂。这个例子的要点是展示守卫如何适应请求/响应周期
当权限不足的用户请求接口时,Nest 会自动返回 403 响应
请注意,当守卫返回 false 时,框架会抛出 ForbiddenException。如果想返回不同的错误响应,应该抛出自己指定的异常。例如:
throw new UnauthorizedException();
守卫抛出的任何异常都将由异常层(全局异常过滤器和应用于当前上下文的任何异常过滤器)处理
拦截器(Interceptors)
注意:需要引入 rxjspnpm i -s rxjs
拦截器是用 @Injectable() 装饰器注释并实现 NestInterceptor 接口的类
拦截器具有一组有用的功能,这些功能的灵感来自面向切面编程 (AOP) 技术。它们可以:
- 在方法执行之前/之后绑定额外的逻辑
- 转换函数返回的结果
- 转换函数抛出的异常
- 扩展基本功能行为
- 根据特定条件完全覆盖函数(例如,出于缓存目的)
基本
每个拦截器都实现了 intercept() 方法,它有两个参数。第一个是 ExecutionContext 实例。ExecutionContext 继承自 ArgumentsHost。ArgumentsHost 是已传递给原始处理程序的参数的封装器,并且包含基于应用类型的不同参数数组
执行上下文
通过扩展 ArgumentsHost,ExecutionContext 还添加了几个新的辅助方法,这些方法提供有关当前执行过程的更多详细信息。这些细节有助于构建更通用的拦截器,这些拦截器可以在广泛的控制器、方法和执行上下文中工作
调用处理程序
第二个参数是 CallHandler。CallHandler 接口实现了 handle() 方法,可以使用它在拦截器中的某个点调用路由处理程序方法。如果在 intercept() 方法的实现中不调用 handle() 方法,则不会执行路由处理程序方法
这种方法意味着 intercept() 方法有效地封装了请求/响应流。因此,可以在最终路由处理程序执行之前和之后实现自定义逻辑。很明显,可以在 intercept() 方法中编写在调用 handle() 之前执行的代码,但是如何影响之后发生的情况呢?因为 handle() 方法返回一个 Observable,可以使用强大的 RxJS 操作符来进一步操作响应。使用面向切面的编程术语,路由处理程序的调用(即调用 handle() )称为切入点,表示它是我们附加逻辑的插入点
例如,考虑传入的 POST /cats 请求。此请求发往 CatsController 内部定义的 create()处理程序。如果在任何地方调用了一个不调用 handle() 方法的拦截器,则不会执行 create() 方法。一旦 handle() 被调用(并且其 Observable 已被返回),create() 处理程序将被触发。一旦通过 Observable 接收到响应流,就可以对该流执行其他操作,并将最终结果返回给调用者
切面拦截
使用拦截器记录用户交互(例如,存储用户调用、异步调度事件或计算时间戳)。下面展示一个简单的 LoggingInterceptor:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
提示:NestInterceptor<T, R> 是一个通用接口,其中 T 表示 Observable (支持响应流)的类型,R 是 Observable 封装的值的类型
注意:拦截器,如控制器、提供器、守卫等,可以通过它们的 constructor 注入依赖
由于 handle() 返回一个 RxJS Observable,可以使用多种运算符来操纵流。在上面的示例中,使用了 tap() 运算符,它在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会以其他方式干扰响应周期
绑定拦截器
为了设置拦截器,使用从 @nestjs/common 包中导入的 @UseInterceptors() 装饰器。与 pipes 和 guards 一样,拦截器可以是控制器作用域的、方法作用域的或全局作用域的
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
提示: @UseInterceptors() 装饰器是从 @nestjs/common 包导入的
使用上述构造,CatsController 中定义的每个路由处理程序都将使用 LoggingInterceptor。当有人调用 GET /cats 接口时,将在标准输出中看到以下输出:
Before...
After... 1ms
与管道、守卫和异常过滤器一样,也可以传递一个就地实例:
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
上面的构造将拦截器附加到此控制器声明的每个处理程序。如果想将拦截器的范围限制为单个方法,只需在方法级别应用装饰器即可
为了设置全局拦截器,使用 Nest 应用实例的 useGlobalInterceptors() 方法:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
全局拦截器用于整个应用,用于每个控制器和每个路由处理程序。在依赖注入方面,从任何模块外部注册的全局拦截器(使用 useGlobalInterceptors() ,如上例所示)无法注入依赖,因为这是在任何模块的上下文之外完成的。为了解决这个问题,可以使用以下结构直接从任何 module 设置拦截器:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
提示:当使用此方法对拦截器执行依赖注入时,请注意,无论采用此构造的 module 如何,拦截器实际上都是全局的。这应该在选择自定义拦截器(上例中的 LoggingInterceptor)的模块。此外,useClass 不是处理自定义提供 provider 的唯一方法
响应映射
handle() 会返回 Observable。该流包含从路由处理程序返回的值,因此可以使用 RxJS 的 map() 运算符轻松地改变它
警告:响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res() 对象)
创建 TransformInterceptor,它将以简单的方式修改每个响应以演示该过程。它将使用 RxJS 的 map() 运算符将响应对象分配给新创建对象的 data 属性,将新对象返回给客户端
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
提示:Nest 拦截器可与同步和异步 intercept() 方法一起使用。如有必要,可以简单地将方法切换为 async
通过上述构造,当有人调用 GET /cats 接口时,响应将如下所示(假设路由处理程序返回一个空数组 [])
{
"data": []
}
拦截器在为整个应用中出现的需求创建可重用的解决方案方面具有很大的价值。例如,假设需要将每次出现的 null 值转换为空字符串''。可以使用一行代码来完成,并全局绑定拦截器,以便每个注册的处理程序自动使用它
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value ));
}
}
异常映射
另一个有趣的用例是利用 RxJS 的 catchError() 运算符来覆盖抛出的异常:
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
流覆盖
有几个原因导致有时可能想要完全阻止调用处理程序并返回一个不同的值。一个明显的例子是实现缓存以提高响应时间。这里看一下一个简单的缓存拦截器,它从缓存返回其响应。在一个现实的例子中,需要考虑到的其他因素,包括 TTL、缓存失效、缓存大小等,但这超出了本次讨论的作用域。在这里将提供一个演示主要概念的基本示例
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
CacheInterceptor 有一个硬编码的 isCached 变量和一个硬编码的响应 []。需要注意的关键点是,在这里返回一个由 RxJS of() 运算符创建的新流,因此根本不会调用路由处理程序。当有人调用使用 CacheInterceptor 的接口时,将立即返回响应(硬编码的空数组)。为了创建通用解决方案,可以利用 Reflector 并创建自定义装饰器
更多运算符
使用 RxJS 运算符操纵流的可能性提供了许多功能。考虑另一个常见的用例。想象一下想要处理路由请求的超时。当接口在一段时间后未返回任何内容时,希望以错误响应终止。以下结构可以实现这一点:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
5 秒后,请求处理将被取消。还可以在抛出 RequestTimeoutException 之前添加自定义逻辑(例如释放资源)
自定义装饰器
Nest 是围绕一种称为装饰器的语言功能构建的。装饰器是许多常用编程语言中的一个众所周知的概念,但在 JavaScript 世界中,它们仍然相对较新。为了更好地理解装饰器是如何工作的,推荐阅读 本文。这是一个简单的定义:
An ES2016 decorator is an expression which returns a function and can take a target, name and property descriptor as arguments. You apply it by prefixing the decorator with an @ character and placing this at the very top of what you are trying to decorate. Decorators can be defined for either a class, a method or a property.
ES2016装饰器是一个返回函数的表达式,它可以接受一个目标、名称和属性描述符作为参数。你可以在装饰器前加上@字符,并将其放在要装饰的东西的最顶端。可以为类、方法或属性定义装饰器
参数装饰器
Nest 提供了一组有用的参数装饰器,可以将它们与 HTTP 路由处理程序一起使用。下面是提供的装饰器和它们代表的普通 Express(或 Fastify)对象的列表
| @Request(), @Req() | req |
|---|---|
| @Response(), @Res() | res |
| @Next() | next |
| @Session() | req.session |
| @Param(param?: string) | req.params / req.params[param] |
| @Body(param?: string) | req.body / req.body[param] |
| @Query(param?: string) | req.query / req.query[param] |
| @Headers(param?: string) | req.headers / req.headers[param] |
| @Ip() | req.ip |
| @HostParam() | req.hosts |
自定义装饰器
const user = req.user;
为了使代码更具可读性和透明性,可以创建一个 @User() 装饰器并在所有控制器中重复使用它:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
然后,可以在任何符合要求的地方简单地使用它
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
传递数据
当装饰器的行为取决于某些条件时,可以使用 data 参数将参数传递给装饰器的工厂函数。一个用例是自定义装饰器,它通过键从请求对象中提取属性。例如,假设我们的认证层验证请求并将用户实体附加到请求对象。经过身份验证的请求的用户实体可能如下所示:
{
"id": 101,
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@email.com",
"roles": ["admin"]
}
定义一个装饰器,它以属性名称作为键,如果存在则返回关联值(如果不存在则返回 undefined,或者尚未创建 user 对象)
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
以下是如何通过控制器中的 @User() 装饰器访问特定属性:
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`);
}
可以使用具有不同键的相同装饰器来访问不同的属性。如果 user 对象很深或很复杂,这可以使请求处理程序的实现更容易、更易读
对于 TypeScript 用户,请注意 createParamDecorator<T>() 是泛型。这意味着你可以显式强制执行类型安全,例如 createParamDecorator<string>((data, ctx) => ...)。或者,在工厂函数中指定参数类型,例如 createParamDecorator((data: string, ctx) => ...)。如果两者都省略,则 data 的类型将为 any
装饰器组合
Nest 提供了一个辅助方法来组合多个装饰器。例如,假设想要将所有与身份验证相关的装饰器组合成一个装饰器。这可以通过以下构造来完成
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
然后,可以按如下方式使用此自定义 @Auth() 装饰器:
@Get('users')
@Auth('admin')
findAllUsers() {}
这具有通过单个声明应用所有四个装饰器的效果
警告
@nestjs/swagger 包中的 @ApiHideProperty() 装饰器不可组合,并且无法与 applyDecorators 函数一起正常工作
装饰器分类
类装饰器
会自动把class的构造函数传入到装饰器的第一个参数 target,然后通过prototype可以自定义添加属性和方法
const decotators = (target: any) => {
target.prototype.name = "nestjs";
};
@decotators
class NestJS {
constructor() {}
}
const nestjs: any = new NestJS();
console.log(nestjs.name);
属性装饰器
返回两个参数
- 原形对象
- 属性的名称
const currency: PropertyDecorator = (target: any, key: string | symbol) => {
console.log(target, key);
};
class Nestjs {
@currency
public name: string;
constructor() {
this.name = "";
}
getName() {
return this.name;
}
}
参数装饰器
返回三个参数
- 原形对象
- 方法的名称
- 参数的位置
const currency: ParameterDecorator = (
target: any,
key: string | symbol,
index: number
) => {
console.log(target, key, index);
};
class Nestjs {
public name: string;
public age: number;
constructor() {
this.name = "nestjs";
this.age = 21;
}
getName(name: string, @currency age: number) {
return this.name;
}
}
方法装饰器
返回三个参数
- 原形对象
- 方法的名称
- 属性描述符
-
- 可写:writable
- 可枚举:enumerable
- 可配置:configurable
const currency: MethodDecorator = (
target: any,
key: string | symbol,
descriptor: any
) => {
console.log(target, key, descriptor);
};
class Nestjs {
public name: string;
constructor() {
this.name = "";
}
@currency
getName(name: string, age: number) {
return this.name;
}
}
执行顺序
所有的中间件->controller上的守卫->controller上的拦截器->方法上的守卫->方法上的拦截器->管道
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule implements NestModule {
// 这里可以添加一些自定义的配置
configure(consumer: MiddlewareConsumer) {
consumer.apply(StartTimeMiddleware, funcAct).forRoutes('cats'); // 在所有 cats 路由上添加中间件
}
}
@UseInterceptors(myInterceptor)
@UseGuards(ConGuard)
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
// 在进入自定义管道之前定义了默认值
@UseGuards(FuncGuard)
@UseInterceptors(funcInterceptor)
@Get(':id')
findOne(@Param('id', new DefaultValuePipe(0), myPipe) id: string) {
console.log('======>这里执行 controller 方法');
return this.catsService.findOne(id);
}
}