Skip to content

装饰器工厂与元数据

概述

装饰器工厂(Decorator Factory)是返回装饰器函数的函数,它允许我们通过参数配置装饰器的行为。装饰器组合(Decorator Composition)让我们能够将多个装饰器组合使用,实现复杂的功能。元数据(Metadata)则提供了在运行时访问类型信息的能力,是实现依赖注入、验证等高级功能的基础。掌握这些高级特性,能够让我们创建更加灵活、强大的装饰器系统。

先修知识

在学习本文档之前,建议先了解:

注意

装饰器工厂和元数据功能需要启用 experimentalDecoratorsemitDecoratorMetadata 选项。某些元数据功能还需要安装 reflect-metadata 库。

装饰器工厂深入

装饰器工厂是一个返回装饰器函数的函数,它允许我们传递参数来配置装饰器的行为。这使得装饰器更加灵活和可复用。

基本概念

装饰器工厂的基本结构:

typescript
// 装饰器工厂:接收配置参数,返回装饰器函数
function decoratorFactory(config: ConfigType) {
  // 返回实际的装饰器函数
  return function (target: any) {
    // 使用 config 配置装饰器行为
    // ...
  };
}

// 使用装饰器工厂
@decoratorFactory({ option: "value" })
class MyClass {
  // ...
}

类装饰器工厂

类装饰器工厂可以用于创建可配置的类装饰器:

typescript
// 可配置的版本装饰器工厂
function version(versionNumber: string) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    return class extends target {
      static version = versionNumber;
      static getVersion() {
        return versionNumber;
      }
    };
  };
}

@version("1.0.0")
class ApiService {
  // ...
}

console.log(ApiService.version);      // "1.0.0"
console.log(ApiService.getVersion());  // "1.0.0"

方法装饰器工厂

方法装饰器工厂是最常用的装饰器工厂类型:

