0%

NestJS 拦截器与跳过拦截器

写 API 接口时,我们通常希望有个统一的结构包裹一下要返回的数据,每个接口分别去写不光繁琐,还容易不一致,所以一般会用一个统一的拦截器来实现这个功能。各种语言的不同框架基本都有对应的拦截器写法,今天分享下 nestjs 里如何编写拦截器和如何跳过拦截器。

比如普通的一个查询接口,返回的数据如:

1
2
3
4
{
"user": "xxx",
"image_url": "https://xxxxx"
}

客户端侧往往需要区分是成功还是失败,所以希望这个数据被包在 data 字段下面,再用一个同级的 "status": "success", "code": 0 来代表成功。如果是失败的,则返回 fail 、对应错误码及错误信息。

1
2
3
4
5
6
7
8
{
"code": 0,
"status": "success",
"data": {
"user": "xxx",
"image_url": "https://xxxxx"
}
}

要包装返回数据的需求非常的常见,相关的文档非常的多。对于 nestjs ,很快就查到正确的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Reflector } from '@nestjs/core';

@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
constructor(@Inject(Reflector) private readonly reflector: Reflector) {}

intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
// 将原有的 `data` 转化为统一的格式后返回
map((data) => ({
code: 0,
success: true,
status: 'success',
data,
})),
);
}
}

对应的还要有一个错误拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ResponseErrorInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
catchError(async (err) => {
const errorResponse = {
code: (err.response && err.response.code) || -1,
success: false,
status: err.status || 'fail',
message: err.message || '不明异常',
data: err.response,
};

return errorResponse;
}),
);
}
}

最后再在 app.module.ts 注册这两个拦截器:

1
2
3
4
5
6
7
8
9
10
@Module({
// imports: ... ,
// controllers: ... ,
providers: [
// ...
{ provide: APP_INTERCEPTOR, useClass: ResponseErrorInterceptor },
{ provide: APP_INTERCEPTOR, useClass: ResponseFormatInterceptor },
],
})
export class AppModule {}

这样就可以满足需求了,但有一些接口,不希望有这个包装,比如微信推送消息的服务,推给你的服务器后需要你的服务器返回一个 ‘success’ 的文本。这时需要跳过这个全局拦截器,这个不知道是搜索姿势不对还是这个需求比较少,比较少找到相关资料。

这里使用的是装饰器 decorator 来实现:

1
2
3
4
// src/decorator/skip.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const SkipInterceptor = () => SetMetadata('skipInterceptor', true);

这样就声明了一个装饰器,装饰器会设置一个 metadata skipInterceptor 值为 true,再修改格式化响应的拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Reflector } from '@nestjs/core';

@Injectable()
export class ResponseFormatInterceptor implements NestInterceptor {
constructor(@Inject(Reflector) private readonly reflector: Reflector) {}

intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const skipInterceptor = this.reflector.get<boolean>(
'skipInterceptor',
context.getHandler(),
);
if (skipInterceptor) {
// 跳过全局拦截器的处理
return next.handle();
}
return next.handle().pipe(
// 将原有的 `data` 转化为统一的格式后返回
map((data) => ({
code: 0,
success: true,
status: 'success',
data,
})),
);
}
}

在 intercept 中判断是否 skipInterceptor ,是的话就直接 next ,否的话正常 pipe 里做包装。最后,使用时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用装饰器
import { Body, Controller, HttpCode, Post } from '@nestjs/common';
import { SkipInterceptor } from '../../decorator/skip.decorator';

@Controller('xxx')
export class xxxController {
constructor() {}

@Post('push')
@HttpCode(200)
@SkipInterceptor()
async handleWechatPush(@Body() data) {
// ...逻辑处理
return 'success';
}
}

就可以拿到纯字符串的 success 了。