装饰器(Decorators)是 TypeScript 中一项极具表现力的语言特性。它允许我们以声明式的方式修改类、方法、属性或参数的行为,被广泛应用于 Angular、NestJS、TypeORM 等主流框架中。本文将从装饰器的基本原理出发,逐步深入,最终带你实现一个简易但完整的依赖注入(DI)框架。
什么是装饰器
装饰器本质上是一个函数,它接收特定的参数,并在运行时被调用,用于对目标对象进行“装饰”——即添加额外的行为或元数据。TypeScript 中的装饰器语法以 @expression 的形式出现,其中 expression 求值后必须是一个函数。
装饰器目前分为两类:
- 标准装饰器:TC39 提案中的新标准,TypeScript 5.0 起支持,无需
experimentalDecorators标志。 - 实验性装饰器:TypeScript 早期实现的版本,需要开启
experimentalDecorators编译选项,也是当前生态中应用最广泛的版本。
本文聚焦于实验性装饰器,因为绝大多数现有框架仍基于此实现。
装饰器的五种类型
实验性装饰器根据所装饰的目标不同,分为以下五类:
1. 类装饰器
作用于类构造函数,接收一个参数:类的构造函数。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
2. 方法装饰器
作用于类方法,接收三个参数:目标对象、方法名、属性描述符。
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`调用 ${key},参数:`, args);
return original.apply(this, args);
};
}
3. 属性装饰器
作用于类属性,接收两个参数:目标对象和属性名。注意它无法直接修改属性的描述符。
4. 访问器装饰器
作用于 getter/setter,参数与方法装饰器相同。
5. 参数装饰器
作用于方法参数,接收三个参数:目标对象、方法名、参数在函数参数列表中的索引。
装饰器的执行顺序
理解执行顺序对调试至关重要。当多个装饰器作用于同一目标时:
- 参数装饰器 → 方法装饰器 → 属性装饰器 → 类装饰器(从内到外)
- 同一目标的多个装饰器,从下往上依次执行(即靠近目标的先执行)
@classDecorator
class Example {
@propertyDecorator
name: string;
@methodDecorator
greet(@paramDecorator msg: string) {}
}
执行顺序为:paramDecorator → methodDecorator → propertyDecorator → classDecorator。
元数据反射:装饰器的好搭档
装饰器本身只能“附加”行为,但若要实现依赖注入,我们需要在运行时读取装饰器写入的信息。这时就需要 reflect-metadata 库。
import 'reflect-metadata';
const INJECTABLE_KEY = Symbol('injectable');
function Injectable(): ClassDecorator {
return (target) => {
Reflect.defineMetadata(INJECTABLE_KEY, true, target);
};
}
reflect-metadata 提供了 Reflect.defineMetadata、Reflect.getMetadata 等 API,让我们可以在任意对象上存取元数据。这是实现 DI 框架的基石。
实现一个简易依赖注入框架
现在,让我们综合运用以上知识,实现一个迷你 DI 框架。它包含三个核心部分:@Injectable、@Inject 和容器。
第一步:定义装饰器
import 'reflect-metadata';
const INJECTABLE_KEY = Symbol('injectable');
const INJECT_KEY = Symbol('inject');
export function Injectable(): ClassDecorator {
return (target) => {
Reflect.defineMetadata(INJECTABLE_KEY, true, target);
};
}
export function Inject(token: string): ParameterDecorator {
return (target, key, index) => {
const existing = Reflect.getMetadata(INJECT_KEY, target, key as string) || [];
existing.push({ index, token });
Reflect.defineMetadata(INJECT_KEY, existing, target, key as string);
};
}
第二步:实现容器
容器负责注册和解析依赖。我们利用 TypeScript 编译后保留的 design:paramtypes 元数据来获取构造函数参数类型。
type Constructor<T = any> = new (...args: any[]) => T;
export class Container {
private registry = new Map<string, Constructor>();
register<T>(token: string, target: Constructor<T>): void {
this.registry.set(token, target);
}
resolve<T>(target: Constructor<T>): T {
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
const injections = tokens.map((token: Constructor, index: number) => {
const injectMeta = Reflect.getMetadata(INJECT_KEY, target, undefined as any) || [];
const custom = injectMeta.find((m: any) => m.index === index);
if (custom) {
const impl = this.registry.get(custom.token);
return this.resolve(impl!);
}
return this.resolve(token);
});
return new target(...injections);
}
}
第三步:使用框架
interface Logger {
log(msg: string): void;
}
@Injectable()
class ConsoleLogger implements Logger {
log(msg: string) {
console.log(`[LOG] ${msg}`);
}
}
@Injectable()
class UserService {
constructor(private logger: Logger) {}
createUser(name: string) {
this.logger.log(`创建用户:${name}`);
}
}
const container = new Container();
container.register('Logger', ConsoleLogger);
const service = container.resolve(UserService);
service.createUser('Alice'); // 输出:[LOG] 创建用户:Alice
至此,一个支持构造函数注入和自定义令牌的简易 DI 框架就完成了。
关键要点与注意事项
- 必须启用配置:在
tsconfig.json中开启experimentalDecorators和emitDecoratorMetadata,后者用于生成design:paramtypes元数据。 - 循环依赖:上述实现无法处理循环依赖,生产级框架(如 InversifyJS)会通过延迟解析或代理解决。
- 作用域管理:真实场景需要单例、瞬态、请求级等不同生命周期,可在容器中扩展。
- 标准装饰器差异:TC39 标准装饰器在参数和返回值上有所不同,迁移时需注意兼容性。
总结
装饰器是 TypeScript 中强大的元编程工具,它让代码更具声明性和可组合性。通过结合 reflect-metadata,我们可以构建出灵活而优雅的依赖注入框架。本文实现的迷你 DI 虽然简单,却涵盖了装饰器的核心用法与元数据反射的精髓。掌握这些原理后,再去阅读 Angular 或 NestJS 的源码,你会发现它们不再神秘——不过是这些基础模式的工程化延伸。
未经允许不得转载:任鹏个人博客 » TypeScript 装饰器完全指南:从原理到实现一个简易依赖注入框架


朋友圈点赞图在线生成源码