TypeScript 自定义装饰器
一、背景与问题
在现代前端和后端开发中,装饰器(Decorator)已成为一种重要的元编程工具。TypeScript 自 2.2 版本引入装饰器支持后,开发者可以通过 @decorator 的形式对类、方法、属性等进行增强。然而,许多开发者在使用装饰器时往往停留在表面的 API 调用层面,而忽略了其底层原理和实际应用中的复杂场景。
问题场景
在实际开发中,我们可能遇到以下问题:
- 需要对类属性进行校验(如非空校验、格式校验)
- 需要为方法添加日志记录功能
- 需要实现基于属性的缓存机制
- 需要动态绑定元数据到类实例
传统解决方案需要通过抽象类、继承、代理等方式实现,而装饰器提供了一种更优雅的解决方案。
二、基本原理
TypeScript 装饰器本质上是通过编译时的 AST(抽象语法树)转换实现的。装饰器分为两类:
- 类装饰器(Class Decorator):用于修饰整个类
- 属性装饰器(Property Decorator):用于修饰类的属性
- 方法装饰器(Method Decorator):用于修饰类的方法
- 参数装饰器(Parameter Decorator):用于修饰方法的参数
装饰器执行顺序
装饰器按照以下顺序执行:
- 参数装饰器(如果存在)
- 方法装饰器(如果存在)
- 属性装饰器(如果存在)
- 类装饰器(如果存在)
元数据存储机制
TypeScript 使用 Reflect API 实现元数据存储。通过 Reflect.metadata 可以将装饰器信息存储到目标对象的 __metadata 属性中。在运行时,我们可以通过 Reflect.getMetadata 获取这些信息。
三、环境准备
确保你的开发环境支持装饰器:
npm install typescript @types/node --save-dev在 tsconfig.json 中启用装饰器支持:
{
"compilerOptions": {
"target": "ES2015",
"module": "ESNext",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}四、核心实现
1. 基础装饰器定义
// 基础装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling method ${propertyKey} with args: ${args}`);
return originalMethod.apply(this, args);
};
}关键代码解释:
target是目标对象(类的原型)propertyKey是方法名descriptor是方法的描述对象- 我们通过重写
descriptor.value实现方法增强
2. 带参数的装饰器
// 带参数的装饰器
function requireAuth(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (args[0] !== role) {
throw new Error(`Unauthorized: Requires role ${role}`);
}
return originalMethod.apply(this, args);
};
};
}关键代码解释:
- 外层函数接收参数
role - 内层函数处理装饰器逻辑
- 通过参数控制权限校验逻辑
3. 属性装饰器
// 属性装饰器
function minLength(length: number) {
return function (target: any, propertyKey: string) {
const value = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: string) => {
if (newValue.length < length) {
throw new Error(`Minimum length is ${length}`);
}
value = newValue;
},
enumerable: true
});
};
}关键代码解释:
- 通过
Object.defineProperty实现属性的动态控制 - 限制属性赋值时的最小长度
- 保持原有属性值的可访问性
五、完整案例
1. 用户管理系统案例
项目结构
user-management/
├── src/
│ ├── decorators/
│ │ ├── auth.decorator.ts
│ │ ├── log.decorator.ts
│ │ └── validate.decorator.ts
│ ├── models/
│ │ └── user.model.ts
│ ├── services/
│ │ └── user.service.ts
│ └── app.ts
└── tsconfig.json1. 用户模型
// src/models/user.model.ts
import { minLength } from '../decorators/validate.decorator';
export class User {
@minLength(3)
public username: string;
@minLength(6)
public password: string;
constructor(username: string, password: string) {
this.username = username;
this.password = password;
}
}2. 验证装饰器
// src/decorators/validate.decorator.ts
import { validate } from 'class-validator';
export function validate(target: any) {
const originalConstructor = target;
target = function (...args: any[]) {
const instance = new originalConstructor(...args);
const errors = validate(instance);
if (errors.length > 0) {
throw new Error('Validation failed: ' + errors.map(e => e.property).join(', '));
}
return instance;
};
return target;
}3. 接口定义
// src/services/user.service.ts
import { validate } from '../decorators/validate.decorator';
export interface IUserService {
register(user: User): void;
}
export class UserService implements IUserService {
@validate
register(user: User) {
console.log('Registering user:', user);
}
}4. 主程序
// src/app.ts
import { UserService } from './services/user.service';
const userService = new UserService();
try {
userService.register(new User('a', '123'));
} catch (error) {
console.error('Error:', error.message);
}运行结果:
Error: Validation failed: username, password六、源码解析
1. 装饰器执行流程
在 tsconfig.json 中启用 experimentalDecorators 和 emitDecoratorMetadata 后,TypeScript 编译器会将装饰器转换为运行时的元数据:
// 编译后的代码示例
function log(target, propertyKey, descriptor) {
...
}2. 元数据存储
通过 Reflect.metadata 存储装饰器信息:
Reflect.metadata('design:paramtypes', [String, String]);3. 运行时获取
import { getMetadata } from 'reflect-metadata';
const metadata = getMetadata('design:paramtypes', User);
console.log(metadata); // [ [class User], [String, String] ]七、进阶使用
1. 装饰器组合
@log
@requireAuth('admin')
register(user: User) {
...
}2. 装饰器工厂
function createLogger(logLevel: string) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
...
};
}3. 装饰器参数校验
function requireRole(roles: string[]) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
...
};
}八、性能与工程实践
1. 性能优化
- 避免在装饰器中执行复杂计算
- 使用缓存机制减少重复计算
- 对于大型项目,考虑使用装饰器工厂模式
2. 异常处理
function safeDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
try {
return original.apply(this, args);
} catch (error) {
console.error(`Error in ${propertyKey}: ${error.message}`);
return null;
}
};
}3. 安全风险
- 避免在装饰器中执行任意代码
- 对用户输入进行严格校验
- 避免暴露内部实现细节
九、常见问题与踩坑
1. 常见错误
// 错误示例:未使用Reflect.metadata
function myDecorator(target: any) {
target.myProperty = 'value';
}问题:无法在运行时获取装饰器信息
解决:使用 Reflect.metadata 存储数据
2. 装饰器顺序问题
@decoratorA
@decoratorB
method() {}问题:装饰器执行顺序与预期不符
解决:了解装饰器的执行顺序规则
3. 类型推断问题
// 错误示例:未指定装饰器参数类型
function myDecorator(param: any) {
...
}问题:类型检查不严格
解决:使用泛型或明确类型参数
十、最佳实践
1. 适用场景
- 需要对类进行统一的增强处理
- 需要记录方法调用日志
- 需要进行参数校验
- 需要实现缓存机制
- 需要动态绑定元数据
2. 不适用场景
- 简单的属性赋值
- 需要频繁修改类结构
- 需要高度动态的运行时行为
- 需要处理复杂的运行时状态
3. 设计建议
- 使用装饰器工厂模式增强可复用性
- 对于复杂逻辑,考虑使用策略模式
- 在大型项目中,建立装饰器规范
- 对于关键业务逻辑,采用装饰器+策略模式的组合
十一、总结
TypeScript 装饰器是一种强大的元编程工具,能够帮助我们以更优雅的方式增强类和对象。通过深入理解装饰器的工作原理,我们可以更好地利用其在实际开发中的潜力。
关键点总结:
- 装饰器通过 AST 转换在编译时处理
- 元数据存储是装饰器实现的核心
- 装饰器执行顺序有明确规则
- 需要合理设计装饰器的参数和逻辑
- 在大型项目中要注重规范和性能优化
- 装饰器适合处理类级别的增强需求
- 避免在装饰器中执行复杂计算
- 安全性需要特别注意
在实际开发中,我们应该根据具体需求选择合适的装饰器方案。对于需要高度动态控制的场景,可以结合装饰器和其他设计模式(如策略模式、观察者模式)来构建更复杂的系统。同时,要时刻注意装饰器可能带来的运行时开销,并在必要时进行性能优化。