找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1776

积分

0

好友

234

主题
发表于 12 小时前 | 查看: 0| 回复: 0

什么是依赖注入?

  • 什么是依赖注入?
  • 装饰器在依赖注入中担任什么角色?
  • 如何通过 Decorator Stage 3 实现依赖注入框架?

Node.js 生态中,NestJSAngular 是两个非常经典的基于依赖注入的框架。

让我们通过一个现实中的例子来理解。假设有一个书店,它除了卖书,可能还卖文具、器材等。先定义这个书店的类:

class BookStore {
    book() {
        return 'book';
    }
    //...
}

在某些场景下,书店可能不止有一种书,文具也可能有多种类型。如果持续在 BookStore 类中添加方法,它很快就会变得臃肿不堪。这时,就需要依赖注入技巧来解耦。

最简单的依赖注入形式是,将书、文具等职责封装到独立的类中。

class BookCase1 {
    book1() {
        // ...
    }
}

class BookStore {
    constructor(private bookCase: BookCase1) {}
    book() {
        return this.bookCase.book1();
    }
    // ...
}

// execute
const bookstore = new BookStore(new BookCase1());
bookstore.book();

这个例子或许不够完美,但其底层逻辑是清晰的:通过外部的类为内部类提供依赖,从而实现职责分离与可配置性。

NestJS 这类成熟的前端框架中,依赖注入的实现方式如下:

// foo.service.ts
@Injectable()
export class FooService {
    foo() {
        // ...code
    }
}

// app.service.ts
@Injectable()
export class BarService {
    constructor(private fooService: FooService) {}
    bar() {
        this.fooService.foo();
    }
    // ...code
}

// app.module.ts
@Module({
    providers: [AppService, FooService],
    exports: [AppService]
})
export class AppModule {}

BarService 中,bar 方法调用了来自 FooService 类的 foo 方法。

装饰器扮演了什么角色?

可以看到,NestJS 处理依赖注入的方式与上面的简单例子不同。它通过 @Module 装饰器来创建和管理不同的服务模块,并为 providers 内部的模块提供所需的服务实例。

那么,这些装饰器(@Module@Injectable)究竟扮演了什么角色?

简单来说,装饰器主要用于提供元数据标记,以便框架进行依赖分析和注入。

例如,@Module 装饰器为类标记了“这是一个模块”的元数据。当依赖注入容器检测到该元数据时,就能根据其中定义的 providers 信息来执行依赖注入。

而对于 @Injectable,在 Decorator Stage 2 的语法下,可以结合参数装饰器来标记需要注入的依赖。

Stage 2 的装饰器语法是目前 TypeScript 官方文档描述的主流实现方式。

@Injectable()
class FooService {
    constructor(@Inject('barService') private barService: BarService) {}
    // ...
}

@Module({
    providers: [
        FooService,
        { provide: 'barService', useClass: BarService }
    ]
})
class AppModule {}

在依赖解析阶段,框架已经知晓模块间的依赖关系,知道何时注入以及通过哪个 token(本例中即 provide: 'barService' 这个值)找到正确的依赖进行注入。上面的写法是一种完整的、显式的依赖声明形式。

以上分析都基于 Stage 2 的装饰器语法。而在 Stage 3 中,装饰器语法发生了显著变化:新增了 accessor 装饰器,但移除了参数装饰器。

如何用 Stage 3 装饰器实现 DI 框架?

accessor 装饰器可以理解为类属性的 getter/setter 语法糖,它同样能提供元数据标记。

最初我认为,在 Stage 3 版的装饰器基础上实现一个 DI 框架是可行的,但实际尝试后遇到了不少问题。

先考虑一个最简单的实现场景:

@Injectable()
class BarService {
    barFunc() {
        return 'bar';
    }
}

@Injectable()
class FooService {
    constructor(private bar: BarService) {}

    fooFunc() {
        return this.bar.barFunc();
    }
}

@Module({
    providers: [BarService, FooService]
})
class AppModule {}

bootstrap() {
    const app = await AppContainerFactory.create(AppModule);
    const foo = app.get(FooService);
}

bootstrap();

这是我为这个 DI 框架设计的最初构想。运行 AppContainerFactory,通过导入的 AppModule 进行依赖分析,最终获取到 FooService 实例,并能解析其内部依赖,供 fooFunc 方法调用。

那么,回到核心问题:如何通过 Decorator Stage 3 实现依赖注入框架?

首先,虽然 Stage 3 装饰器移除了参数装饰器,但这不代表无法获取构造函数的参数信息。我们可以利用 accessor 装饰器来实现这一目的。

@Injectable()
class AppService {
    @Inject('demoService')
    accessor #DemoService: DemoService;
    constructor(demo: DemoService) {
        this.#DemoService = demo;
    }
}

这里的 @Inject 装饰器主要用于在依赖解析时提供一个标记。如果没有这个标记,运行时将无法确定应该注入哪个模块。这相当于将 Stage 2 的参数装饰器功能,转移到了属性(accessor)装饰器上来实现。

如果仅仅是为了实现一个简易的依赖注入框架,上述写法没有问题。然而,当遇到像 NestJSController 这样的场景时,问题就出现了——至少目前我还没想出优雅的解决方案。

@Controller()
class AppController {
    @Get()
    getHello(@Query() query: string) {
        return `query: ${query}`;
    }
}

这里在方法参数上使用了装饰器(@Query()),这属于参数装饰器的范畴,而 Stage 3 并不支持参数装饰器。如何在新的语法规范下处理这类 Web 框架常见的需求,是一个待解的难题。

这次在 云栈社区 分享的探索过程,记录了我尝试基于新标准构建底层框架时遇到的真实挑战。技术规范的演进往往伴随着取舍,而如何在新约束下找到平衡点,正是开发者需要不断思考的。

参考链接

  • stage3 装饰器使用参考 2ality.com/2022/10/javascript-decorators
  • typescript支持stage3装饰器的blog devblogs.microsoft.com/typescript/announcing-typescript-5-0
  • stage3的metadata polyfill github.com/daomtthuan/polyfill-symbol-metadata



上一篇:为什么真正的学习在于“提取”而非“听懂”?
下一篇:UI/UX设计师作品集深度解析:从B端后台到App设计的实战案例
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-25 20:31 , Processed in 0.304037 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表