typescript
// 可配置的日志装饰器工厂
function log(options?: { level?: "info" | "warn" | "error"; prefix?: string }) {
  const level = options?.level || "info";
  const prefix = options?.prefix || "";
  
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const message = `${prefix}${propertyKey}(${args.join(", ")})`;
      
      switch (level) {
        case "info":
          console.log(`[INFO] ${message}`);
          break;
        case "warn":
          console.warn(`[WARN] ${message}`);
          break;
        case "error":
          console.error(`[ERROR] ${message}`);
          break;
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

class Calculator {
  @log({ level: "info", prefix: "计算: " })
  add(a: number, b: number): number {
    return a + b;
  }
  
  @log({ level: "warn" })
  subtract(a: number, b: number): number {
    return a - b;
  }
}

const calc = new Calculator();
calc.add(5, 3);        // 输出: [INFO] 计算: add(5, 3)
calc.subtract(10, 4);  // 输出: [WARN] subtract(10, 4)

属性装饰器工厂

属性装饰器工厂可以用于创建可配置的属性装饰器:

typescript
// 可配置的属性验证装饰器工厂
function validate(validator: (value: any) => boolean, errorMessage?: string) {
  return function (target: any, propertyKey: string) {
    let value: any;
    
    const getter = function () {
      return value;
    };
    
    const setter = function (newVal: any) {
      if (!validator(newVal)) {
        throw new Error(errorMessage || `${propertyKey} 验证失败`);
      }
      value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class User {
  @validate((v: string) => v.length >= 3, "用户名至少 3 个字符")
  username: string;
  
  @validate((v: number) => v > 0 && v < 150, "年龄必须在 0-150 之间")
  age: number;
  
  constructor(username: string, age: number) {
    this.username = username;
    this.age = age;
  }
}

const user1 = new User("john", 30);  // ✅ 成功
// const user2 = new User("jo", 30);  // ❌ 错误: 用户名至少 3 个字符

参数装饰器工厂

参数装饰器工厂常用于依赖注入:

typescript
// 依赖注入装饰器工厂
function inject(token: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    const metadataKey = `__inject_${propertyKey || "constructor"}`;
    if (!target[metadataKey]) {
      target[metadataKey] = [];
    }
    target[metadataKey][parameterIndex] = token;
  };
}

class Service {
  constructor(
    @inject("DatabaseService") private db: any,
    @inject("LoggerService") private logger: any
  ) {}
}

装饰器组合

装饰器组合是指将多个装饰器应用于同一个声明,它们按照特定的顺序执行。理解装饰器的执行顺序对于正确使用装饰器组合至关重要。

执行顺序规则

装饰器的执行顺序遵循以下规则:

  1. 装饰器工厂函数:从上到下执行
  2. 装饰器函数:从下到上执行(对于类和方法装饰器)
  3. 参数装饰器:从最后一个参数到第一个参数
typescript
// 装饰器执行顺序示例
function first() {
  console.log("1. first() 工厂函数执行");
  return function (target: any) {
    console.log("4. first() 装饰器应用");
  };
}

function second() {
  console.log("2. second() 工厂函数执行");
  return function (target: any) {
    console.log("3. second() 装饰器应用");
  };
}

function third() {
  console.log("3. third() 工厂函数执行");
  return function (target: any) {
    console.log("2. third() 装饰器应用");
  };
}

@first()
@second()
@third()
class Example {
  // ...
}

// 输出顺序:
// 1. first() 工厂函数执行
// 2. second() 工厂函数执行
// 3. third() 工厂函数执行
// 2. third() 装饰器应用
// 3. second() 装饰器应用
// 4. first() 装饰器应用

实际应用:组合多个功能

在实际开发中,我们经常需要组合多个装饰器来实现复杂的功能:

typescript
// 权限检查装饰器工厂
function requirePermission(permission: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const user = (this as any).currentUser;
      if (!user || !user.permissions.includes(permission)) {
        throw new Error(`需要权限: ${permission}`);
      }
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// 缓存装饰器
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const cacheMap = new Map<string, any>();
  
  descriptor.value = function (...args: any[]) {
    const cacheKey = JSON.stringify(args);
    if (cacheMap.has(cacheKey)) {
      return cacheMap.get(cacheKey);
    }
    const result = originalMethod.apply(this, args);
    cacheMap.set(cacheKey, result);
    return result;
  };
  
  return descriptor;
}

// 日志装饰器工厂
function log(options?: { level?: string }) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const level = options?.level || "info";
    
    descriptor.value = function (...args: any[]) {
      console.log(`[${level.toUpperCase()}] ${propertyKey}(${args.join(", ")})`);
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// 组合使用多个装饰器
class ApiController {
  currentUser: { permissions: string[] } | null = null;
  
  // 执行顺序:权限检查 -> 缓存 -> 日志 -> 实际方法
  @requirePermission("read")
  @cache
  @log({ level: "info" })
  async getData(id: string) {
    return { id, data: "some data" };
  }
  
  // 执行顺序:权限检查 -> 验证 -> 日志 -> 实际方法
  @requirePermission("write")
  @log({ level: "warn" })
  async updateData(id: string, data: any) {
    return { id, data, updated: true };
  }
}

创建装饰器组合工具

我们可以创建工具函数来简化装饰器组合:

typescript
// 装饰器组合工具函数
function composeDecorators(...decorators: Array<Function>) {
  return function (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
    // 从最后一个装饰器开始应用
    for (let i = decorators.length - 1; i >= 0; i--) {
      const decorator = decorators[i];
      if (propertyKey && descriptor) {
        // 方法装饰器
        const result = decorator(target, propertyKey, descriptor);
        if (result) {
          descriptor = result;
        }
      } else {
        // 类装饰器
        const result = decorator(target);
        if (result) {
          target = result;
        }
      }
    }
    return descriptor || target;
  };
}

// 使用组合工具
class Service {
  @composeDecorators(
    log({ level: "info" }),
    cache,
    requirePermission("read")
  )
  getData(id: string) {
    return { id, data: "data" };
  }
}

元数据(Metadata)

元数据允许我们在运行时访问类型信息,这对于实现依赖注入、验证、序列化等高级功能至关重要。TypeScript 通过 emitDecoratorMetadata 选项和 reflect-metadata 库来支持元数据。

启用元数据支持

首先需要在 tsconfig.json 中启用元数据:

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

然后安装 reflect-metadata 库:

bash
npm install reflect-metadata

在应用入口导入:

typescript
import "reflect-metadata";

基本元数据操作

reflect-metadata 提供了基本的元数据操作:

typescript
import "reflect-metadata";

// 定义元数据
class MyClass {
  @Reflect.metadata("key", "value")
  property: string;
}

// 获取元数据
const value = Reflect.getMetadata("key", MyClass.prototype, "property");
console.log(value); // "value"

// 检查是否有元数据
const hasMetadata = Reflect.hasMetadata("key", MyClass.prototype, "property");
console.log(hasMetadata); // true

// 获取所有元数据键
const keys = Reflect.getMetadataKeys(MyClass.prototype, "property");
console.log(keys); // ["key"]

// 删除元数据
Reflect.deleteMetadata("key", MyClass.prototype, "property");

类型元数据

启用 emitDecoratorMetadata 后,TypeScript 会自动为装饰的参数、属性和方法生成类型元数据:

typescript
import "reflect-metadata";

// 类型元数据的键
const designType = "design:type";
const designParamTypes = "design:paramtypes";
const designReturnType = "design:returntype";

class Service {
  // 属性类型元数据
  @Reflect.metadata(designType, String)
  name: string;
  
  // 方法参数类型元数据
  process(
    @Reflect.metadata(designParamTypes, [String, Number])
    data: string,
    count: number
  ): void {
    // ...
  }
}

// 获取类型元数据
const nameType = Reflect.getMetadata(designType, Service.prototype, "name");
console.log(nameType); // [Function: String]

const paramTypes = Reflect.getMetadata(designParamTypes, Service.prototype, "process");
console.log(paramTypes); // [Function: String, Function: Number]

依赖注入实现

使用元数据实现完整的依赖注入系统:

typescript
import "reflect-metadata";

// 依赖注入标记
const INJECT_TOKEN = Symbol("inject");

// 可注入装饰器
function Injectable(token?: string) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    const tokenValue = token || target.name;
    Reflect.defineMetadata(INJECT_TOKEN, tokenValue, target);
    return target;
  };
}

// 参数注入装饰器
function Inject(token: string) {
  return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
    const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey || "constructor");
    const injectionTokens = Reflect.getMetadata(INJECT_TOKEN, target) || {};
    
    if (!injectionTokens[propertyKey || "constructor"]) {
      injectionTokens[propertyKey || "constructor"] = [];
    }
    
    injectionTokens[propertyKey || "constructor"][parameterIndex] = token;
    Reflect.defineMetadata(INJECT_TOKEN, injectionTokens, target);
  };
}

// 依赖注入容器
class Container {
  private services = new Map<string, any>();
  
  register<T>(token: string, service: T): void {
    this.services.set(token, service);
  }
  
  resolve<T>(target: new (...args: any[]) => T): T {
    const injections = Reflect.getMetadata(INJECT_TOKEN, target) || {};
    const constructorInjections = injections["constructor"] || [];
    const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
    
    const args = constructorInjections.map((token: string, index: number) => {
      if (token) {
        return this.services.get(token);
      }
      // 如果没有指定 token,尝试使用类型
      const paramType = paramTypes[index];
      if (paramType && paramType !== Object) {
        const typeToken = Reflect.getMetadata(INJECT_TOKEN, paramType) || paramType.name;
        return this.services.get(typeToken);
      }
      return undefined;
    });
    
    return new target(...args);
  }
}

// 使用示例
@Injectable("DatabaseService")
class DatabaseService {
  connect() {
    return "数据库已连接";
  }
}

@Injectable("LoggerService")
class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

@Injectable("UserService")
class UserService {
  constructor(
    @Inject("DatabaseService") private db: DatabaseService,
    @Inject("LoggerService") private logger: LoggerService
  ) {}
  
  getUsers() {
    this.logger.log("获取用户列表");
    return this.db.connect() + " - 用户列表";
  }
}

// 注册服务
const container = new Container();
container.register("DatabaseService", new DatabaseService());
container.register("LoggerService", new LoggerService());

// 解析服务
const userService = container.resolve(UserService);
console.log(userService.getUsers());
// 输出: [LOG] 获取用户列表
// 输出: 数据库已连接 - 用户列表

验证系统实现

使用元数据实现类型验证系统:

typescript
import "reflect-metadata";

// 验证装饰器工厂
function Validate(validator: (value: any) => boolean, errorMessage?: string) {
  return function (target: any, propertyKey: string) {
    const validators = Reflect.getMetadata("validators", target) || {};
    validators[propertyKey] = { validator, errorMessage };
    Reflect.defineMetadata("validators", validators, target);
  };
}

// 验证方法装饰器
function ValidateClass(target: any) {
  const validators = Reflect.getMetadata("validators", target.prototype) || {};
  
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      
      // 验证所有标记的属性
      for (const [propertyKey, { validator, errorMessage }] of Object.entries(validators)) {
        const value = (this as any)[propertyKey];
        if (!validator(value)) {
          throw new Error(errorMessage || `${propertyKey} 验证失败`);
        }
      }
    }
  };
}

