导读:本期聚焦于下班再修创作的《如何使用TypeScript装饰器实现统一日志打印与异常捕获?》,敬请观看详情。业务代码里散落着大量console.log和try catch,不仅重复冗余,还容易遗漏关键信息,后续维护成本很高。本文介绍一种基于TypeScript装饰器的切面式方案,通过方法装饰器在函数执行前后自动注入日志,在异常抛出时统一捕获并记录上下文,让业务逻辑与横切关注点彻底解耦。文章涵盖装饰器的底层原理、元数据reflect-metadata的使用、日志装饰器与异常捕获装饰器的完整实现,以及在类、方法、属性等不同场景下的落地细节,并给出生产环境中关于异步方法、错误分类和日志分级的实践建议。

在TypeScript项目中写业务逻辑时,日志打印和异常捕获是最典型的横切关注点。同一个项目里,有人喜欢在每个方法开头打印一行入参,有人在catch里console.error一下就完事,风格五花八门,而且大量的样板代码淹没了真正的业务逻辑。装饰器提供了一种优雅的解决思路:把日志和异常处理封装成可复用的注解,贴在方法上即可生效,业务代码保持干净清爽。本文将从头到尾讲清楚这套方案的原理与实现。

如何使用TypeScript装饰器实现统一日志打印与异常捕获?

一、装饰器的底层原理:它到底改写了什么

要理解装饰器方案,先得明白装饰器在运行时的行为。装饰器本质上是一个高阶函数,接收被修饰的目标(类、方法、属性或参数),返回一个“加工后”的目标。对于方法装饰器而言,它拿到的是方法所属类的原型、方法名以及该方法的属性描述符,这意味着我们可以在不修改原方法代码的前提下,用一个新的函数把原方法包一层。

这个“包一层”的能力正是切面编程的核心。我们可以在包装函数里先记录入参、开始时间,再调用原方法拿到返回值,最后打印耗时和出参;如果原方法抛出异常,就在catch块里统一处理后选择重新抛出或吞掉。整个流程对业务代码完全透明,调用方感知不到任何变化。

需要注意的是,TypeScript 5.0之后官方支持了标准的Stage 3装饰器语法,与早期的experimentalDecorators行为有差异。本文以目前使用最广泛的实验性装饰器为准(NestJS、TypeORM等主流框架均基于此),在tsconfig.json中需要开启如下配置:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2017"
  }
}

其中emitDecoratorMetadata会额外把类型信息写入元数据,配合reflect-metadata库使用,可以让我们在运行时拿到参数类型、设计返回类型等信息,对日志的格式化非常有用。

二、实现统一日志打印装饰器

日志装饰器的目标是:方法被调用时自动打印类名、方法名、入参、返回值和执行耗时。这些信息足够排查绝大多数问题,而且不需要开发者手写一行日志代码。

实现时有个关键细节:TypeScript编译后方法会挂到原型上,描述符中的value就是原方法本体。我们要做的就是替换这个value为一个异步或同步的包装函数。由于现代业务大量使用async方法,包装函数最好统一按异步处理,用await调用原方法,这样同步和异步函数都能正确计时。

import 'reflect-metadata';

// 可选参数:自定义日志器与标签
export function Log(option?: { label?: string }) {
  return function (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    const className = target.constructor.name;

    descriptor.value = async function (...args: any[]) {
      const start = Date.now();
      const label = option?.label ?? `${className}.${propertyKey}`;

      console.log(`[${label}] 调用,入参:`, JSON.stringify(args));

      const result = await original.apply(this, args);
      const cost = Date.now() - start;

      console.log(`[${label}] 完成,耗时 ${cost}ms,返回:`, JSON.stringify(result));
      return result;
    };

    return descriptor;
  };
}

使用方式非常简洁,直接贴在方法上即可:

class OrderService {
  @Log({ label: '订单服务' })
  async createOrder(userId: number, sku: string) {
    // 纯业务逻辑,没有任何日志代码
    await new Promise(r => setTimeout(r, 120));
    return { orderId: 'A1001', userId, sku };
  }
}

