NestJS拦截器 (Interceptors)
学习目标
- 理解拦截器在NestJS中的作用和地位
- 掌握拦截器的创建和基本使用方法
- 学会实现响应映射和请求预处理
- 理解如何使用拦截器处理异常和实现缓存
- 能够应用拦截器优化请求处理流程
核心知识点
1. 拦截器概念
拦截器是NestJS中用于处理请求和响应的组件,它们在请求处理管道中的位置如下:
- 客户端发送请求
- 中间件处理请求
- 守卫验证权限
- 管道处理数据
- 控制器处理请求
- 服务执行业务逻辑
- 拦截器处理响应
- 异常过滤器处理异常
- 响应返回给客户端
拦截器的主要作用:
- 响应映射:转换或修改响应数据
- 请求预处理:在控制器处理前修改请求数据
- 异常处理:捕获和处理异常
- 缓存:缓存响应结果
- 日志记录:记录请求和响应信息
- 性能监控:测量请求处理时间
2. 拦截器的创建
创建拦截器需要实现NestInterceptor接口:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({ data, status: 'success' })),
);
}
}3. 拦截器的应用
控制器级别
使用@UseInterceptors()装饰器在控制器级别应用拦截器:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
@Controller('cats')
@UseInterceptors(TransformInterceptor)
export class CatsController {
@Get()
findAll() {
return [{ name: 'Cat 1' }, { name: 'Cat 2' }];
}
}方法级别
使用@UseInterceptors()装饰器在方法级别应用拦截器:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
@Controller('cats')
export class CatsController {
@Get()
@UseInterceptors(TransformInterceptor)
findAll() {
return [{ name: 'Cat 1' }, { name: 'Cat 2' }];
}
}全局级别
在应用程序级别注册全局拦截器:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();4. 响应映射
使用拦截器转换响应数据:
// transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
data,
statusCode: context.switchToHttp().getResponse().statusCode,
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
})),
);
}
}5. 请求预处理
使用拦截器在控制器处理前修改请求数据:
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const request = context.switchToHttp().getRequest();
this.logger.log(
`[${request.method}] ${request.url} - Request started`,
);
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const responseTime = Date.now() - now;
this.logger.log(
`[${request.method}] ${request.url} - Response ${response.statusCode} - ${responseTime}ms`,
);
}),
);
}
}6. 异常处理
使用拦截器捕获和处理异常:
// error.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpStatus } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
// 统一处理异常
const response = context.switchToHttp().getResponse();
// 设置默认状态码
const statusCode = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
// 构建错误响应
const errorResponse = {
statusCode,
message: error.message || 'Internal server error',
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
};
// 设置响应状态码
response.status(statusCode);
// 返回错误响应
return throwError(() => errorResponse);
}),
);
}
}7. 缓存实现
使用拦截器实现简单的缓存:
// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private readonly cache = new Map<string, any>();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const cacheKey = `${request.method}:${request.url}`;
// 检查缓存
if (this.cache.has(cacheKey)) {
return of(this.cache.get(cacheKey));
}
return next.handle().pipe(
tap(data => {
// 缓存响应
this.cache.set(cacheKey, data);
// 设置缓存过期时间(示例:5分钟)
setTimeout(() => {
this.cache.delete(cacheKey);
}, 5 * 60 * 1000);
}),
);
}
}8. 拦截器的依赖注入
拦截器可以通过依赖注入系统注入其他服务:
// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private cacheService: CacheService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const cacheKey = `${request.method}:${request.url}`;
// 检查缓存
const cachedData = this.cacheService.get(cacheKey);
if (cachedData) {
return of(cachedData);
}
return next.handle().pipe(
tap(data => {
// 缓存响应
this.cacheService.set(cacheKey, data, 300); // 5分钟过期
}),
);
}
}9. 拦截器链
可以同时应用多个拦截器,它们会按照注册顺序执行:
@Controller('protected')
@UseInterceptors(LoggingInterceptor, TransformInterceptor, CacheInterceptor)
export class ProtectedController {
// 控制器方法
}10. 上下文类型
拦截器可以处理不同类型的上下文,不仅仅是HTTP请求:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class WsLoggingInterceptor 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`)),
);
}
}实践案例分析
案例:响应格式化和日志记录
需求分析
我们需要创建一个拦截器系统,包括:
- 统一的响应格式
- 详细的请求和响应日志
- 性能监控(响应时间)
- 错误处理和日志记录
- 可配置的拦截器链
实现步骤
- 创建响应格式化拦截器
- 创建日志记录拦截器
- 创建错误处理拦截器
- 配置拦截器链
- 测试拦截器效果
代码实现
1. 创建响应格式化拦截器
// transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
success: true,
data,
metadata: {
statusCode: context.switchToHttp().getResponse().statusCode,
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
},
})),
);
}
}2. 创建日志记录拦截器
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const ip = request.ip;
this.logger.log(`[${method}] ${url} - Request from ${ip}`, {
method,
url,
ip,
headers: request.headers,
body: request.body,
query: request.query,
});
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse();
const statusCode = response.statusCode;
const responseTime = Date.now() - now;
this.logger.log(`[${method}] ${url} - Response ${statusCode} - ${responseTime}ms`, {
method,
url,
statusCode,
responseTime,
});
}),
);
}
}3. 创建错误处理拦截器
// error.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpStatus, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
private readonly logger = new Logger(ErrorInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
// 记录错误日志
this.logger.error(`[${method}] ${url} - Error: ${error.message}`, error.stack, {
method,
url,
error: {
message: error.message,
status: error.status,
stack: error.stack,
},
});
// 构建错误响应
const statusCode = error.status || HttpStatus.INTERNAL_SERVER_ERROR;
const errorResponse = {
success: false,
error: {
statusCode,
message: error.message || 'Internal server error',
timestamp: new Date().toISOString(),
path: url,
},
};
return throwError(() => errorResponse);
}),
);
}
}4. 创建测试控制器
// cats.controller.ts
import { Controller, Get, Post, Body, Param, HttpException, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return [
{ id: 1, name: 'Cat 1', age: 2 },
{ id: 2, name: 'Cat 2', age: 3 },
];
}
@Get(':id')
findOne(@Param('id') id: string) {
if (id === '999') {
throw new HttpException('Cat not found', HttpStatus.NOT_FOUND);
}
return { id: parseInt(id), name: `Cat ${id}`, age: 2 };
}
@Post()
create(@Body() cat: { name: string; age: number }) {
if (!cat.name) {
throw new HttpException('Name is required', HttpStatus.BAD_REQUEST);
}
return { id: 3, ...cat };
}
}5. 配置全局拦截器
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { ErrorInterceptor } from './error.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局拦截器(顺序很重要)
app.useGlobalInterceptors(
new LoggingInterceptor(), // 首先记录请求开始
new TransformInterceptor(), // 然后格式化响应
new ErrorInterceptor(), // 最后处理错误
);
await app.listen(3000);
}
bootstrap();6. 创建模块
// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
})
export class CatsModule {}// app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
import { LoggingInterceptor } from './logging.interceptor';
import { TransformInterceptor } from './transform.interceptor';
import { ErrorInterceptor } from './error.interceptor';
@Module({
imports: [CatsModule],
providers: [LoggingInterceptor, TransformInterceptor, ErrorInterceptor],
})
export class AppModule {}测试结果
1. 正常请求 - 获取所有猫咪
请求:
GET /cats响应:
{
"success": true,
"data": [
{ "id": 1, "name": "Cat 1", "age": 2 },
{ "id": 2, "name": "Cat 2", "age": 3 }
],
"metadata": {
"statusCode": 200,
"timestamp": "2023-10-01T10:00:00.000Z",
"path": "/cats"
}
}日志:
[LOG] [GET] /cats - Request from ::1 {
"method": "GET",
"url": "/cats",
"ip": "::1",
"headers": { /* 请求头 */ },
"body": {},
"query": {}
}
[LOG] [GET] /cats - Response 200 - 5ms {
"method": "GET",
"url": "/cats",
"statusCode": 200,
"responseTime": 5
}2. 正常请求 - 创建猫咪
请求:
POST /cats
Content-Type: application/json
{
"name": "Cat 3",
"age": 1
}响应:
{
"success": true,
"data": {
"id": 3,
"name": "Cat 3",
"age": 1
},
"metadata": {
"statusCode": 201,
"timestamp": "2023-10-01T10:00:00.000Z",
"path": "/cats"
}
}日志:
[LOG] [POST] /cats - Request from ::1 {
"method": "POST",
"url": "/cats",
"ip": "::1",
"headers": { /* 请求头 */ },
"body": {
"name": "Cat 3",
"age": 1
},
"query": {}
}
[LOG] [POST] /cats - Response 201 - 8ms {
"method": "POST",
"url": "/cats",
"statusCode": 201,
"responseTime": 8
}3. 错误请求 - 猫咪不存在
请求:
GET /cats/999响应:
{
"success": false,
"error": {
"statusCode": 404,
"message": "Cat not found",
"timestamp": "2023-10-01T10:00:00.000Z",
"path": "/cats/999"
}
}日志:
[LOG] [GET] /cats/999 - Request from ::1 {
"method": "GET",
"url": "/cats/999",
"ip": "::1",
"headers": { /* 请求头 */ },
"body": {},
"query": {}
}
[ERROR] [GET] /cats/999 - Error: Cat not found {
"method": "GET",
"url": "/cats/999",
"error": {
"message": "Cat not found",
"status": 404,
"stack": "/* 错误堆栈 */"
}
}4. 错误请求 - 缺少必填字段
请求:
POST /cats
Content-Type: application/json
{
"age": 1
}响应:
{
"success": false,
"error": {
"statusCode": 400,
"message": "Name is required",
"timestamp": "2023-10-01T10:00:00.000Z",
"path": "/cats"
}
}日志:
[LOG] [POST] /cats - Request from ::1 {
"method": "POST",
"url": "/cats",
"ip": "::1",
"headers": { /* 请求头 */ },
"body": {
"age": 1
},
"query": {}
}
[ERROR] [POST] /cats - Error: Name is required {
"method": "POST",
"url": "/cats",
"error": {
"message": "Name is required",
"status": 400,
"stack": "/* 错误堆栈 */"
}
}代码解析
响应格式化拦截器:
- 统一了成功响应的格式,包含
success、data和metadata字段 - 从上下文中获取响应状态码、请求路径等信息
- 统一了成功响应的格式,包含
日志记录拦截器:
- 记录了请求的详细信息,包括方法、URL、IP、请求头、请求体和查询参数
- 记录了响应的状态码和处理时间
- 使用NestJS内置的
Logger类,支持不同级别的日志
错误处理拦截器:
- 捕获和记录所有异常
- 统一了错误响应的格式,包含
success、error字段 - 确保错误信息的一致性和完整性
拦截器链:
- 按照合理的顺序注册拦截器:先记录请求,然后处理响应,最后处理错误
- 确保每个拦截器都能正确执行
测试控制器:
- 提供了正常和错误的测试场景
- 模拟了不同类型的错误,验证错误处理拦截器的效果
互动思考问题
思考:拦截器和中间件的区别是什么?它们各自的使用场景是什么?
讨论:在实际应用中,如何设计一个高效的缓存拦截器?需要考虑哪些因素?
实践:尝试创建一个性能监控拦截器,记录每个请求的处理时间和资源使用情况。
挑战:如何实现一个基于Redis的分布式缓存拦截器,支持多实例共享缓存?
扩展:了解NestJS的
@nestjs/throttler模块,思考它如何与拦截器配合使用来实现请求限流。
小结
本集我们学习了NestJS拦截器的核心概念和使用方法,包括:
- 拦截器的基本概念和作用
- 拦截器的创建和注册方法
- 响应映射的实现
- 请求预处理和日志记录
- 异常处理的实现
- 缓存的简单实现
- 拦截器链的使用
- 不同上下文类型的处理
通过实践案例,我们创建了一个完整的响应格式化和日志记录系统,展示了如何使用拦截器优化请求处理流程并提供一致的响应格式。拦截器是NestJS处理请求和响应的重要组件,它使得响应处理更加灵活和可管理。
在下一集中,我们将学习NestJS的自定义装饰器(Custom Decorators),了解如何创建和使用自定义装饰器来增强代码的可读性和可维护性。