文章目录

TypeScript装饰器元编程在NestJS依赖注入中的应用

发布于 2026-04-03 06:58:30 · 浏览 6 次 · 评论 0 条

TypeScript装饰器元编程在NestJS依赖注入中的应用

NestJS 使用 TypeScript 的装饰器(Decorator)机制实现依赖注入(Dependency Injection),这是其核心架构能力之一。通过装饰器元编程,框架能在运行前收集类、方法或属性的元数据,并据此自动创建和管理对象依赖关系。你无需手动实例化服务,只需用特定装饰器标记即可。


理解装饰器与元数据反射

TypeScript 装饰器是一种特殊的函数,可附加到类、方法、属性或参数上,在编译时被调用。NestJS 利用这一机制配合 reflect-metadata 库,在运行时读取这些装饰器写入的元数据,从而决定如何构建依赖图。

启用装饰器元数据支持

  1. 安装 reflect-metadata

    npm install reflect-metadata
  2. 在应用入口文件(如 main.ts)顶部导入

    import 'reflect-metadata';
  3. 确保 tsconfig.json 中启用了装饰器和元数据发射

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

创建一个可注入的服务

定义服务类并用 @Injectable() 标记

  1. 新建文件 cats.service.ts

    import { Injectable } from '@nestjs/common';
    
    @Injectable()
    export class CatsService {
      private cats: string[] = ['Whiskers', 'Mittens'];
    
      findAll(): string[] {
        return this.cats;
      }
    }

    @Injectable() 装饰器告诉 NestJS:这个类可以被其他组件自动注入。

  2. 将服务注册到模块中
    cats.module.ts 中添加 CatsServiceproviders 数组:

    import { Module } from '@nestjs/common';
    import { CatsService } from './cats.service';
    
    @Module({
      providers: [CatsService],
    })
    export class CatsModule {}

在控制器中注入服务

使用构造函数注入自动获取服务实例

  1. 创建控制器 cats.controller.ts

    import { Controller, Get } from '@nestjs/common';
    import { CatsService } from './cats.service';
    
    @Controller('cats')
    export class CatsController {
      constructor(private readonly catsService: CatsService) {}
    
      @Get()
      findAll(): string[] {
        return this.catsService.findAll();
      }
    }

    NestJS 检测到 CatsController 的构造函数参数类型为 CatsService,并在模块上下文中查找已注册的提供者,自动传入其实例。

  2. 将控制器加入模块

    @Module({
      controllers: [CatsController],
      providers: [CatsService],
    })
    export class CatsModule {}

自定义提供者与非默认注入

当需要更复杂的依赖关系(如接口实现、动态值、别名等),可使用自定义提供者。

使用 @Inject() 显式指定注入令牌

  1. 定义一个抽象类或接口标识符

    // database.interface.ts
    export abstract class DatabaseService {
      abstract connect(): void;
    }
    
    export const DATABASE_SERVICE = Symbol('DatabaseService');
  2. 实现具体服务并绑定到符号令牌

    // mysql.service.ts
    import { Injectable } from '@nestjs/common';
    import { DatabaseService } from './database.interface';
    
    @Injectable()
    export class MysqlService implements DatabaseService {
      connect() {
        console.log('Connected to MySQL');
      }
    }
  3. 在模块中注册自定义提供者

    import { Module } from '@nestjs/common';
    import { MysqlService } from './mysql.service';
    import { DATABASE_SERVICE } from './database.interface';
    
    @Module({
      providers: [
        {
          provide: DATABASE_SERVICE,
          useClass: MysqlService,
        },
      ],
    })
    export class DatabaseModule {}
  4. 在消费者中使用 @Inject() 注入

    import { Injectable, Inject } from '@nestjs/common';
    import { DATABASE_SERVICE, DatabaseService } from './database.interface';
    
    @Injectable()
    export class AppService {
      constructor(
        @Inject(DATABASE_SERVICE) private db: DatabaseService,
      ) {}
    
      init() {
        this.db.connect();
      }
    }

    此处 @Inject(DATABASE_SERVICE) 明确告诉 NestJS:不要根据类型推断,而是使用 DATABASE_SERVICE 这个令牌查找提供者。


方法与参数装饰器的实际用途

除了类级别的依赖注入,NestJS 还广泛使用方法和参数装饰器处理 HTTP 请求。

解析请求数据无需手动提取

  1. 使用 @Body()@Param() 等装饰器直接获取数据

    @Post()
    create(@Body('name') name: string) {
      return this.catsService.create(name);
    }
    
    @Get(':id')
    findOne(@Param('id') id: string) {
      return this.catsService.findOne(id);
    }

    这些装饰器由 NestJS 内部实现,利用元数据在运行时从请求对象中提取对应字段并注入到方法参数。

  2. 自定义参数装饰器(高级用法)
    若需复用复杂解析逻辑,可创建自己的参数装饰器:

    import { createParamDecorator, ExecutionContext } from '@nestjs/common';
    
    export const User = createParamDecorator(
      (data: unknown, ctx: ExecutionContext) => {
        const request = ctx.switchToHttp().getRequest();
        return request.user; // 假设认证中间件已设置 user
      },
    );

    使用时:

    @Get('/profile')
    getProfile(@User() user: any) {
      return user;
    }

装饰器执行顺序与元数据合并

多个装饰器作用于同一目标时,执行顺序有明确规定:

  • 类装饰器:从最内层向外执行。
  • 方法/属性/参数装饰器:按代码书写顺序从上到下执行。

NestJS 内部通过 Reflect.defineMetadata()Reflect.getMetadata() 存取元数据。例如:

@Injectable()
@Controller('test')
export class TestController {}

等价于:

let TestController = class TestController {};
TestController = Injectable()(TestController);
TestController = Controller('test')(TestController);

每次装饰器调用都会向该类附加新的元数据键值对,NestJS 启动时统一读取这些信息构建依赖图。


验证依赖注入是否生效

检查常见错误

问题现象 可能原因 解决方案
Nest can't resolve dependencies 服务未在模块 providers 中注册 将服务添加到对应模块的 providers 数组
构造函数参数为 undefined 未启用 emitDecoratorMetadata 检查 tsconfig.json 中相关选项是否开启
自定义提供者未被识别 令牌不匹配或未正确使用 @Inject() 确保 provide 值与 @Inject() 参数完全一致

扩展:工厂提供者与异步初始化

对于需要异步创建的服务(如数据库连接池),使用 useFactory

  1. 定义工厂函数

    const databaseProvider = {
      provide: 'DATABASE_CONNECTION',
      useFactory: async () => {
        const connection = await createConnection({
          type: 'mysql',
          host: 'localhost',
          // ...其他配置
        });
        return connection;
      },
    };
  2. 在模块中注册

    @Module({
      providers: [databaseProvider],
    })
    export class AppModule {}
  3. 注入时使用字符串令牌

    constructor(
      @Inject('DATABASE_CONNECTION') private connection: any,
    ) {}

NestJS 会等待所有异步提供者完成后再启动应用。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文