简介
安装
可以使用 Nest CLI 创建项目,也可以克隆一个项目(两者的结果是一样的)。
使用 Nest CLI 构建项目
$ npm i -g @nestjs/cli
$ nest new project-name
其他安装方式
使用 Git 安装基于 TypeScript 的项目
$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start
要安装基于 JavaScript 的项目,执行上面的命令时使用 javascript-starter.git
还可以通过 npm、yarn安装核心和支撑文件,这种情况下,你将自己创建项目样板文件。
$ npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata
概述
第一步
新建
$ npm i -g @nestjs/cli
$ nest new project-name
TypeScript
strict模式的项目,将--strict传递给nest new命令
src/目录几个核心文件。
app.controller.spec.tsapp.controller.tsapp.module.tsapp.service.tsmain.ts
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();
使用核心类 NestFactory 创建应用实例。NestFactory 暴露一些静态方法用于创建应用实例。 create() 方法返回实现 INestApplication 接口的对象。该对象提供一组可用的方法,后面章节中对这些方法进行详细描述。 上例中,只是启动 HTTP 服务,让应用程序等待 HTTP 请求。
在创建应用实例时发生错误,应用会退出并返回错误代码
1。如果想让它抛出错误,请禁用abortOnError选项 (如,NestFactory.create(AppModule, { abortOnError: false }))。
平台
两种平台开箱即用express、fastify
platform-express | Express一个著名的极简web框架。默认使用@nestjs/platform-express |
platform-fastify | Fastify一个高性能和低开销的框架,高度关注提供最大的效率和速度。这里使用。 |
无论哪个平台,都公开自己的接口。分别被视为NestExpressApplication和NestFastifyApplication
将一个类型传递给NestFactory.create(),应用程序对象将有该平台的方法。除非要访问平台API,否则不用指定类型。
const app = await NestFactory.create<NestExpressApplication>(AppModule);
控制器
控制器处理传入请求并向客户端返回响应。
创建有内置验证的CRUD控制器
$ nest g resource [name]
路由
@Controller()装饰器是定义控制器所必需的。在@Controller()装饰器中指定路径前缀cats可对一组相关路由分组。
//cats.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get('list') // 'cats/list'
findAll(): string {
return 'This action returns all cats';
}
}
使用CLI创建控制器
$ nest g controller cats
@Get()装饰器,装饰请求处理方法。
该方法返回200状态码和响应, 介绍两种不同响应:
| Standard (recommended)标准(推荐) | 使用这个内置方法,请求返回对象或数组时,自动序列化为JSON。返回基本类型(如,字符串,数字,布尔值)时只发送值不尝试序列化它。响应状态码默认是200,除了使用201的POST请求。添加@HttpCode(…)装饰器,可以改变状态码。 |
| Library-specific 库特有的 | @Res() 注入库特定的响应对象,(如,findAll(@Res() response))。使用此方法,就能使用由该响应对象暴露的原生响应处理函数。例如,使用 Express,可以使用 response.status(200).send() 构建响应 |
Nest 检测处理程序何时使用 @Res() 或 @Next(),表明你选择了特定于库的选项。如果在一个处理函数上同时使用了这两个方法,那么此处的标准方式就是自动禁用此路由, 你将不会得到你想要的结果。如果需要在某个处理函数上同时使用这两种方法(例如,通过注入响应对象,单独设置 cookie / header,但把其余部分留给框架),你必须在装饰器 @Res({ passthrough: true }) 中将 passthrough 选项设为 true
请求对象
Nest提供了对底层平台(默认为Express)请求对象的访问。添加@Req()装饰器注入请求对象,从而访问请求对象。
//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';
}
}
安装@types/express便于类型提示。
请求对象有查询字符串、参数、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 |
注入@Res()或@Response()时,Nest变成Library-specific模式,您将负责管理响应。必须调用响应对象(如res.json(…)或res.send(…))发出相应类型的响应,否则HTTP服务器将挂起。
资源
创建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(…)装饰器可改变状态码
import HttpCode from '@nestjs/common'
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
状态码不是静态的,而取决于各种因素。在这种情况下,您可以使用library-specific的响应对象(@Res()注入),或在发生错误时抛出异常。
头
自定义响应头,使用@Header()装饰器或特定库(library-specific)的响应对象(调用res.header())。
import Header from '@nestjs/common'
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
重定向
将响应重定向到特定URL,使用@Redirect()装饰器或特定库(library-specific)的响应对象(调用res.redirect())。
@Redirect()有两个参数,url和statusCode,都是可选的。如果省略,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/' };
}
}
路由参数
路由参数使用@Param()装饰器访问。
import Param from '@nestjs/common'
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
参数传递给@Param()装饰器,通过名称使用参数。
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}
子域路由
@Controller装饰器传入含host属性对象,请求需匹配特定值。
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
Fastify缺乏对嵌套路由器的支持,使用子域路由时,应使用(默认)Express适配器。
host选项可以使用标记捕获主机名中该位置的动态值。使用@HostParam()装饰器访问主机参数。
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
作用域
几乎所有内容共享。数据库连接池,全局状态单例服务等。Node.js不遵循请求/响应多线程无状态模型,每个请求由单独线程处理。
某些情况下,控制器基于请求的生命周期是所需的行为,如GraphQL应用程序中请求缓存、请求跟踪或多租户。
异步性
async返回Promise
//cats.controller.ts
@Get()
async findAll(): Promise<any[]> {
return [];
}
Nest路由处理程序甚至更强大,它能够返回RxJS可观察流(observable streams)。Nest自动订阅底层的源并接受最后发出的值(一旦流完成)。
//cats.controller.ts
@Get()
findAll(): Observable<any[]> {
return of([]);
}
请求有效载荷
添加@Body()装饰器。
使用TypeScript,需要确定DTO(数据传输对象)。
//create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
//cats.controller.ts
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
ValidationPipe可以过滤掉不该被接收的属性。将可接受的属性列入白名单,没有在白名单中的属性将从结果对象中删除。在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操作响应的标准方式。操作响应的第二种方式是使用特定库(library-specific)的响应对象。注入特定的响应对象,使用@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标准响应)的兼容性,如Interceptors(拦截器)和@HttpCode() / @Header()装饰器。解决这个问题,设置passthrough为true
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.status(HttpStatus.OK);
return [];
}
现在根据条件设置cookies或headers,其余工作留给框架。
提供者
提供者是基本概念。许多基本类被视为提供者——服务(services)、存储库(repositories)、工厂(factories)、助手(helpers)等。提供者主要思想是作为依赖注入; 意味着对象之间可以创建各种关系,“连接”对象实例可以委托给运行系统。
构建的CatsController。控制器处理HTTP请求,复杂的任务委派给提供者。提供者是声明为提供者的普通类。
服务
//cats.service.ts
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;
}
}
@Injectable()装饰器附加了元数据(metadata),声明CatsService是个由Nest IoC容器管理的类。
//interfaces/cat.interface.ts
export interface Cat {
name: string;
age: number;
breed: string;
}
//cats.controller.ts
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。这种简写方式立即声明和初始化catsService成员。注意私有语法。
依赖注入
Nest是围绕依赖注入的强设计模式构建的。
TypeScript管理依赖关系很容易,根据类型进行解析。
constructor(private catsService: CatsService) {}
作用域
提供者与应用程序生命周期(作用域)同步。引导(bootstrapped)应用程序时,须解析每个依赖项,须实例化每个提供者。应用程序关闭时,每个提供者将被销毁。有些方法可以使提供者的生命周期作为请求作用域。更多技巧 here.
自定义提供者
内置的控制反转(IoC)容器,用来解决提供者之间的关系。这是依赖注入特性的基础,实际上它更强大。定义提供者有几种方法: 纯值、类、异步或同步工厂。More examples here.
可选提供者
有时,有不一定需要解析的依赖项。类可能依赖于配置对象,没有传配置对象,应使用默认值。依赖项为可选,因为缺少配置提供者不会导致错误。
要表示提供者是可选的,可以在构造函数的签名中使用@Optional()装饰器。
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
例子中,使用一个自定义提供者,这是包含HTTP_OPTIONS自定义令牌的原因。更多自定义提供者及令牌的信息这里。
基于属性的注入
到目前使用的基于构造函数的注入,提供者通过构造函数注入。特定情况下,基于属性的注入是有用的。顶级类依赖一个或多个提供者,在子类构造函数中调用super()向上传递非常繁琐。避免这种情况,属性级别使用@Inject()装饰器。
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
如果您的类没有扩展其他提供者,应该使用基于构造函数的注入。
提供者登记
//app.module.ts
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 {}
目录结构应该是这样
src
cats
dto
create-cat.dto.ts
interfaces
cat.interface.ts
cats.controller.ts
cats.service.ts
app.module.ts
main.ts
手动实例化
可能需要跳出内置的依赖注入系统,手动检索或实例化提供者。讨论两个这样的主题。
获得现有实例或动态实例提供者,参考模块module.
bootstrap()函数中获取提供者(没有控制器的独立应用程序,或引导期间利用配置服务),参阅( Standalone applications)。
模块
模块是一个用@Module()装饰器装饰的类。@Module()装饰器提供了(Nest组织应用程序结构的)元数据。
每个应用程序至少有一个模块,每个模块封装一组密切相关的功能。
@Module()装饰器接受单个对象,其属性描述了模块:
providers | 注入实例化的提供者,在这个模块中共享 |
controllers | 模块控制器集,须被实例化 |
imports | 导入模块列表 |
exports | 导入此模块的其他模块中可用的providers子集 |
模块默认封装提供者。意味着不可能注入提供者(既不是当前模块的直接组成部分,也不是从导入模块导出的)。可以将从模块导出的提供者视为模块的公共接口或API。
功能模块
功能模块组织与功能相关的代码,建立明确边界。有助于管理复杂的使用SOLID原则的项目
//cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
//app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
src
cats
dto
create-cat.dto.ts
interfaces
cat.interface.ts
cats.controller.ts
cats.module.ts
cats.service.ts
app.module.ts
main.ts
共享模块
模块默认是单例的,可以在多个模块之间共享任何提供者的同一个实例。
模块都是共享的,可以被任何模块重用。在其他几个模块之间共享CatsService的一个实例。将CatsService提供者添加到模块的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,与其他导入它的模块共享相同的CatsService实例。
模块重新导出
可以重新导出导入的模块,使它对导入该模块的其他模块可用。
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
依赖注入
模块类可以注入提供者(如,出于配置目的):
//cats.module.ts
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) {}
}
由于循环依赖,模块类本身不能作为提供者被注入。
全局模块
提供者封装在模块内。不先导入模块,就不能在其他地方使用模块的提供者。
提供一组在任何地方开箱即用的提供者(如,helper,数据库连接等),@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 {}
全局模块只注册一次,由根模块或核心模块注册。
动态模块
动态注册、配置提供者。动态模块介绍。
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
forRoot()方法可同步或异步的返回一个动态模块(如,通过Promise)。
该模块定义了Connection提供者(在@Module()装饰器元数据中),根据传递到forRoot()方法的entities 和options 对象,公开一个提供者集合,如,存储库。动态模块返回属性,扩展(不是覆盖)@Module()装饰器中定义的模块元数据。这是静态声明的Connection 提供者和动态生成的存储库提供者从模块导出的方式。
在全局作用域中注册一个动态模块,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({
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],
})
export class AppModule {}
中间件
路由处理之前调用。中间件可以访问 request 和 response 对象、请求-响应周期中的next()(下一个中间件函数)。下一个中间件函数通常由名为next的变量表示。
默认情况下,Nest中间件相当于 express 中间件。官方express文档描述的中间件功能:
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用栈中下一个中间件函数。
- 中间件没有结束请求-响应周期,须调用
next()将控制传给下一个中间件。否则,请求将被搁置。
Nest中间件(函数或带有@Injectable()装饰器的类)。类实现NestMiddleware接口,函数没有特殊要求。
//logger.middleware.ts
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();
}
}
依赖注入
中间件支持依赖注入。像提供者和控制器一样,能够注入在同一模块内可用的依赖项。通过“构造函数(constructor)”完成的。
应用中间件
中间件在模块类configure()方法中设置,不是在@Module()装饰器中。模块须实现NestModule接口。
//app.module.ts
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');
}
}
/cats路由处理程序设置了LoggerMiddleware中间件。
配置中间件时将包含路由path和请求method的对象传递给forRoutes()方法
//app.module.ts
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 });
}
}
configure()可用async/await
路由通配符
支持基于Pattern(模式)的路由。
forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
'ab*cd'路由路径匹配abcd, ab_cd, abecd等。?, +, *, 和 ()可以在路由路径中使用,对应正则表达式的子集。
fastify包使用path-to-regexp包的最新版本,不支持通配符星号*。须使用参数(如,(.*),:splat*)
中间件消费者(consumer)
MiddlewareConsumer 一个helper(助手)类。几个内建方法管理中间件,可以链式链接。forRoutes()方法接受一(多)个字符串、一个RouteInfo 对象、一(多)个控制器类,多数情况下,是以逗号分隔的控制器列表。
//app.module.ts
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 类非常简单。没有成员,没有其他方法,没有注入依赖项。可定义为函数。
// logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};
// app.module.ts
consumer
.apply(logger)
.forRoutes(CatsController);
函数中间件不能注入依赖项。类中间件通过“构造函数(
constructor)”注入依赖项
多个中间件
apply()中提供一个逗号分隔的中间件列表(顺序执行)
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
全局中间件
绑定每个注册的路由,使用INestApplication实例提供的use()方法:
// main.ts
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
全局中间件中不能访问DI(
Dependency Injection:依赖注入)容器。app.use()使用函数中间件。 若使用类中间件,在AppModule(或其他模块)中使用.forRoutes('*')。
异常过滤器
内置全局异常过滤器,负责应用未处理的异常。代码没处理异常时,它将捕获异常,发送友好响应。
内置全局异常过滤器处理HttpException类型(及其子类)的异常。当异常无法识别时(不是HttpException类型(及其子类)),内置异常过滤器会生成默认JSON响应:
{
"statusCode": 500,
"message": "Internal server error"
}
内置全局异常过滤器部分支持
http-errors库。抛出异常都包含statusCode、message属性并作为返回响应,不是默认的InternalServerErrorException(处理无法识别的异常)
抛出标准异常
内置的HttpException类,从@nestjs/common包中公开。基于HTTP REST/GraphQL API的应用发生错误时,发送标准HTTP响应对象。
//cats.controller.ts
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
客户端调用时,响应如下
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException(response, status)接受两个必填参数:
response字符串或对象。statusHTTP状态码.。
响应包含两个属性:
statusCode状态码message错误描述
第一个参数response(响应),传字符串(覆盖JSON响应体消息部分)。传对象(覆盖整个JSON响应体)。
第二个参数status(状态码),有效的HTTP状态码。使用从@nestjs/common导入的HttpStatus枚举。
第三个参数options(可选),提供错误原因。原因对象没有序列化到响应对象中,但对日志有用。
//cats.controller.ts
@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"
}
自定义异常
可以创建异常体系,自定义异常继承基础的HttpException类。Nest识别你的异常,自动处理错误响应。
//forbidden.exception.ts
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}
ForbiddenException扩展了HttpException,与内置异常无缝衔接。
//cats.controller.ts
@Get()
async findAll() {
throw new ForbiddenException();
}
内置HTTP异常
Nest提供一组继承自HttpException类的标准异常。从@nestjs/common包中公开,代表常见HTTP异常:
BadRequestExceptionUnauthorizedExceptionNotFoundExceptionForbiddenExceptionNotAcceptableExceptionRequestTimeoutExceptionConflictExceptionGoneExceptionHttpVersionNotSupportedExceptionPayloadTooLargeExceptionUnsupportedMediaTypeExceptionUnprocessableEntityExceptionInternalServerErrorExceptionNotImplementedExceptionImATeapotExceptionMethodNotAllowedExceptionBadGatewayExceptionServiceUnavailableExceptionGatewayTimeoutExceptionPreconditionFailedException
内置异常使用options参数 提供错误 原因 和 描述:
throw new BadRequestException('Something bad happened', { cause: new Error(), description: 'Some error description' })
下面是响应:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400,
}
异常过滤器
基本的(内置的)异常过滤器可以处理许多情况。完全控制异常层是设计异常过滤器的目的。
创建一个异常过滤器,负责捕获HttpException类型的异常,实现自定义响应。需要访问底层平台Request和Response对象。
//http-exception.filter.ts
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<T>接口。提供catch(exception: T, host: ArgumentsHost)方法。T表示异常的类型。
@Catch(HttpException)装饰器将所需的元数据绑定到异常过滤器,告诉Nest这个过滤器寻找类型为HttpException的异常,而不是其他类型的异常。@Catch()装饰器可以接受单个参数,也可接受逗号分隔的列表。允许同时为几种类型的异常设置过滤器。
参数host
catch()方法的参数。exception参数是正在处理的异常对象。host参数是一个ArgumentsHost对象。使用ArgumentsHost上的辅助方法获得所需的Request和Response对象。了解ArgumentsHost这里。
绑定过滤器
将新的HttpExceptionFilter绑定到CatsController的create()方法。
//cats.controller.ts
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@UseFilters()装饰器是从@nestjs/common包导入的。
@UseFilters()与@Catch()装饰器类似。接受单个或逗号分隔的过滤器实例。可以传递类(而不是实例),将实例化的责任留给框架。
//cats.controller.ts
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
最好使用类而不是实例。减少内存使用,可以在整个模块中重用同一个类的实例。
上例中,HttpExceptionFilter只应用于单个create()路由处理程序,具有方法作用域。异常过滤器可以在不同的级别上确定作用域:方法作用域、控制器作用域或全局作用域。例如,将过滤器设置为控制器作用域
//cats.controller.ts
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
每个在CatsController中定义的路由处理程序设置HttpExceptionFilter过滤器。
创建一个全局作用域的过滤器:
//main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
useGlobalFilters()方法不为网关或混合应用程序设置过滤器。
全局过滤器针对每个控制器和每个路由处理程序。从模块外部注册的全局过滤器(如上例,使用useGlobalFilters())不能注入依赖,因为他在任何模块上下文之外完成的。使用以下结构,可以从任何模块注册一个全局作用域过滤器:
//app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
在哪个模块中使用了这种构造,过滤器都是全局的。另外,
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 {
// 在某些情况下,' httpAdapter '可能在构造函数方法中不可用,因此我们应该在这里解析它。
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()方法。
//all-exceptions.filter.ts
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实例化。让框架自动实例化它们。
全局过滤器可以扩展基本过滤器。有两种方法可以做到这一点。
第一种方法:实例化自定义全局过滤器时注入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 如示【绑定过滤器】。
管道
用@Injectable()装饰器标注的类,实现了PipeTransform接口。
管道有两个典型的用例:
- transformation: 输入数据转换为所需形式(如,字符串到整数)
- validation[验证]: 评估输入数据,有效,简单通过不变; 否则,抛出异常
两种情况,管道都对路由处理程序arguments[参数]进行操作。调用方法之前插入一个管道,管道接收该方法的参数并对它们进行操作。此时发生转换或验证操作,之后使用转换后参数调用路由处理程序。
管道抛出异常时,将由异常层处理(应用到当前的任何异常过滤器)。
内置管道
提供了8个内置管道:
ValidationPipeParseIntPipeParseFloatPipeParseBoolPipeParseArrayPipeParseUUIDPipeParseEnumPipeDefaultValuePipe
从@nestjs/common包导出的。
使用ParseIntPipe。这是transformation的示例,确保参数被转换为整数(或转换失败抛出异常)。下面示例也适用其他内置转换管道(ParseBoolPipe, ParseFloatPipe, ParseEnumPipe, ParseArrayPipe和ParseUUIDPipe,称为Parse*管道)。
绑定管道
方法参数级别绑定管道:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
要么findOne()接收的参数是数字,要么调用路由处理程序之前抛出异常。
假设路由调用如下:
GET localhost:3000/abc
会抛出一个异常:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
该异常阻止findOne()方法的主体执行。
上例中,传递一个类(ParseIntPipe),不是实例。与守卫一样,可以传递一个实例。传递选项定制内置管道行为:
@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,可以在管道选项中传递版本。
参见验证技术获得验证管道的广泛示例
自定义管道【?】
获取输入值,返回相同的值。
//validation.pipe.ts
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接口契约。两个参数:
valuemetadata
value方法参数,metadata方法参数的元数据。元数据对象有以下属性:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
这些属性描述参数。
type | 指示参数是一个body @Body(), query @Query(), param @Param(), or a custom parameter(阅读更多这里)。 |
metatype | 参数的元类型,如String。路由方法中省略了类型声明,或使用普通的JavaScript,该值为undefined。 |
data | 传递给装饰器的字符串,如@Body('string')。装饰符括号为空,则为“未定义”。 |
接口在编译中消失。如果方法参数类型声明为接口而不是类,
metatype值将是对象。
基于模式的验证
让管道验证更有用。CatsController控制器的create()方法,运行service方法之前,想要确保post body对象是有效的。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Body()参数createCatDto,类型是CreateCatDto:
//create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
确保create方法传入请求包含有效主体。必须验证createCatDto 对象成员。可以在路由处理方法中做,但会破坏单一责任规则 (SRP)。
另种方法创建验证器类,并在那里委托任务。缺点是,必须在每个方法开头调用这个验证器。
验证中间件怎么样? 这可以工作,但不幸的是,不可能创建通用中间件,在应用程序所有上下文中使用。因为中间件不知道执行上下文,包括要调用的处理程序及其任何参数。
当然,这正是管道所设计的用例。让我们继续改进我们的验证管道。
对象模式验证
有几种方法以干净的DRY方式进行对象验证。一种常见的是使用基于模式的验证。让我们尝试这种方法。
Joi库使用可读的API创建模式。构建基于joi模式的验证管道。
首先安装所需的软件包:
$ npm install --save joi
$ npm install --save-dev @types/joi
下例中,创建了简单的类,将模式作为constructor 参数。然后应用schema.validate()方法,针对提供的模式验证传入参数。
如前所述,验证管道要么原封不动返回值,要么抛出异常。
下节中使用@UsePipes()装饰器为控制器方法提供适当的模式。这样可以使验证管道在上下文中可重用。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
绑定验证管道
前面,看到了如何绑定转换管道(如ParseIntPipe 和Parse*管道的其余部分)。
绑定验证管道也非常简单。
本例中,在方法级别绑定管道。做以下事情来使用JoiValidationPipe:
- 创建一个
JoiValidationPipe实例 - 在管道类构造函数中传递特定于上下文的
Joi模式 - 将管道绑定到方法
使用@UsePipes()装饰器来做到这一点,如下所示:
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@UsePipes()装饰器是从@nestjs/common包中导入的。
类验证器
**警告 **本节中需要
TypeScript,如果应用使用普通JavaScript编写,则不可用。
验证技术的另一种实现。
class-validator。这个库基于装饰器验证。功能非常强大,特别与Pipe结合时,因为可以访问metatype。安装所需的包:
$ npm i --save class-validator class-transformer
向CreateCatDto 类中添加装饰器。优势:CreateCatDto类仍然是 Post body object 的真实来源(而不必创建一个单独的验证类)。
//create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
阅读更多关于
class-validator装饰器这里。
创建一个使用这的ValidationPipe类。
//validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } 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 = plainToClass(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);
}
}
上面,使用了class-transformer库。是由
class-validator库作者编写,它们可以配合使用。
注意transform()方法标记为async,class-validator验证可以是async(利用Promises)。
接下来注意,提取metatype字段。
注意函数toValidate()。当参数是原生JavaScript类型时,它负责绕过验证步骤
接下来,使用plainToClass()将纯JavaScript参数对象转换为类型化对象,以便应用验证。
最后,由于这是一个验证管道,要么原封不动返回值,要么抛出异常。
最后一步绑定ValidationPipe。管道可以是(参数、方法、控制器或全局)作用域。下例中,管道绑定到路由处理程序@Body()装饰器。
//cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
验证只涉及一个指定参数时,参数作用域管道非常有用。
全局作用域管道
ValidationPipe创建为尽可能通用,设置全局作用域管道实现全部效用,应用到每个路由处理程序。
//main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
混合应用程序
useGlobalPipes()方法不为网关和微服务设置管道。“标准”(非混合)微服务应用程序,useGlobalPipes()可用。
注意,在依赖注入方面,从任何模块外部注册的全局管道(使用useGlobalPipes(),如上例)不能注入依赖项,因为绑定已经在任何模块的上下文中完成。为了解决这个问题,直接从任何模块建立一个全局管道,使用下面的结构:
//app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
内置的ValidationPipe
不必构建通用验证管道,内置的ValidationPipe提供更多选项,这里完整的细节和大量的例子。
转换用例
一个简单ParseIntPipe,字符串解析为整数。(有个内置的更复杂的ParseIntPipe; 下例为自定义转换管道的简单示例)。
//parse-int.pipe.ts
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;
}
接收输入值id返回输出值(UserEntity对象)。
提供默认值
Parse*管道需要参数值。接收到null or undefined时抛出异常。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 });
}
守卫
守卫是用@Injectable()装饰器标注的类,实现了CanActivate接口。
守卫只有一个职责。决定请求是否由路由程序处理,取决于(如权限、角色、ACLs等)。称为授权(authorization)。
守卫在中间件之后执行,在拦截器、管道之前执行。
授权守卫
已验证的用户(令牌附加到请求头)。
//auth.guard.ts
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 or Observable)。
执行上下文
canActivate()只有一个参数,ExecutionContext实例。ExecutionContext继承自ArgumentsHost。上例中,使用了ArgumentsHost上的switchToHttp方法,获得Request对象的引用。更多,可回到 exception filters 章节 Arguments host 部分。
通过扩展ArgumentsHost,ExecutionContext也添加一些新的helper方法,提供额外细节。有助于构建更通用的守卫,更多这里。
基于角色的认证
构建更强大的守卫,允许特定角色访问。从一个基本的守卫模板开始,接下来对其进行构建。目前,允许所有请求:
//roles.guard.ts
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 {}
全局守卫,使用Nest应用实例的useGlobalGuards()方法:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
混合应用程序(
hybrid application),useGlobalGuards()默认不会为网关、微服务设置守卫(更改此行为,参阅混合应用程序)。“标准”(非混合)微服务应用,useGlobalGuards()全局作用域设置守卫。
全局守卫用于每个路由。依赖注入方面,模块外部注册的全局守卫(如上useGlobalGuards())不能注入依赖。从任何模块建立全局守卫:
//app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
这种方法为守卫执行依赖注入时,无论在哪个模块中,守卫实际上是全局的。
每个处理程序设置角色
RolesGuard还不够智能。没有利用守卫特性 - 执行上下文。
灵活、可重用的将角色与路由匹配? 自定义元数据 发挥作用(更多这里)。通过@SetMetadata()将自定义元数据附加到路由处理程序。
//cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@SetMetadata()装饰器是从@nestjs/common包中导入的。
roles元数据(roles键,['admin']值)附加到create()方法。路由中直接使用@SetMetadata()不好。可创建自定义装饰器,如下:
//roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
更清晰,可读性更强,是强类型。自定义@Roles()装饰器。
//cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
把它放在一起
与RolesGuard结合。获取路由的角色(自定义元数据),使用Reflector辅助类,@nestjs/core中公开。
//roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
假设
request.user包含角色。能在自定义认证守卫(或中间件)中建立关联。查看本章了解更多。
matchRoles()根据需要简单或复杂。
反射器(Reflector)的更多细节,参阅Execution context章节的反射(Reflection)和元数据(metadata)部分。
没有权限用户请求端点时,自动返回以下响应:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
守卫返回false时,框架会抛出ForbiddenException。希望返回不同的错误响应,应该抛出特定异常。
throw new UnauthorizedException();
守卫抛出的异常都由异常层(exceptions layer)处理(全局异常过滤器、当前上下文异常过滤器)。
授权示例,查看本章。
拦截器
拦截器是一个带有@Injectable()装饰器的类,实现了NestInterceptor接口。
拦截器有一组有用的功能,这些功能受到面向切面编程 (AOP)技术的启发。
- 在方法执行之前/之后绑定额外的逻辑
- 转换函数返回的结果
- 转换函数抛出的异常
- 扩展基本函数行为
- 完全覆盖一个函数根据特定条件(如,为了缓存目的)
基础
实现intercept()方法,两个参数。第一个ExecutionContext实例(与 guards 相同)。ExecutionContext继承ArgumentsHost。
调用处理程序
第二个参数CallHandler。CallHandler接口实现了handle()方法,使用它在拦截器中调用路由处理程序。intercept()中不调用handle(),不会执行路由处理程序。
intercept()方法包装了请求/响应流。intercept()中编写代码,调用handle()之前执行,如何影响之后发生的事情?handle()返回一个Observable,使用RxJS操作符操作响应。handle()被称为切入点(Pointcut)。
切面拦截
使用拦截器来记录用户交互(例如,存储用户调用、异步分派事件或计算时间戳)。
//logging.interceptor.ts
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<T>的类型(支持响应流),R是Observable<R>包装的值的类型。
拦截器,控制器,提供者,守卫等,可以通过
constructor注入依赖。
handle()返回一个RxJS Observable,有很多操作流的操作符。上例,使用了tap()操作符,它在可观察流正常或异常终止时调用匿名日志记录函数,但不会干扰响应周期。
绑定拦截器
设置拦截器,使用@nestjs/common中的@UseInterceptors()装饰器。与pipes和guards一样,可以是(控制器、方法、全局)范围。
//cats.controller.ts
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
调用GET /cats端点时,看到以下输出:
Before...
After... 1ms
也可以传递一个实例:
//cats.controller.ts
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
全局拦截器,使用 useGlobalInterceptors()方法:
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
全局拦截器用于每个路由程序。在赖注入方面,使用useGlobalInterceptors(),不能注入依赖项,这是在任何模块上下文之外完成的。下面结构从任何模块中建立一个全局拦截器:
//app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
响应映射
handle()返回Observable。该流包含从路由程序返回的值,RxJS的map()操作符轻松地更改它。
响应映射不适用于
library-specific的响应策略(禁止使用@Res()对象)。
RxJS的map()操作符将响应对象分配给新对象的data属性,返回新对象。
//transform.interceptor.ts
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 => {
return { data }
})
);
}
}
intercept()前可添加异步async。
响应如下,路由返回[]
{
"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 )
);
}
}
异常映射
catchError()操作符覆盖抛出的异常:
//errors.interceptor.ts
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 => {
return throwError(() => {
return new BadGatewayException()
})
}),
);
}
}
流覆盖
阻止调用处理程序。如,从缓存中返回值的缓存拦截器。
//cache.interceptor.ts
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();
}
}
RxJS的of()操作符创建新流。
更多操作符
RxJS操作符控制流为我们提供许多功能。处理路由请求的超时。
//timeout.interceptor.ts
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);
}),
);
};
};
可以在抛出RequestTimeoutException之前添加逻辑(如释放资源)。
自定义路由装饰器
Nest围绕装饰器构建的。更好地理解装饰器,阅读这篇文章。简单的定义:
ES2016装饰器是返回函数的表达式,可接受目标、名称和属性作为参数。前加@应用它。可为类、方法或属性定义装饰器。
参数装饰器
一组参数装饰器,与路由程序一起使用。装饰器和代表的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 |
属性user附加到request对象。创建自定义@User()装饰器,在路由程序中提取它。
//user.decorator.ts
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对象未创建)。
//user.decorator.ts
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参数执行管道。管道应用到自定义装饰器:
@Get()
async findOne( @User(new ValidationPipe({ validateCustomDecorators: true })) user: UserEntity ) {
console.log(user);
}
validateCustomDecorators选项须设置为true。ValidationPipe默认不验证自定义装饰器注释的参数。
装饰器组合
一个方法组合多个装饰器。
//auth.decorator.ts
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函数。