有一点要提醒:JSON.stringify序列化入参时,如果参数包含循环引用会直接抛错,所以生产环境建议做一层保护,比如try catch包裹序列化,或者在参数是复杂对象时只打印其构造函数名和关键字段。另外,入参中可能包含密码、token等敏感信息,最好支持一个脱敏列表,把指定位置的参数替换为星号后再输出。

三、实现统一异常捕获装饰器

异常捕获装饰器的思路类似,但它关注的是错误路径。包装函数用try catch包裹原方法调用,捕获到异常后记录完整的错误堆栈和调用上下文,然后根据配置决定是抛出一个统一的业务异常对象,还是返回一个默认值保证流程不中断。

export function Catch(option?: {
  rethrow?: boolean;      // 是否重新抛出,默认true
  fallback?: any;         // 不抛出时返回的默认值
}) {
  return function (
    target: Object,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    const className = target.constructor.name;

    descriptor.value = async function (...args: any[]) {
      try {
        return await original.apply(this, args);
      } catch (err: any) {
        console.error(
          `[${className}.${propertyKey}] 异常:`,
          err instanceof Error ? err.stack : err,
          '\n调用上下文 入参:',
          JSON.stringify(args)
        );

        if (option?.rethrow === false) {
          return option?.fallback;
        }
        // 包装成统一错误对象再抛出,方便上层全局处理
        throw new Error(`[${className}.${propertyKey}] 执行失败: ${err.message}`);
      }
    };

    return descriptor;
  };
}

这里有一个容易被忽视的坑:异步方法抛出的异常是Promise拒绝,如果包装函数不加await直接调用原方法,catch块将永远捕获不到异常,错误会变成未处理的Promise rejection。所以即不确定原方法是否为异步,也建议统一按await处理,同步函数的返回值经过await不会有任何变化。

另一个实践建议是对错误做分类。可以定义一套错误码枚举,把数据库错误、网络超时、参数校验失败分别映射到不同的日志级别和提示文案,catch块里根据err.name或自定义的err.code判断类别,这样日志输出对排查问题更有针对性,也方便接入告警系统。

四、组合使用与生产环境注意事项

日志装饰器和异常装饰器可以叠加使用,TypeScript支持多个装饰器依次修饰同一个方法,执行顺序是从上到下应用、从下到上执行:

class PaymentService {
  @Catch({ rethrow: false, fallback: { success: false } })
  @Log({ label: '支付服务' })
  async pay(orderId: string) {
    const res = await fetch(`https://api.ipipp.com/pay/${orderId}`);
    if (!res.ok) throw new Error(`支付接口返回 ${res.status}`);
    return res.json();
  }
}

这种叠加方式下,外层的Catch能捕获到内层Log包装函数抛出的一切异常,两层切面互不干扰,职责清晰。

落地到生产环境还有几点需要权衡。首先是性能,装饰器包装会带来一层函数调用开销,对绝大多数业务来说可以忽略,但在每秒调用量十万级以上的热点路径上要谨慎。其次是日志量,全量打印入参出参在高并发下会产生海量日志,建议给Log装饰器加采样开关或按环境区分级别,开发环境打印全量,生产环境只打印慢请求和异常请求。最后是可测试性,装饰器依赖target.constructor.name,代码经过压缩混淆后类名可能变成单字母,最好在装饰器参数里显式传入业务标签,避免日志失去定位能力。

总结一下,装饰器方案的价值在于把重复的横切逻辑收敛到一处,新增方法时只需一个注解就能获得完整的日志和异常处理能力,排查问题时信息齐全,代码评审时也不再纠结日志风格。如果你的项目还在满天飞地手写console和try catch,不妨从核心Service类开始逐步迁移,收益会非常明显。

TypeScript装饰器日志打印异常捕获修改时间:2026-09-10 08:34:50

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0910/53928.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。