装饰器与元数据
一、TypeScript 装饰器本质
装饰器是一个在类/方法/属性/参数被定义时执行的函数(注意:是类定义时,不是实例化时,也不是调用时)。
TypeScript 目前支持两套装饰器:
- Legacy Decorators(
experimentalDecorators: true)— NestJS 使用此版本 - Stage 3 Decorators(TC39 提案,TS 5.0+ 支持)— 与 NestJS 不兼容
typescript
// tsconfig.json 中必须开启:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}四种装饰器类型
typescript
// 1. 类装饰器:接收构造函数,返回值可替换原类
function Injectable(): ClassDecorator {
return (target: Function) => {
Reflect.defineMetadata('injectable', true, target);
};
}
// 2. 方法装饰器:接收原型、方法名、属性描述符
function Get(path: string): MethodDecorator {
return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
Reflect.defineMetadata('path', path, target, key as string);
Reflect.defineMetadata('method', 'GET', target, key as string);
};
}
// 3. 属性装饰器:接收原型、属性名
function Column(): PropertyDecorator {
return (target: object, key: string | symbol) => {
const columns = Reflect.getMetadata('columns', target) || [];
Reflect.defineMetadata('columns', [...columns, key], target);
};
}
// 4. 参数装饰器:接收原型、方法名、参数索引
function Body(): ParameterDecorator {
return (target: object, key: string | symbol, index: number) => {
const params = Reflect.getMetadata('body_params', target, key as string) || [];
params[index] = 'body';
Reflect.defineMetadata('body_params', params, target, key as string);
};
}装饰器执行顺序
多个装饰器叠加时,执行顺序是从下到上(求值是从上到下,执行是从下到上):
typescript
@A // 第2步执行
@B // 第1步执行
@C // 先执行(最接近被装饰目标)
class Foo {}
// 等价于:A(B(C(Foo)))类上的多个方法装饰器,按方法定义顺序执行:
typescript
@Controller('users')
export class UsersController {
@Get() // 先处理
findAll() {}
@Get(':id') // 后处理
findOne() {}
}二、reflect-metadata 深度解析
reflect-metadata 是 TC39 提案的 polyfill,提供了一套标准化的元数据 API。
核心 API:
typescript
// 定义元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); // 方法/属性上
// 读取元数据(含继承链)
Reflect.getMetadata(metadataKey, target);
Reflect.getMetadata(metadataKey, target, propertyKey);
// 读取元数据(仅自身,不含继承)
Reflect.getOwnMetadata(metadataKey, target);
// 检查是否存在
Reflect.hasMetadata(metadataKey, target);
// 获取所有元数据 key
Reflect.getMetadataKeys(target);
// 删除
Reflect.deleteMetadata(metadataKey, target);emitDecoratorMetadata 的魔法
当 emitDecoratorMetadata: true 时,TypeScript 编译器会在有装饰器的地方自动注入三个元数据:
| 元数据键 | 值 | 说明 |
|---|---|---|
design:type | 属性的类型 | 属性装饰器可用 |
design:paramtypes | 构造函数参数类型数组 | 类装饰器可用 |
design:returntype | 方法返回类型 | 方法装饰器可用 |
typescript
@Injectable()
class UserService {
constructor(
private repo: UserRepository, // TypeScript 类型
private config: ConfigService,
) {}
}
// 编译后,TypeScript 自动注入:
// Reflect.defineMetadata('design:paramtypes',
// [UserRepository, ConfigService],
// UserService
// )
// NestJS DI 容器读取:
const types = Reflect.getMetadata('design:paramtypes', UserService);
// → [UserRepository, ConfigService]
// 然后去容器里查找对应实例并注入注意:接口在运行时会被擦除!
typescript
// ❌ 接口在编译后消失,无法作为注入 Token
interface IUserRepo {}
constructor(private repo: IUserRepo) {} // design:paramtypes 中是 Object
// ✅ 使用抽象类或具体类
abstract class UserRepo {}
constructor(private repo: UserRepo) {} // design:paramtypes 中是 UserRepo三、NestJS 内置元数据键
| 元数据键 | 用途 | 由谁设置 |
|---|---|---|
design:paramtypes | 构造函数参数类型 | TypeScript 自动注入 |
path | 路由路径 | @Controller()/@Get() 等 |
method | HTTP 方法 | @Get()/@Post() 等 |
guards | 守卫列表 | @UseGuards() |
interceptors | 拦截器列表 | @UseInterceptors() |
pipes | 管道列表 | @UsePipes() |
filters | 异常过滤器 | @UseFilters() |
roles | 角色要求 | 自定义 @Roles() |
__isController__ | 标记是否是 Controller | @Controller() |
__isRouteParamMetadata__ | 路由参数绑定 | @Param()/@Body() 等 |
四、自定义元数据实战
场景一:角色权限
typescript
// 定义装饰器(推荐用 SetMetadata 工厂函数)
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// 使用
@Get('admin')
@Roles('admin', 'superuser')
getAdminData() {}
// 在守卫中读取
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// getAllAndOverride:方法级优先,回退到类级
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), // 方法上的 @Roles
context.getClass(), // 类上的 @Roles
]);
if (!roles) return true; // 无角色限制,放行
const user = context.switchToHttp().getRequest().user;
return roles.some(role => user.roles.includes(role));
}
}场景二:接口版本标记
typescript
export const ApiVersion = (version: number) =>
SetMetadata('api_version', version);
@Get('users')
@ApiVersion(2)
findAllV2() {}场景三:公开接口(跳过认证)
typescript
export const Public = () => SetMetadata('isPublic', true);
@Public()
@Get('health')
healthCheck() { return 'ok'; }
// JWT 守卫中判断
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { super(); }
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true; // 直接放行
return super.canActivate(context);
}
}五、Reflector 的三种方法对比
Reflector 是 NestJS 对 Reflect.getMetadata 的封装,提供更便捷的 API:
typescript
// get:只读取方法/类上的元数据(不合并)
const roles = reflector.get<string[]>('roles', context.getHandler());
// getAllAndMerge:收集类和方法上的所有值,合并为一个数组
const allRoles = reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// 若类上是 ['admin'],方法上是 ['editor'],结果是 ['admin', 'editor']
// getAllAndOverride:方法级优先,若方法上没有则用类上的
const effectiveRoles = reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// 若方法上是 ['editor'],直接返回 ['editor'](不管类上是什么)使用原则:
getAllAndOverride:权限控制(方法级覆盖类级)getAllAndMerge:标签/分组(累积收集)get:简单场景,只关心一个层级
六、常见陷阱
陷阱 1:忘记在 tsconfig.json 开启选项
json
// 缺少这两项,DI 和很多装饰器功能会失效
{
"experimentalDecorators": true, // ← 必须
"emitDecoratorMetadata": true // ← 必须
}陷阱 2:装饰器在 Class 外使用
typescript
// ❌ 装饰器只能用于类、方法、属性、参数
@Injectable()
const fn = () => {}; // 语法错误陷阱 3:继承时元数据不自动继承
typescript
@Controller('base')
class BaseController {
@Get()
findAll() {}
}
// 子类不会继承父类的 @Get() 装饰器行为
// NestJS 不支持通过继承复用路由——使用 Mixin 模式替代
class UsersController extends BaseController {}可运行 Demo:
practice/01-ioc-demo— 观察 reflect-metadata 元数据读写(npm test)
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
Reflect.getMetadata 返回 undefined | emitDecoratorMetadata 未开启 | tsconfig.json 设置 "emitDecoratorMetadata": true |
| 装饰器执行顺序出错 | 多个装饰器从下到上执行 | 记住:@A @B 等价于 A(B(target)),B 先执行 |
| 参数装饰器拿不到类型 | TypeScript 编译目标太低 | "target": "ES2020" 以上才能正确发射元数据 |