第 11 章:自定义装饰器
学习目标
- 理解 TS 装饰器原理
- 写参数装饰器、方法装饰器、类装饰器
- 实战:封装 @User、@Roles
- 避开 3 个装饰器坑
一、装饰器是什么
装饰器是给类/方法/参数附加元数据的特殊函数。NestJS 大量依赖装饰器。
typescript
@Get(':id') // 方法装饰器
@UseGuards(AuthGuard) // 方法装饰器(嵌套)
findOne(@Param('id') id: string) {} // 参数装饰器二、参数装饰器(最常用)
2.1 封装 @User()
typescript
// decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
const user = req.user;
if (!user) return null;
return data ? user[data] : user; // 不传参返回整个 user
},
);
// 用法
@Get('profile')
profile(@User() user: any) {
return user; // 整个 user 对象
}
@Get('email')
email(@User('email') email: string) {
return { email }; // 只取 email
}2.2 @Ip()、@HostParam() 都是这样实现的
typescript
export const Ip = createParamDecorator((_d, ctx) => {
return ctx.switchToHttp().getRequest().ip;
});三、方法装饰器(挂元数据)
typescript
// decorators/cache.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const CACHE_KEY = 'cache:key';
export const Cache = (key: string, ttl = 60) =>
SetMetadata(CACHE_KEY, { key, ttl });typescript
@Cache('users.list', 30)
@Get()
list() {}读:
typescript
const meta = this.reflector.get(CACHE_KEY, ctx.getHandler());装饰器本身不干活 —— 只是"贴标签",真正执行的是 Interceptor:
typescript
// 1️⃣ 装饰器(声明意图:这个接口要缓存,key 是 X,ttl 是 Y 秒)
export const CACHE_KEY = 'cache:key';
export const Cache = (key: string, ttl = 60) =>
SetMetadata(CACHE_KEY, { key, ttl });
// 2️⃣ Interceptor(执行意图:读标签 + 查/写 Redis)
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly redis: RedisService,
) {}
async intercept(ctx: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
// 读装饰器的元数据
const meta = this.reflector.get<{ key: string; ttl: number }>(
CACHE_KEY,
ctx.getHandler(),
);
if (!meta) return next.handle(); // 没贴标签,不缓存
// 查 Redis
const cached = await this.redis.get(meta.key);
if (cached) return of(JSON.parse(cached)); // 命中,直接返回
// 未命中 → 执行 Controller + 写 Redis
return next.handle().pipe(
tap(data => this.redis.set(meta.key, JSON.stringify(data), 'EX', meta.ttl)),
);
}
}
// 3️⃣ Controller 用法
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UsersController {
@Get()
@Cache('users.list', 30) // 30 秒缓存
list() { return this.usersService.findAll(); }
@Get(':id')
@Cache('users.detail', 60) // 60 秒缓存
findOne(@Param('id') id: string) { return this.usersService.findOne(id); }
}装饰器 vs Interceptor 分工:
| 类型 | 职责 |
|---|---|
@Cache(...) 装饰器 | 声明意图 —— "这个接口要缓存,key=xxx, ttl=30s" |
CacheInterceptor | 执行意图 —— 读标签、查 Redis、写 Redis |
完整执行流程:
请求 GET /users
↓
NestJS 拦截 → 触发 CacheInterceptor
↓
reflector.get(CACHE_KEY, list 方法)
→ 拿到 { key: 'users.list', ttl: 30 }
↓
查 Redis(键 = users.list)
├─ 命中 → of(JSON.parse(cached))→ 直接返回,不执行 Controller
└─ 未命中 → next.handle() 执行 Controller → 拿到 data → tap 写 Redis
↓
返回 data为什么分两步?
- 声明式 —— Controller 只写
@Cache(...),不用关心怎么缓存 - 复用 —— 一个
CacheInterceptor处理所有@Cache接口 - 可配置 —— 改缓存策略只改 Interceptor,不动 Controller
记忆口诀:
- 装饰器 = "贴标签"(声明)
- Interceptor = "读标签 + 干活"(执行)
reflector.get(key, handler)= "去方法上读这个 key 的标签"- 装饰器不执行任何逻辑,只挂元数据
四、自定义方法装饰器(包装行为)
typescript
// decorators/retry.decorator.ts
export function Retry(times = 3, delay = 1000) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastErr;
for (let i = 0; i < times; i++) {
try {
return await original.apply(this, args);
} catch (err) {
lastErr = err;
await new Promise(r => setTimeout(r, delay));
}
}
throw lastErr;
};
return descriptor;
};
}
// 用法
@Retry(3, 500)
async fetchData() { /* ... */ }⚠️ 坑 1:方法装饰器返回的不是 Observable → 不能直接配
pipe(),要配合 Interceptor。
五、类装饰器(组合元数据)
typescript
// decorators/controller.decorator.ts
import { applyDecorators, Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../guards/auth.guard';
export function AuthedController(path: string) {
return applyDecorators(
Controller(path),
UseGuards(AuthGuard),
);
}
// 用法
@AuthedController('admin')
export class AdminController {}applyDecorators 组合多个装饰器。
记忆口诀:
applyDecorators(A, B, C)=@A @B @C写在一起- 没新功能 = 只是装饰器合并
- 用于:多个 Controller 有相同装饰器组合时
- 核心目的 = DRY(Don't Repeat Yourself)
六、组合元数据 + 参数装饰器
typescript
// decorators/roles.decorator.ts
import { SetMetadata, createParamDecorator } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
export const CurrentUser = createParamDecorator((_d, ctx) => {
return ctx.switchToHttp().getRequest().user;
});
// 用法
@Roles('admin')
@Get()
list(@CurrentUser() user: any) {
return { msg: `${user.name} 你是 admin`, list: [] };
}七、装饰器执行顺序
类装饰器 → 方法装饰器 → 参数装饰器(从下往上)
typescript
@A // 1
@B // 2
class Foo {
@C // 3
@D // 4
method(@E x: string) {} // 5
}⚠️ 坑 2:
@Get和@Post都属于方法装饰器,多个一起用顺序敏感。
八、TS 配置
tsconfig.json 必须开装饰器:
json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}NestJS 脚手架默认开好。
⚠️ 坑 3:没开
emitDecoratorMetadata→ NestJS 拿不到参数类型元数据,DI 失效。
九、实战:统一日志装饰器
typescript
// decorators/log-time.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const LOG_TIME_KEY = 'log_time';
export const LogTime = () => SetMetadata(LOG_TIME_KEY, true);typescript
// interceptors/log-time.interceptor.ts
@Injectable()
export class LogTimeInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler) {
const enabled = this.reflector.get(LOG_TIME_KEY, ctx.getHandler());
if (!enabled) return next.handle();
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`${ctx.getHandler().name} 耗时 ${Date.now() - start}ms`)),
);
}
}typescript
@LogTime()
@Get('heavy')
heavy() { /* ... */ }十、本章小结
| 类型 | API | 用途 |
|---|---|---|
| 参数 | createParamDecorator | 抽取 user/ip 等 |
| 元数据 | SetMetadata | 给 Guard/Interceptor 读 |
| 方法 | 自定义 descriptor.value | 重试、计时 |
| 组合 | applyDecorators | 把多个装饰器合并 |
| 配置 | experimentalDecorators + emitDecoratorMetadata | 必须开 |
动手练习
- @CurrentUser:封装
createParamDecorator拿req.user - @Roles:写方法装饰器 + 配合 Guard 使用
- @CacheKey:写方法装饰器,把 key 传给 CacheInterceptor
下一章:第 12 章:配置管理 →