背景与问题界定

TypeScript 装饰器经历了从实验性特性到 ECMAScript 标准的演进,目前 5.x 版本同时支持传统的实验性装饰器和 ESMirror 规范的新式装饰器。然而,许多团队对装饰器的认知仍然停留在"给类加个标记"的层面,没有充分发挥其在 AOP 侧切面编程中的能力。实际工程中,基础设施代码与业务逻辑的横切关注点混杂——每个 Controller 方法开头重复的权限检查、参数校验、日志记录形成了大量模板代码。Decorator 正好是解决这类横切问题的天然工具。但同时也面临着类型安全、装饰器执行顺序、以及新旧装饰器语法兼容性等工程挑战。

目标拆解与工程约束

  • 装饰器类型安全:自定义装饰器应使用泛型保留被装饰目标的原始类型签名,不破坏 TypeScript 的静态类型推导。
  • 执行顺序可预测:多个装饰器叠加时,执行顺序遵循"自下而上应用、自外向内执行"的契约,团队应有统一规范记录装饰器组合的语义。
  • 新旧装饰器兼容:项目在迁移到 Stage 3 装饰器过程中,提供过渡工具确保两种装饰器语法能在不同模块中共存。
  • 运行时开销可控:装饰器的元数据解析和代理创建不应显著影响热路径性能,需要提供 Benchmark 基准。

方案设计

我们将装饰器的应用场景分为四类,分别对应四种装饰器类型:类装饰器用于依赖注入容器注册和单例管理;方法装饰器用于日志切片、权限校验和重试控制;访问器装饰器用于计算属性的缓存和延迟加载;参数装饰器用于参数验证和依赖注入的参数标记。

以方法装饰器为例,一个通用的日志装饰器实现可以在不侵入业务逻辑的前提下,自动采集方法调用的入参、返回值和耗时:

function Log(level: 'info' | 'warn' | 'error' = 'info') {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value
    descriptor.value = function (...args: any[]) {
      const start = performance.now()
      try {
        const result = original.apply(this, args)
        if (result instanceof Promise) {
          return result.then(r => {
            logger.log(level, `${propertyKey} succeeded`, { args, duration: performance.now() - start })
            return r
          }).catch(err => {
            logger.error(`${propertyKey} failed`, { args, error: err })
            throw err
          })
        }
        logger.log(level, `${propertyKey} succeeded`, { args, duration: performance.now() - start })
        return result
      } catch (err) {
        logger.error(`${propertyKey} failed`, { args, error: err })
        throw err
      }
    }
    return descriptor
  }
}

对于依赖注入场景,我们使用类装饰器 + reflect-metadata 实现一个轻量级的 DI 容器。通过在构造函数的参数上使用 @Inject 参数装饰器标记依赖,容器在实例化时自动解析依赖树并注入。这种方法在 NestJS 中已被大规模验证,但在纯前端项目中同样适用——只需要一个简洁的容器实现和几个装饰器辅助函数。

新式装饰器(Stage 3)引入了不同的 API 签名。例如方法装饰器变为接收 target: Function, context: ClassMethodDecoratorContext,且必须通过 context.addInitializer 注册初始化逻辑。我们在迁移策略上采用"API 适配层"模式:封装一个统一的高阶函数 decorate,接收新旧两种回调函数的 map,根据运行环境自动选择正确的装饰器实现。

实施路径与关键决策

  • 明确装饰器适用范围:装饰器应作用于横切关注点和基础设施代码,不应用于替换普通的函数组合或高阶函数。
  • 建立装饰器文档目录:在项目 wiki 中维护一个"可用装饰器清单",标注每个装饰器的参数含义、执行顺序和性能消耗。
  • 装饰器单元测试策略:使用装饰器后的类或方法在测试中只需关注业务逻辑本身,装饰器行为通过独立的测试文件验证。
  • 迁移路径规划:先在非核心模块试用 Stage 3 装饰器,积累经验后再制定全量迁移计划,周期为 2 个 Sprint。

验证指标与可持续迭代

方法装饰器引入后,重复的日志和权限校验代码应减少 80% 以上。每个 Controller 方法的行数降低至 3 ~ 8 行(仅包含真正的业务逻辑)。装饰器本身的单测覆盖率达到 100%。后续迭代方向:探索基于装饰器的限流、熔断组件,以及装饰器元数据的 DevTools 可视化插件。

工程落地思考

装饰器是 TypeScript 最接近"声明式编程"的工具之一。它让我们可以写下 @Authorize('admin') 这样可读性极高的声明,而不需要关心底层实现。但装饰器也是一把双刃剑——过度使用会让代码的执行流程变得隐晦,debug 时需要通过堆栈回溯才能理解某个行为的来源。在工程中应用装饰器的黄金法则是:装饰器的作用应该是"增强"而非"改变"。如果一个装饰器让函数的行为变得与签名不一致,那它就是一个反模式。