Nestjs
起步
// 创建项目
npm i -g @nestjs/cli
nest new project-name
// 快速创建模块
nest -h
nest g res module/Co
控制器
路由
import { Controller, Get } from '@nestjs/common';
import { CoService } from './co.service';
@Controller('co')
export class CoController {
constructor(private readonly coService: CoService) {}
// GET /co
@Get()
// 路由与处理函数命名无关
getHello(): string {
// 当请求返回一个 JavaScript 对象或者数组时,他将自动序列化为 JSON
// 返回类型为基本类型(string、number、boolean),nest 只发送值
return 'hello';
}
// Nest 会检测程序使用 @Res 和 @Next,表明你选择了特定的库。
// 如果在一个处理函数上同时使用了这两个方法,那么此处的标准方式就是自动禁用此路由, 你将不会得到你想要的结果。
// 如果你在处理函数上使用这两种方法,必须将装饰器 @Res({ passthrough: true }) 中将 passthrough 选项设为 true。
@Get('res')
getHelloRes(@Res({ passthrough: true }) res: Response) {
// res.status(200).send('123');
console.log(res);
return '456';
}
@Get('req')
// @Req 获取请求对象
getReqInfo(@Req() req: Request) {
console.log(req);
return 'req';
}
}
Nest 提供许多装饰器及其代表的底层对象
方法 | 代表对象 |
---|---|
@Request(), @Req() | req |
@Response, @Res() | res |
@Next() | next |
@Session() | req.session |
@Param(Key?: string) | req.params/req.params[key] |
@Body(Key?: string) | req.body/req.body[key] |
@Query(Key?: string) | req.query/req.query[key] |
@Headers(Name?: string) | req.headers/req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
资源
Nest 为所有标准的 HTTP 方法提供了相应的装饰器:@Get()
、@Post()
、 @Put()
、@Delete()
、@Patch()
、@Options()
、以及 @Head()
。此外,@All()
则用于定义一个用于处理所有 HTTP 请求方法的处理程序。
路由通配符
// 路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。
@Get('ab*cd')
getUrl(@Req() req: Request) {
return req.url;
}
状态码
// 通常状态码并不是固定的,而是取决于各种因素。
@Get('code')
@HttpCode(204)
getCode(@Req() req: Request) {
console.log(req);
return 'req';
}
Headers
// 自定义响应头
@Get('header')
@Header('name', 'fwb')
getHeader(@Res() res: Response) {
console.log(res.headers);
return 'req';
}
重定向
// 将响应重定向到特定的 URL
@Get('redirect')
// @Redirect() 装饰器有两个参数,url 和 statusCode,省略 statusCode 默认为 302
@Redirect('https://docs.nestjs.com', 302)
getRedirect() {
return 'redirect';
}
路由参数
接受动态数据 作为请求的一部分 (GET /co/1)
来获取 id
为 1
的值,可以在路由路径中添加路由参数 **标记(token)**来获取动态值。
@Get(':id')
getParams(@Param('id') id: string) {
return 'id is ' + id;
}
子路由
@Controller
装饰器可以接受一个 host
选项,以要求传入请求的 HTTP
主机匹配某个特定值。
Fastify 缺乏对嵌套路由的支持,不支持子路由
异步性
Nest 完美支持异步函数,每个异步函数都必须返回一个Promise
,Nest 会自行进行解析。
@Get()
async findAll(): Promise<any[]> {
return [];
}
Nest 也可以返回 RxJS observable 流
。
// TODO: 暂时不明白这个 RxJS
@Get()
findAll(): Observable<any[]> {
return of([]);
}
负载请求
我们可以定义一个DTO
,来作为对数据结构的规则,它是一个对象。可以使用 TypeScript 接口或者 ES6 类。建议使用ES6 类,因为接口在编译时会被抹去。
// co.dto.ts
export class CreateCoDto {
readonly name: string;
readonly age: number;
}
// co.controller.ts
@Post()
setCo(@Body() body: CreateCoDto) {
return 'my name is ' + body.name;
}
提供者
服务
控制器处理 HTTP 请求,应将开发逻辑交于提供者 (providers)
。Prociders
是一个纯粹的 JavaScript
类,在其类声明之前带有 @Injectable()
装饰器。
import { Injectable } from '@nestjs/common';
@Injectable()
export class CoService {}
// co.service.ts
import { Injectable } from '@nestjs/common';
import { CreateCoDto } from './co.dto';
@Injectable()
export class CoService {
private readonly Co: CreateCoDto[] = [];
create(co: CreateCoDto) {
this.Co.push(co);
}
findAll() {
return this.Co;
}
}
// co.controller.ts
import { Body, Controller ,Get, Post } from '@nestjs/common';
import { CoService } from './co.service';
import { CreateCoDto } from './co.dto';
@Controller('co')
export class CoController {
// coService 是通过类构造函数注入,使用只读。这意味着我们已经在同一位置创建并初始化了 coService 成员。
constructor(private readonly coService: CoService) {}
@Post()
setCo(@Body() createCoDto: CreateCoDto) {
this.coService.create(createCoDto);
return 'ok';
}
@Get('all')
getCoAll() {
return this.coService.findAll();
}
}
依赖注入
Nest
将 coService
通过创建并返回一个实例来解析 CoService`(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):
constructor(private readonly coService: CoService) {}
作用域(TODO:不懂)
Provider 通常具有与应用程序生命周期同步的生命周期("作用域")。在启动应用程序时,必须解析每个依赖项,因此必须实例化每个提供者。同样,应用程序关闭时,每个 provider 都将被销毁。但是有一些方法可以改变 provider 的生命周期。
可选提供者
TODO: 没懂,应该是可以选择性的注入依赖。
import { Inject, Injectable, Optional } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private readonly HttpService: T) {}
}
HttpService
类可接受一个泛型参数 T
,用于表示注入的 HTTP 客户端的类型。构造函数中使用了 @Optional()
装饰器来标记 HTTP 客户端为可选的,使用 @Inject('HTTP_OPTIONS')
装饰器来注入标记为 'HTTP_OPTIONS'
的依赖,这可以用于传递 HTTP 客户端的配置选项。
属性注入
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
如果顶级类依赖于一个或多个 providers,那么通过从构造函数中调用子类中的 super()
来传递它们就会非常烦人了。因此,为了避免出现这种情况,可以在属性上使用 @Inject()
装饰器。
模块
模块是具有 @module
的装饰器的类。@module()
装饰器提供了元数据,Nest 用他来组织应用程序结构。
每个 Nest 应用至少要有一个模块,即根模块
@module()
装饰器接受一个描述模块属性的对象:
name | desc |
---|---|
providers | 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享。 |
controllers | 必须创建的一组控制器。 |
imports | 导入模块的列表,这些模块导出了此模块中所需的提供者。 |
exports | 由本模块提供并应用在其他模块中可用的提供者的子集。 |
共享模块
在 Nest 中每个模块都是一个单例,可以轻松的在多个模块之间使用同一个提供者的实例。
每一个模块都是共享模块。一旦被创建就能被任意模块使用。
模块导入/导出
import { Module } from '@nestjs/common';
import { MoService } from './mo.service';
import { MoController } from './mo.controller';
import { CoModule } from '../co/co.module';
import { CoService } from '../co/co.service';
@Module({
imports: [CoModule], // 导入其他模块
controllers: [MoController],
providers: [MoService, CoService],
})
export class MoModule {}
在 MoModule
中导入 CoModule
作用:
- 更加明确了各模块之间的关系。
- 如果
MoModule
中使用了CoService
,可以单独注入。但如果CoService
使用到了其他的 service 就需要继续注入。但如果使用imports
导入,nest 就会自动帮你分析依赖关系,自动帮你引入。
依赖注入
提供者也可以注入到模块(类)中,(例如用于配置目的):
import { Module } from '@nestjs/common';
import { CoService } from './co.service';
import { CoController } from './co.controller';
@Module({
controllers: [CoController],
providers: [CoService],
})
export class CoModule {
constructor(private readonly coService: CoService) {}
}
但是,由于循环依赖性,模块类不能注入到提供者中。
全局模块
如果你的模块被多次相同的引用,可以注册为全局模块,这样就不用多次的 imports
导入。例如 helper、数据库连接。
@Global
装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。
import { Module, Global } from '@nestjs/common';
import { CoService } from './co.service';
import { CoController } from './co.controller';
@Global
@Module({
controllers: [CoController],
providers: [CoService],
})
export class CoModule {
constructor(private readonly coService: CoService) {}
}
动态模块
// 动态模块
import { DynamicModule, Module } from '@nestjs/common';
import { ConnectionProvider } from './connection.provider';
import { DatabaseProviders } from './database.providers';
@Module({
providers: [ConnectionProvider],
})
export class DatabaseModule {
static forRoot(entities: [], options?: any): DynamicModule {
const providers = DatabaseProviders(entities, options);
return {
// 如果注册为全局模块
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
// 注册动态模块
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 {}
中间件
中间件是在路由处理程序之前调用的函数。中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next()
中间件函数。 next()
中间件函数通常命名为 next()
的变量表示。
中间件功能:
- 执行任何代码
- 对请求和响应对象进行更改
- 结束请求-响应周期
- 调用堆栈中的下一个中间件函数
- 如果当前的中间件函数没有结束请求-响应周期,他必须调用
next()
将控制传递给下一个中间件函数。否则,请求将会被挂起
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
依赖注入
Nest
中间件完全支持依赖注入。就像提供者和控制器一样,他们能够注入属于统一模块的依赖项
应用中间件
中间件不能在 @Module()
装饰器中列出。我们必须使用模块类的 configure()
的方法设置他们。包含中间件的模块必须实现 NestModule
接口。我们将 LoggerMiddleware
设置在 AppModule
层上。
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoModule } from './module/co/co.module';
import { MoModule } from './module/mo/mo.module';
import { DatabaseModule } from './module/database/database.module';
import { LoggerMiddleware } from './middleware/logger.middleware';
@Module({
imports: [CoModule, MoModule, DatabaseModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}
在配置中间件时将包含路由路径的对象和请求方法传递给 forRoutes()
方法。forRoutes(mo/all)
,这样当我们请求路由 mo/all
时就会使用到中间件 LoggerMiddleware
,当forRoutes(*)
时,每个路由都会被应用。
我们还可以通过 forRoutes({ path: 'cats', method: RequestMethod.GET });
限制请求的方法。
路由通配符
路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。
中间件消费者
MiddlewareConsumer
是一个帮助类。他提供了几种内置方法来管理中间件。他们都可以被简单的连接起来。forRoutes()
可以接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类。在大多数情况下,您可能会传递一个由逗号分割的控制器类。
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer.apply(LoggerMiddleware).forRoutes(MoController);
}
}
该 apply()
方法可以使用单个中间件,也可以使用多个参数来指定多个中间件**。**
// exclude() 可以轻松的排除某些路由。可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
函数中间件
LoggerMiddleware
类非常简单。它没有成员,没有额外的方法,没有依赖关系。因此可以简单的抽离成一个函数。
// 函数中间件
import { NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
使用方法一致。
多个中间件
// 多个中间件使用逗号分隔
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
全局中间件
将中间件注册到每一个路由中。
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
异常过滤器
内置的异常层负责处理整个应用程序中的所有抛出异常。当捕获到未处理异常时,最终用户将收到友好的响应。
开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理异常类型 HttpException
(及其子类)的异常。每个发生的异常都由全局异常过滤器处理,当这个异常无法识别时(既不是 HttpException
也不是继承类 HttpException
),用户将会收到一下响应。
{
"statusCode": 500,
"message": "Internal server error"
}
基础异常类
Nest
提供了一个内置的 HttpException
类。对于典型的 HttpException
REST/GrapQL
API程序,最佳实践是在发生某些错误情况时发送的标准HTTP响应对象。
@Get()
async findAll() {
// 传入一个 string 将会仅覆盖 message 内容
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
客户端响应
{
"statusCode": 403,
"message": "Forbidden"
}
HttpException
的构造函数有两个必要的参数来决定响应:
response
参数定义JSON
响应体。它可以是string
或object
,如下所述。status
参数定义HTTP
。
默认情况下,JSON
响应主体包含两个属性:
statusCode
:默认为status
参数中提供的Http
状态码。message
: 基于状态的HTTP
错误简述。
仅覆盖 JSON
响应主体的消息部分,请在 response
参数中提供一个 string
。
要覆盖整个 JSON
响应主体,请在 response
参数中传递一个 object
。 Nest
将序列化对象,并将其作为 JSON
响应返回。
参数 status
是有效的 HTTP
状态码。最佳方法是使用 HttpStatus
枚举引入。
@Get()
async findAll() {
// 传入一个 object 将会仅覆盖整个响应主体
@Get('all')
getCoAll() {
throw new HttpException(
{
code: HttpStatus.FORBIDDEN,
error: 'Forbidden',
},
HttpStatus.FORBIDDEN,
);
}
}
客户端响应
{
"code": 403,
"error": "Forbidden"
}
自定义异常
自定义异常可以创建自己的异常层次结构。自定义异常继承于 HttpException
基类。Nest
可以识别你的异常,并自动处理异常响应。
// 继承于 HttpException,实现一个自定义异常
import { HttpException, HttpStatus } from '@nestjs/common';
export class ForbiddenException extends HttpException {
constructor() {
super('forbidden', HttpStatus.FORBIDDEN);
}
}
// 使用自定义异常
@Get('all')
getCoAll() {
throw new ForbiddenException();
}
内置HTTP异常
Nest 继承 HttpException
实现了许多异常。
BadRequestException
UnauthorizedException
NotFoundException
ForbiddenException
NotAcceptableException
RequestTimeoutException
ConflictException
GoneException
PayloadTooLargeException
UnsupportedMediaTypeException
UnprocessableException
InternalServerErrorException
NotImplementedException
BadGatewayException
ServiceUnavailableException
GatewayTimeoutException
异常过滤器
基本(内置)过滤器也可以解决许多问题,但有时候你希望对异常层有完全控制权 ,例如,基于某些动态因素添加日志记录或使用不同的 JSON
模式。异常过滤器 正是可以解决整个问题,可以控制精确的控制流以及将响应的内容发送客户端。
创建一个异常过滤器,他负责捕获作为 HttpException
类实例的异常,并为他设置自定义响应逻辑。因此我们需要访问底层平台的 Request
Response
。我们将访问 Request
对象,以便提取原始 url 并将包含在日志信息中。我们将使用 Response.json()
方法,使用 Response
对象直接控制发送的响应。
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
import { Response } from 'express';
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): any {
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: Exception, host: ArgumentsHost)
方法。T表示异常的类型。
@Catch()
装饰器绑定所需的元数据到异常过滤器上。他告诉 Nest
这个特定的过滤器正在寻找 HttpException
而不是其他的。@Catch()
可以传递多个参数,可以通过逗号分隔多个类型的异常设置过滤器。
参数主机
catch()
方法两个参数 exception
和 host
。exception
参数是当前正在处理的异常对象。 host
参数是一个 ArgumentsHost
对象。 ArgumentsHost
是一个功能强大的应用程序对象。我们可以用他来回去 response
和 request
对象的引用,这些对象被传递给原始请求处理程序(在异常发生的控制器中)。ArgumentsHost
可以在所有上下文中使用。
绑定过滤器
@Get('all')
// 可以绑定多个过滤器,使用逗号分隔过滤器实例列表
// 仅针对 getCoAll() 单个路由程序
// 过滤器的控制范围可以分为方法范围,控制器范围或全局范围
// 这种方式属于方法范围
// @UseFilters(new HttpFilter())
@UseFilters(HttpFilter)
getCoAll() {
throw new ForbiddenException();
}
// 控制器范围
@Controller('co')
@UseFilters(new HttpFilter())
export class CoController {
constructor(private readonly coService: CoService) {}
}
// 全局范围 -- 全局过滤器
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpFilter());
await app.listen(3000);
}
bootstrap();
全局过滤器用于整个应用程序、每个控制器和每个路由处理程序。
// 使用依赖注入,注册全局过滤器
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpFilter,
},
],
})
export class AppModule {}
捕获异常
// 表示捕获 HttpException 类型的异常
// @Catch() 表示捕获所有异常
@Catch(HttpException)
export class HttpFilter implements ExceptionFilter {}
// 过滤器将捕获抛出的每个异常,而不管其类型(类)如何。
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
管道
管道是具有 @Injectable()
装饰器的类。管道应实现 PipeTransform
接口。
管道的两个应用场景:
- **转化:**管道将输入数据转化为所需的数据输出(例如,将 string 转为 number)
- **验证:**对输入数据进行验证,如果验证成功继续传递;验证失败则抛出异常
在这两种情况下,管道 参数(arguments)
会由控制器的路由处理程序进行处理。Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转化或者验证处理,然后用转化好或者验证好的参数调用原方法。
当 Pipe 发生异常时,controller 不会执行任何方法。
Nest 的内置管道:
ValidationPipe
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
绑定管道
@Get(':id')
getCoById(@Param('id', ParseIntPipe) id) {
return this.moService.findOne(id);
}
这确保了我们在 findOne()
方法中接收的参数是一个数字(与 this.moService.findOne()
方法的诉求一致),或者在路由处理程序被调用之前抛出异常。
@Get(':id')
// 传递一个实例而非类,可以用来自定义管道的行为
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.moService.findOne(id);
}
自定义管道
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata): any {
return value;
}
}
PipeTransform<T, R>
是每个管道必须实现的泛型接口。泛型T
表示输入的 value 的类型泛型
R
表示transform()
的返回值。
transform()
有两个参数:
value
metadata
value
参数是当前处理的方法参数(在被路由处理程序方法接收之前)
metadata
是当前处理的方法的元数据。
// metadata 包括以下属性
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string
}
类验证器
Nest 与 class-validator 配合得很好。这个优秀的库允许使用基于装饰器的验证。
npm i --save class-validator class-transforme
安装完成后,我就可以向 DTO
类添加一些装饰器。
export class CreateCoDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
}
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class ValidationPipe implements PipeTransform {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 验证 metatype
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const error = await validate(object);
if (error.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);
}
}
plainToInstance()
方法将普通的 JavaScript 参数对象转换为可验证的类型对象。
使用 import { ValidationPipe } from '@nestjs/common';
也能达到相同的效果。
{
"message": [
"age must be an integer number"
],
"error": "Bad Request",
"statusCode": 400
}
全局管道
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
也可以采用依赖注入的方式
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe
}
]
})
export class AppModule {}
守卫
守卫是一个使用 @Injectable()
装饰器的类。守卫实现 CanActivate
接口。
守卫有一个独立的职责。他们会根据运行时出现的某种条件(例如权限、角色、访问控制列表等)来确定给定的请求是否由路由程序处理。
守卫在每个中间件之后执行,或者拦截器和管道之前执行。
授权守卫
授权是守卫的一大重要用例。只有当用户或特定身份具有足够权限时才能够使用特定的路由。
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()
函数。返回返回一个 Boolean,用于指示是否允许当前请求。他可以通过同步或异步的返回响应(通过 Promise
或 Observable
)。
- 如果返回
true
,将处理用户调用。 - 如果返回
false
,则 Nest 会忽略当前请求。
执行上下文
ExecutionContext
继承于 ArgumentsHost
。并且提供更多的功能。
绑定守卫
// 可以给每个路由处理函数绑定守卫,也可以绑定控制器
@Get(':id')
@UseGuards(AuthGuard)
getCoById(@Param('id', ParseIntPipe) id) {
return this.moService.findOne(id);
}
// 也可以通过注入实例而不是类来绑定守卫
@Get(':id')
@UseGuards(new AuthGuard())
getCoById(@Param('id', ParseIntPipe) id) {
return this.moService.findOne(id);
}
// 注册一个全局守卫
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
// 通过依赖注入的方式,注册全局守卫
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
为每个处理器设置角色
Nest
通过 @SetMatedata()
来给每个处理器设置角色。
@Get(':id')
@SetMetadata('roles', ['admin'])
@UseGuards(AuthGuard)
getCoById(@Param('id', ParseIntPipe) id) {
return this.moService.findOne(id);
}
// roles.decorator.ts 我们可以创建自己的装饰器来接收自定义的角色
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
拦截器
拦截器是使用 @Injectable()
装饰器注解的类。拦截器应该实现 NestInterceptor
的接口。
拦截器具有一系列功能
- 在函数执行之前/之后绑定额外逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展函数的基本行为
- 根据所选条件完全重写函数
基础
每个拦截器都有 intercept()
方法。他有两个参数:第一个参数 ExecutionContext
,ExecutionContext
继承自 ArgumentsHost
。第二个参数 CallHandler
,如果不手动调用 handle()
那么主程序不会进行求值,CallHandler
是一个包装执行流的对象,因此推迟了最终的处理程序执行。
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
export class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<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>
中的值的返回类型。
绑定拦截器
与守卫一样,拦截器可以是控制器范围内的,方法范围内的或者全局范围内的。
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
同样的,可以选择注入实例,而不是类来绑定拦截器
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
绑定全局拦截器
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
使用依赖注入的方式,绑定全局拦截器
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
响应映射
使用 map()
方法,对你的响应数据进行映射,返回你所需要的响应内容。
import {
NestInterceptor,
Injectable,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
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, // 数据
status: 0, // 接口状态值
extra: {}, // 拓展信息
message: '操作成功!', // 异常信息
success: true, // 接口业务返回状态
};
}),
);
}
}
异常映射
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())),
);
}
}
自定义装饰器
在代码中,我们可能多次需要使用到 req.url,为了使代码更具有可读性和透明性,我们可以创建一个
@User()
装饰器来帮助我们获取到他。
const user = req.url;
// url.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Url = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<Request>();
return request.url;
},
);
使用自定义装饰器
@Get('all')
getCoAll(@Url() url) {
console.log('url', url);
return this.coService.findAll();
}
传递数据
通过 data 参数,获取到传递的数据
// url.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const Url = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
console.log(data);
const request = ctx.switchToHttp().getRequest<Request>();
return request.url;
},
);
@Get('all')
getCoAll(@Url('http://xxxx') url) {
console.log('url', url);
return this.coService.findAll();
}
装饰器聚合
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' })
);
}
使用 applyDecorators()
方法,可以聚合多个装饰器,但也有部分装饰器由于自身特性没法进行聚合。
自定义提供者
依赖注入
依赖注入是一种控制反转(IoC
) 技术,您可以将依赖的实例化委派给IoC
容器。
首先,我们定义一个提供者。@Injectable()
装饰器将 CatsService
类标记为提供者。
标准提供者
@Module({
controllers: [CoController],
providers: [CoService],
})
providers属性接受一个提供者数组。到目前为止,我们已经通过一个类名列表提供了这些提供者。实际上,该语法providers: [CoService]
是更完整语法的简写:
providers: [
{
provide: CoService,
useClass: CoService
}
]
现在我们看到了这个显式的构造,我们可以理解注册过程。在这里,我们明确地将令牌 CoService
与类 CoService
关联起来。简写表示法只是为了简化最常见的用例,其中令牌用于请求同名类的实例。