Skip to content
第 11 章 后端 ⏱ 12 分钟阅读

第 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必须开

动手练习 ​

  1. @CurrentUser:封装 createParamDecorator 拿 req.user
  2. @Roles:写方法装饰器 + 配合 Guard 使用
  3. @CacheKey:写方法装饰器,把 key 传给 CacheInterceptor

下一章:第 12 章:配置管理 →

本站基于 VitePress 构建 · 由 StackHub 团队维护