@ValidateClass
class User {
  @Validate((v: string) => v.length >= 3, "用户名至少 3 个字符")
  username: string;
  
  @Validate((v: number) => v > 0 && v < 150, "年龄必须在 0-150 之间")
  age: number;
  
  @Validate((v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "邮箱格式不正确")
  email: string;
  
  constructor(username: string, age: number, email: string) {
    this.username = username;
    this.age = age;
    this.email = email;
  }
}

const user1 = new User("john", 30, "john@example.com");  // ✅ 成功
// const user2 = new User("jo", 30, "john@example.com");  // ❌ 错误: 用户名至少 3 个字符

序列化系统实现

使用元数据实现对象序列化:

typescript
import "reflect-metadata";

// 序列化选项
interface SerializeOptions {
  name?: string;
  optional?: boolean;
  transform?: (value: any) => any;
}

// 序列化装饰器工厂
function Serialize(options?: SerializeOptions) {
  return function (target: any, propertyKey: string) {
    const serializers = Reflect.getMetadata("serializers", target) || {};
    serializers[propertyKey] = options || {};
    Reflect.defineMetadata("serializers", serializers, target);
  };
}

// 序列化工具类
class Serializer {
  static serialize<T>(instance: T): any {
    const serializers = Reflect.getMetadata("serializers", instance.constructor.prototype) || {};
    const result: any = {};
    
    for (const [propertyKey, options] of Object.entries(serializers)) {
      const value = (instance as any)[propertyKey];
      const opts = options as SerializeOptions;
      
      // 跳过可选且为 undefined 的属性
      if (opts.optional && value === undefined) {
        continue;
      }
      
      // 应用转换函数
      const serializedValue = opts.transform ? opts.transform(value) : value;
      
      // 使用自定义名称或原属性名
      const key = opts.name || propertyKey;
      result[key] = serializedValue;
    }
    
    return result;
  }
}

// 使用示例
class Product {
  @Serialize({ name: "product_name" })
  name: string;
  
