什麼是 AOP
Springboot 中就存在 AOP 切面程式設計。在 Nest 中也同樣提供了該能力。
通常,一個請求過來,可能會經過 Controller(控制器)、Service(服務)、DataBase(資料庫訪問)的邏輯。
在這個流程中,若想要新增一些通用的邏輯,比如 日誌記錄、許可權控制、異常處理等作為一個通用的邏輯。
AOP 的目的就是將那些與業務無關,卻需要被業務所用的邏輯單獨封裝,以減少重複程式碼,減低模組之間耦合度,以方便專案的迭代與維護。
Nest 中的 AOP 有五種:Middleware
中介軟體、Guard
守衛、Pipe
管道、Interceptor
攔截器、ExceptionFilter
異常過濾器。
Middleware
Nest 中文網-中介軟體
全域性中介軟體
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(function (req: Request, res: Response, next: NextFunction) {
console.log('before-----', req.url);
next();
console.log('after-----');
});
await app.listen(3000);
}
當訪問 http://localhost:3000/person 時,則會觸發列印,同時,訪問其它請求地址同樣會觸發該中介軟體邏輯,這就是全域性中介軟體,可以在多個 handler 之間複用中介軟體的邏輯。
路由中介軟體
Nest 中除了全域性中介軟體,還有路由中介軟體。
建立一個路由中介軟體
# --no-spec 不生成測試檔案,--flat 平鋪,不生成目錄
nest g middleware log --no-spec --flat
在建立出的檔案中列印以下前後
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('before2', req.url);
next();
console.log('after2');
}
}
然後在 AppModule 中啟用該中介軟體
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LogMiddleware).forRoutes('person*');
}
}
其中 forRoutes
中就是指定該中介軟體在匹配到哪些路由時可以生效
路由中介軟體可以明確或者泛指定哪些路由可以使用該中介軟體,其餘是無法觸發該中介軟體的
全域性中介軟體則是所有的路由都會觸發該中介軟體,沒有任何約束
Guard
Guard 是路由守衛的意思,可以用於在呼叫某個 Controller 之前判斷許可權,返回 true 或者 false 來決定是否放行。
建立一個 Guard
nest g guard login --no-spec --flat
Guard 需要實現 CanActivate 介面,實現 canActivate 方法,可以從 context 拿到請求的資訊,然後做一些許可權驗證等處理之後返回 true 或者 false。
簡單使用
對生成的 guard 檔案內容返回 false,
@Injectable()
export class LoginGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// 其中 context 是該此請求呼叫的執行上下文
// 比如 context.getClass(), context.getHandler() 得到的列印就是 [class PersonController] [Function: findAll]
console.log('guard log');
return false;
}
}
其中 context
引數是執行上下文,可以用來獲取此次呼叫的上下文資訊,比如context.getClass()
context.getHandler()
,訪問 /person
,得到的列印就是如下圖所示內容,其中 context.getClass()
獲取當前路由的類,context.getHandler()
可以獲取到路由將要執行的方法
在某個請求(此處為 PerosnController 的 /person 路由
)的 Controller 上新增 @UseGuards(LoginGuard)
然後訪問 http://localhost:3000/person ,請求報錯,並返回無許可權的提示資訊
就像這樣,Controller 本身不需要做啥修改,只需要加一個裝飾器,就加上了許可權判,這就是 AOP 架構的好處。
而且,就像 Middleware 支援全域性級別和路由級別一樣,Guard 也可以全域性啟用
全域性守衛
guard 的全域性守衛有兩種新增方式,可以在 main.ts
的 app 中新增,也可以在 AppModule 中新增,但是兩者之間在使用上是有一些不同。
main.ts 中新增全域性守衛
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 新增全域性守衛
app.useGlobalGuards(new LoginGuard());
await app.listen(3000);
}
此時由於 LoginGuard
中直接寫死返回 false,所以訪問任何一個路由都是 403 無權
AppModule 中新增全域性守衛
在 AppModule 中新增時,需要注意,全域性守衛是在 providers
中進行關聯,且其 proovide
提供的 token,必須是由 @nestjs/core
匯出的 APP_GUARD
import { APP_GUARD } from '@nestjs/core';
import { LoginGuard } from './login.guard';
@Module({
imports: [PersonModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD, // 注入的 token 必須是由 @nestjs/core 匯出的
useClass: LoginGuard,
},
],
})
兩種註冊方式的區別
在 main.ts
中,由 app 註冊的,是手動 new 的例項,是不在 IOC 容器中的,無法執行注入操作
@Injectable()
export class LoginGuard implements CanActivate {
// 注入一個服務,呼叫其方法
@Inject(PersonService)
private readonly personService: PersonService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('guard log', this.personService.findAll());
return false;
}
}
在使用 app 註冊全域性守衛時,訪問任何一個路由都報錯,無法找到注入的服務模組。
而如果使用 AppModule 註冊全域性守衛,由於 Guard 是在 IOC 容器中,所以可以正常執行注入操作
當模組需要注入其它 provider 時,則需要在 AppModule 中進行註冊宣告
Interceptor
Interceptor
(攔截器) 可以處理請求處理過程中的請求和響應,例如身份驗證、日誌記錄、資料轉換等,其作用和 Middleware
較為相似,但卻有明顯的不同之處。可參考文件 Nest 中文網-攔截器
新建一個 interceptor 模板:
nest g interceptor test --no-spec --flat
// test.interceptor.ts
@Injectable()
export class TestInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 這裡的 context 和 Guard 的 context 差不多
console.log(context.getClass(), context.getHandler());
return next.handle();
}
}
在 main.ts
中進行全域性註冊
import { TestInterceptor } from './test.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TestInterceptor());
await app.listen(3000);
}
攔截器的簡單應用
interceptor 與 middleware 其中不同的一點就是,interceptor 可以獲取到資料資訊,並對其進行操作,rjsx
中提供了多種功能,例如 map
tap
等。
同時 interceptor 內部可以獲取到 context
,和 guard
一樣,而 middleware 是不行的。
- 對返回資料進行格式化包裝
export interface Response<T = any> {
code: number;
data: T;
}
@Injectable()
export class TestInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response> {
// 返回的結果都會被格式化為 {code:xxx, data:...} 的格式
return next.handle().pipe(map((data) => ({ code: 200, data })));
}
}
攔截器的全域性註冊和區域性註冊
interceptor
的註冊方式和 guard
類似
- 全域性註冊(方式一:app 註冊)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TestInterceptor());
await app.listen(3000);
}
- 全域性註冊(方式二:AppModule 註冊)
@Module({
imports: [PersonModule],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR, // 該常量由 @nestjs/core 匯出
useClass: TestInterceptor,
},
],
})
- 區域性註冊(在 Controller 新增裝飾器)
@Controller('person')
// @UseInterceptors(LoginGuard) // 當寫在這裡時,表明此攔截器將作用於該 Controller 下的所有 handler 內容
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get()
@UseInterceptors(LoginGuard) // 使用 @UseInterceptors 新增區域性攔截器
findAll() {
return this.personService.findAll();
}
}
Pipe
管道的主要作用就是對引數進行校驗和轉換。Nest 中文網-管道
自定義管道
- 建立 pipe 管道模板
nest g pipe validate --no-spec --flat
- 對管道模板進行自定義的引數校驗
@Injectable()
export class ValidatePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if (Number.isNaN(parseInt(value))) {
throw new BadRequestException(`引數${metadata.data}錯誤`);
}
return typeof value === 'number' ? value * 10 : parseInt(value) * 10;
}
}
- 建立一個請求
import { ValidatePipe } from 'src/validate.pipe';
@Controller('person')
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get('/add') // @Query 接收第二個引數,即剛才的自定義校驗管道
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
}
- 訪問瀏覽器地址 http://localhost:3000/person/add?num=amd
- 訪問瀏覽器地址 http://localhost:3000/person/add?num=2
區域性管道和全域性管道
- 全域性管道也是兩種註冊方式(app 和 AppModule)
- 區域性管道
Nest 內建 Pipe
Nest 附帶了九個開箱即用的管道
- ValidationPipe
- ValidationPipe 較為特殊,詳細使用請檢視中文網文件:參考連結
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
簡單使用
- ParseIntPipe 只允許整數 int 型別的數字透過校驗
import { ParseIntPipe } from '@nestjs/common';
@Get('/add')
addNum(@Query('num', ParseIntPipe) num: number) {
return num + 1;
}
更多便捷引數驗證請檢視 Nest 中文網-管道#類驗證器
自定義內建管道的內容
當需要返回自定義的錯誤狀態碼時,可以例項化內建管道
@Get('/add')
addNum( @Query('num', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }) ) num: number ) {
return num + 1;
}
ExceptionFilter
Nest 內建的異常層,所有未經處理的異常錯誤,該異常層就會進行捕獲,並返回對應的響應資訊,相當於內建的一個全域性異常攔截器。Nest 中文網-異常過濾器
比如手動 throw new BadRequestException(...)
就會被預設內建的異常層捕獲
nest g filter ex --no-spec --flat
對丟擲的 BadRequestException
進行單獨的異常過濾
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Request, Response } from 'express'; // Request 一定是從 express 中匯出,而不是 @nestjs/common
@Catch(BadRequestException) // 捕獲 BadRequestException 異常
export class HttpFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
區域性使用該異常過濾器
@Get('/add')
@UseFilters(HttpFilter)
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
http 相關的內建異常
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableException
- InternalServerErrorException
- NotImplementedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
也可以自行擴充套件,自定義異常
export class MyException extends HttpException {
constructor() {
super('Custom Error', 999);
}
}
全域性註冊和區域性註冊
- 全域性異常過濾器的兩種註冊方式和管道、守衛等一致(app 和 AppModule)
// main.ts 中註冊
app.useGlobalFilters(new HttpFilter());
// AppModule 中註冊
@Module({
providers: [
AppService,
{
provide: APP_FILTER, // // 該常量由 @nestjs/core 匯出
useClass: HttpFilter,
},
],
})
- 區域性註冊,可作用於單個 handler 指定異常,也可作用於整個 Controller 的 handler 指定異常
@Controller('person')
// @UseFilters(HttpFilter) 作用於該 controller 下的所有捕獲到的指定異常
export class PersonController {
constructor(private readonly personService: PersonService) {}
@Get('/add')
@UseFilters(HttpFilter) // 只作用於該 handler 的指定異常
addNum(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
}
AOP 的執行順序
該圖來自於 stackoverflow。原文地址
Middleware
是 Express 的概念,Nest 將其放在最外層執行。到了某個路由之後,會先呼叫 Guard
,Guard
用於判斷路由有沒有許可權訪問,然後會呼叫 Interceptor
,對 Contoller
前後擴充套件一些邏輯,在到達目標 Controller
之前,還會呼叫 Pipe
來對引數做檢驗和轉換。在此期間,不論是 Pipe
校驗丟擲的異常或是其它所有的 HttpException 的異常都會被 ExceptionFilter
處理,最後返回不同的響應。