什么是拦截器?🤔
拦截器(Interceptor)是一个使用 @Injectable() 装饰器注解的类,它实现了 NestInterceptor 接口。拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换函数返回的结果
- 转换函数抛出的异常
- 扩展基本函数行为
- 根据特定条件完全重写函数
Express vs NestJS 对比 🆚
让我们看看在传统 Express 和 NestJS 中如何处理响应转换:
Express 写法:
javascript
// 响应包装中间件
const responseWrapper = (req, res, next) => {
const originalJson = res.json;
res.json = function(data) {
return originalJson.call(this, {
code: 200,
data,
message: 'success'
});
};
next();
};
// 日志中间件
const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} ${duration}ms`);
});
next();
};
app.use(responseWrapper);
app.use(logger);NestJS 写法:
typescript
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
code: 200,
data,
message: 'success'
}))
);
}
}
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
const request = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
console.log(`${request.method} ${request.url} ${duration}ms`);
})
);
}
}拦截器的应用场景 🎯
1. 响应数据转换
typescript
@Injectable()
export class ResponseTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => {
const response = context.switchToHttp().getResponse();
return {
code: response.statusCode,
data,
message: 'success',
timestamp: new Date().toISOString()
};
})
);
}
}
// 使用拦截器
@Controller('users')
@UseInterceptors(ResponseTransformInterceptor)
export class UsersController {
@Get()
findAll() {
return this.usersService.findAll();
}
}2. 缓存拦截器
typescript
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private cacheManager: Cache) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const cacheKey = request.url;
const cachedResponse = await this.cacheManager.get(cacheKey);
if (cachedResponse) {
return of(cachedResponse);
}
return next.handle().pipe(
tap(response => {
this.cacheManager.set(cacheKey, response, { ttl: 60 });
})
);
}
}3. 超时处理
typescript
@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) {
throw new RequestTimeoutException();
}
throw err;
}),
);
}
}4. 文件上传进度
typescript
@Injectable()
export class FileUploadInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
if (request.file) {
const totalSize = request.file.size;
let uploadedSize = 0;
request.file.on('data', (chunk) => {
uploadedSize += chunk.length;
const progress = (uploadedSize / totalSize) * 100;
console.log(`Upload progress: ${progress}%`);
});
}
return next.handle();
}
}高级用法 🚀
1. 异常映射
typescript
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => {
if (err instanceof HttpException) {
return throwError(() => err);
}
return throwError(() => new InternalServerErrorException({
message: '服务器内部错误',
originalError: err.message
}));
}),
);
}
}2. 流数据处理
typescript
@Injectable()
export class StreamInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => {
if (Array.isArray(data)) {
return from(data).pipe(
map(item => this.transformItem(item)),
toArray()
);
}
return this.transformItem(data);
}),
);
}
private transformItem(item: any) {
// 转换逻辑
return item;
}
}3. 条件拦截
typescript
@Injectable()
export class ConditionalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const shouldIntercept = request.query.transform === 'true';
if (shouldIntercept) {
return next.handle().pipe(
map(data => this.transform(data))
);
}
return next.handle();
}
private transform(data: any) {
// 转换逻辑
return data;
}
}实战示例 💡
让我们创建一个完整的日志系统:
typescript
// interceptors/logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(
private readonly logger: Logger,
private readonly configService: ConfigService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
const requestId = uuid();
// 记录请求信息
this.logger.log({
type: 'request',
requestId,
method,
url,
body: request.body,
query: request.query,
headers: this.filterSensitiveHeaders(request.headers)
});
return next.handle().pipe(
tap({
next: (data) => {
// 记录响应信息
const duration = Date.now() - now;
this.logger.log({
type: 'response',
requestId,
duration,
statusCode: context.switchToHttp().getResponse().statusCode,
data: this.shouldLogResponse(url) ? data : '[FILTERED]'
});
},
error: (error) => {
// 记录错误信息
const duration = Date.now() - now;
this.logger.error({
type: 'error',
requestId,
duration,
error: {
name: error.name,
message: error.message,
stack: this.configService.get('NODE_ENV') === 'development'
? error.stack
: undefined
}
});
}
})
);
}
private filterSensitiveHeaders(headers: any) {
const sensitiveHeaders = ['authorization', 'cookie'];
return Object.keys(headers).reduce((acc, key) => {
if (sensitiveHeaders.includes(key.toLowerCase())) {
acc[key] = '[FILTERED]';
} else {
acc[key] = headers[key];
}
return acc;
}, {});
}
private shouldLogResponse(url: string): boolean {
const excludePaths = ['/health', '/metrics'];
return !excludePaths.some(path => url.startsWith(path));
}
}最佳实践 ✨
1. 合理使用全局拦截器
typescript
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector)),
new ResponseTransformInterceptor(),
new LoggingInterceptor()
);
await app.listen(3000);
}2. 拦截器组合
typescript
const CommonInterceptors = applyDecorators(
UseInterceptors(
CacheInterceptor,
TimeoutInterceptor,
LoggingInterceptor
)
);
@Controller('users')
@CommonInterceptors
export class UsersController {}3. 性能考虑
typescript
@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
private readonly logger = new Logger('Performance');
private readonly threshold = 1000; // 1秒
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
const request = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
if (duration > this.threshold) {
this.logger.warn(
`Slow request detected: ${request.method} ${request.url} ${duration}ms`
);
}
})
);
}
}总结 📝
通过这篇文章,我们深入学习了:
- 拦截器的基本概念和作用
- 不同类型的拦截器实现
- 拦截器的高级用法
- 实际项目中的应用场景
- 性能和最佳实践考虑
拦截器是 NestJS 中非常强大的功能,它可以帮助我们:
- 统一处理响应格式
- 实现日志记录
- 处理缓存
- 处理超时
- 转换响应数据
- 处理异常
下一篇文章,我们将探讨 NestJS 的异常过滤器(Exception Filter)。