  @Serialize({ transform: (v: number) => `$${v.toFixed(2)}` })
  price: number;
  
  @Serialize({ optional: true })
  description?: string;
  
  constructor(name: string, price: number, description?: string) {
    this.name = name;
    this.price = price;
    this.description = description;
  }
}

const product = new Product("Laptop", 999.99, "高性能笔记本");
const serialized = Serializer.serialize(product);
console.log(serialized);
// { product_name: "Laptop", price: "$999.99", description: "高性能笔记本" }

高级应用场景

场景 1:ORM 实体定义

使用装饰器和元数据定义数据库实体:

typescript
import "reflect-metadata";

// 列装饰器工厂
function Column(options?: { type?: string; nullable?: boolean; primaryKey?: boolean }) {
  return function (target: any, propertyKey: string) {
    const columns = Reflect.getMetadata("columns", target) || {};
    columns[propertyKey] = {
      type: options?.type || "string",
      nullable: options?.nullable || false,
      primaryKey: options?.primaryKey || false,
      ...options
    };
    Reflect.defineMetadata("columns", columns, target);
  };
}

// 表装饰器工厂
function Table(tableName: string) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    Reflect.defineMetadata("table", tableName, target);
    return target;
  };
}

@Table("users")
class User {
  @Column({ type: "number", primaryKey: true })
  id: number;
  
