TypeScript 装饰器完全指南:从原理到实现一个简易依赖注入框架

装饰器(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. 参数装饰器

作用于方法参数,接收三个参数:目标对象、方法名、参数在函数参数列表中的索引。

装饰器的执行顺序

理解执行顺序对调试至关重要。当多个装饰器作用于同一目标时:

  1. 参数装饰器 → 方法装饰器 → 属性装饰器 → 类装饰器(从内到外)
  2. 同一目标的多个装饰器,从下往上依次执行(即靠近目标的先执行)
@classDecorator
class Example {
  @propertyDecorator
  name: string;

  @methodDecorator
  greet(@paramDecorator msg: string) {}
}

执行顺序为:paramDecoratormethodDecoratorpropertyDecoratorclassDecorator

元数据反射:装饰器的好搭档

装饰器本身只能“附加”行为,但若要实现依赖注入,我们需要在运行时读取装饰器写入的信息。这时就需要 reflect-metadata 库。

import 'reflect-metadata';

const INJECTABLE_KEY = Symbol('injectable');

function Injectable(): ClassDecorator {
  return (target) => {
    Reflect.defineMetadata(INJECTABLE_KEY, true, target);
  };
}

reflect-metadata 提供了 Reflect.defineMetadataReflect.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 框架就完成了。

关键要点与注意事项

  1. 必须启用配置:在 tsconfig.json 中开启 experimentalDecoratorsemitDecoratorMetadata,后者用于生成 design:paramtypes 元数据。
  2. 循环依赖:上述实现无法处理循环依赖,生产级框架(如 InversifyJS)会通过延迟解析或代理解决。
  3. 作用域管理:真实场景需要单例、瞬态、请求级等不同生命周期,可在容器中扩展。
  4. 标准装饰器差异:TC39 标准装饰器在参数和返回值上有所不同,迁移时需注意兼容性。

总结

装饰器是 TypeScript 中强大的元编程工具,它让代码更具声明性和可组合性。通过结合 reflect-metadata,我们可以构建出灵活而优雅的依赖注入框架。本文实现的迷你 DI 虽然简单,却涵盖了装饰器的核心用法与元数据反射的精髓。掌握这些原理后,再去阅读 Angular 或 NestJS 的源码,你会发现它们不再神秘——不过是这些基础模式的工程化延伸。

未经允许不得转载:任鹏个人博客 » TypeScript 装饰器完全指南:从原理到实现一个简易依赖注入框架

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