  @Column({ type: "string" })
  name: string;
  
  @Column({ type: "string", nullable: true })
  email?: string;
  
  @Column({ type: "date" })
  createdAt: Date;
}

// 获取表信息
const tableName = Reflect.getMetadata("table", User);
const columns = Reflect.getMetadata("columns", User.prototype);
console.log(tableName); // "users"
console.log(columns);
// {
//   id: { type: "number", primaryKey: true, nullable: false },
//   name: { type: "string", nullable: false },
//   email: { type: "string", nullable: true },
//   createdAt: { type: "date", nullable: false }
// }

场景 2:路由装饰器系统

创建类似 Express 或 NestJS 的路由装饰器系统:

typescript
import "reflect-metadata";

// HTTP 方法装饰器工厂
function createHttpMethodDecorator(method: string) {
  return function (path: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      const routes = Reflect.getMetadata("routes", target) || [];
      routes.push({
        method,
        path,
        handler: propertyKey
      });
      Reflect.defineMetadata("routes", routes, target);
    };
  };
}

// 便捷方法
const Get = createHttpMethodDecorator("GET");
const Post = createHttpMethodDecorator("POST");
const Put = createHttpMethodDecorator("PUT");
const Delete = createHttpMethodDecorator("DELETE");

// 控制器装饰器
function Controller(basePath: string) {
  return function <T extends { new (...args: any[]): {} }>(target: T) {
    Reflect.defineMetadata("basePath", basePath, target);
    return target;
  };
}

@Controller("/api/users")
class UserController {
  @Get("/")
  getAllUsers() {
    return { users: [] };
  }
  
  @Get("/:id")
  getUserById(id: string) {
    return { id, user: {} };
  }
  
  @Post("/")
  createUser(data: any) {
    return { created: true, data };
  }
  
  @Put("/:id")
  updateUser(id: string, data: any) {
    return { updated: true, id, data };
  }
  
  @Delete("/:id")
  deleteUser(id: string) {
    return { deleted: true, id };
  }
}

// 获取路由信息
const basePath = Reflect.getMetadata("basePath", UserController);
const routes = Reflect.getMetadata("routes", UserController.prototype);
console.log(basePath); // "/api/users"
console.log(routes);
// [
//   { method: "GET", path: "/", handler: "getAllUsers" },
//   { method: "GET", path: "/:id", handler: "getUserById" },
//   { method: "POST", path: "/", handler: "createUser" },
//   { method: "PUT", path: "/:id", handler: "updateUser" },
//   { method: "DELETE", path: "/:id", handler: "deleteUser" }
// ]

注意事项

注意

  1. 性能影响:元数据会增加代码体积和运行时开销,特别是在大量使用的情况下。在生产环境中需要权衡。

  2. 类型安全:元数据操作在编译时无法完全验证,需要开发者自行确保类型安全。

  3. 库依赖:使用元数据功能需要安装 reflect-metadata 库,这会增加项目依赖。

  4. 浏览器兼容性reflect-metadata 使用了一些较新的 JavaScript 特性,可能需要 polyfill 支持。

  5. 执行顺序:装饰器组合的执行顺序可能影响最终行为,需要仔细理解执行顺序规则。

  6. 元数据键冲突:使用自定义元数据键时,需要注意避免键名冲突,建议使用 Symbol 或命名空间。

最佳实践

  1. 使用 Symbol 作为元数据键:避免键名冲突,提高代码安全性。

  2. 封装元数据操作:创建工具函数封装常用的元数据操作,提高代码复用性。

  3. 文档化装饰器:为自定义装饰器添加清晰的文档,说明参数、行为和元数据使用。

  4. 类型定义:为装饰器工厂提供准确的类型定义,提高类型安全性。

  5. 测试覆盖:充分测试装饰器组合和元数据功能,确保行为符合预期。

  6. 性能监控:在性能敏感的场景中,监控装饰器和元数据的使用对性能的影响。

  7. 渐进式采用:从简单的装饰器工厂开始,逐步增加复杂度,避免过度设计。

相关链接

基于 VitePress 构建