Typescript: Nest.js 中使用声明文件定义依赖注入的类型

Nest.js 是一个流行的 Node.js 服务端框架,它基于 TypeScript 构建,并采用了依赖注入(Dependency Injection, DI)的设计模式。在 Nest.js 中,我们可以使用声明文件(declaration files)来定义依赖注入的类型,从而获得更好的类型检查和智能提示。本文将从浅入深,结合 Nest.js 的源码,讲解如何使用声明文件定义依赖注入的类型。

Nest.js 中的依赖注入

在深入探讨声明文件之前,我们先来了解一下 Nest.js 中的依赖注入。在 Nest.js 中,依赖注入是通过 @Injectable 装饰器实现的。当一个类被 @Injectable 装饰时,Nest.js 会自动创建该类的实例,并将其注入到需要该类实例的地方。

下面是一个简单的例子:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  getUser(id: number) {
    // ...
  }
}

@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) {}

  async validateUser(id: number) {
    const user = await this.userService.getUser(id);
    // ...
  }
}

在这个例子中,UserServiceAuthService 都是可注入的类。AuthService 的构造函数中声明了一个 UserService 类型的依赖,Nest.js 会自动创建 UserService 的实例,并将其注入到 AuthService 中。

在 Nest.js 源码中探索声明文件的使用

那么,Nest.js 内部是如何使用声明文件来定义依赖注入的类型呢?让我们一起探索一下 Nest.js 的源码。

在 Nest.js 的源码中,有一个 injectable.ts 文件,它定义了 @Injectable 装饰器:

export function Injectable(): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, { scope: Scope.DEFAULT }, target);
  };
}

这里使用了 Reflect.defineMetadata 来为类添加元数据,标记该类是可注入的。SCOPE_OPTIONS_METADATA 是一个常量,用于定义元数据的键名。

injector.ts 文件中,定义了 Injector 类,它负责创建和管理可注入类的实例:

export class Injector {
  // ...

  public get<T>(typeOrToken: Type<T> | Abstract<T> | string | symbol, options: InjectOptions = {}): T {
    const instanceKey = this.getInstanceKey(typeOrToken, options);
    const instanceWrapper = this.instances.get(instanceKey);
    if (instanceWrapper) {
      return instanceWrapper.instance;
    }

    // ...

    const instance = this.instantiateClass<T>(instanceWrapper, moduleRef, ctorHost);
    this.instances.set(instanceKey, new InstanceWrapper(instance));
    return instance;
  }

  // ...
}

这里的 get 方法用于获取可注入类的实例。它首先根据类型或令牌(token)获取实例的键名,然后从 instances 映射中获取实例。如果实例不存在,就调用 instantiateClass 方法创建实例,并将其存储到 instances 映射中。

instantiateClass 方法的定义如下:

private instantiateClass<T>(
  instanceWrapper: InstanceWrapper<T>,
  moduleRef: Module,
  ctorHost: Type<any> | undefined,
): T {
  const { metatype, inject } = instanceWrapper;

  // ...

  const instance = new metatype(...deps);
  instanceWrapper.instance = instance;
  return instance;
}

这里使用了 new 关键字来创建类的实例,并将依赖项作为构造函数的参数传入。metatype 表示要实例化的类,inject 表示类的依赖项。

声明文件在 Nest.js 依赖注入中的应用

为了更好地理解声明文件在 Nest.js 依赖注入中的应用,我们以一个实际的例子来说明。假设我们有一个 UserModule,它包含了 UserServiceAuthService:

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { AuthService } from './auth.service';

@Module({
  providers: [UserService, AuthService],
  exports: [UserService],
})
export class UserModule {}

这里使用 @Module 装饰器定义了 UserModule,并在 providers 属性中声明了 UserServiceAuthService

现在,我们要在其他模块中使用 UserService,可以像下面这样导入 UserModule:

import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { PostService } from './post.service';

@Module({
  imports: [UserModule],
  providers: [PostService],
})
export class PostModule {}

PostModule 中,我们导入了 UserModule,这样就可以在 PostService 中注入 UserService 了:

import { Injectable } from '@nestjs/common';
import { UserService } from './user/user.service';

@Injectable()
export class PostService {
  constructor(private readonly userService: UserService) {}

  createPost(userId: number, content: string) {
    const user = this.userService.getUser(userId);
    // ...
  }
}

这里的关键在于,PostService 是如何知道 UserService 的类型的呢?答案就在于 Nest.js 使用了声明文件来定义依赖注入的类型。

@nestjs/common 包的根目录下,有一个 index.d.ts 文件,它定义了 Nest.js 中常用的类型:

// @nestjs/common/index.d.ts

export * from './decorators';
export * from './enums';
export * from './exceptions';
export * from './http';
export * from './interfaces';
export * from './pipes';
export * from './services';
export * from './utils';

这里使用 export * from 语法导出了各个子目录下的类型定义。其中,./services 目录下的 index.d.ts 文件定义了 Injectable 类型:

// @nestjs/common/services/index.d.ts

export declare type Injectable<T = any> = T;

我们可以用类图来表示这个过程中的关系:

从图中可以看出,UserServiceAuthServicePostService 都是 Injectable 类型的子类型。PostService 依赖于 UserService,这种依赖关系是通过声明文件中的类型定义来实现的。

总结

本文结合 Nest.js 的源码,讲解了如何使用声明文件定义依赖注入的类型。我们首先了解了 Nest.js 中的依赖注入,然后深入 Nest.js 源码,探索了 @Injectable 装饰器和 Injector 类的实现。接着,我们通过一个实际的例子,说明了声明文件在 Nest.js 依赖注入中的应用。

Read more

Vue.js异步更新与nextTick机制深度解析(上篇)

Vue.js异步更新与nextTick机制深度解析(上篇)

本文目标 学完本文,你将能够: * 理解Vue.js为什么采用异步更新策略 * 掌握更新队列的设计思想和实现机制 * 深入理解Event Loop在Vue中的应用 * 了解nextTick的多种实现方式 系列导航 上一篇: Diff算法深度剖析 | 下一篇: Vue.js异步更新与nextTick机制(下篇) | 组件系统架构 引言:为什么DOM更新是异步的? 在Vue.js开发中,你可能遇到过这样的场景: // 场景1:连续修改数据 export default { data() { return { count: 0 } }, methods: { increment() { // 如果每次修改都立即更新DOM,会触发3次DOM更新 this.count++ // 触发一次? this.count++ // 触发一次? this.count++ // 触发一次? // 实际上:Vue只会触发一次DOM更新!

Vue.js组件系统架构深度解析

本文目标 学完本文,你将能够: * 理解Vue.js组件从创建到销毁的完整生命周期 * 掌握组件实例化和初始化的内部流程 * 深入理解父子组件通信的底层机制 * 学会实现完整的插槽系统(包括作用域插槽) * 掌握动态组件和异步组件的实现原理 * 应用组件级别的性能优化技巧 系列导航 上一篇: 异步更新与nextTick(下篇) | 下一篇: 状态管理模式 引言:组件是如何工作的? 在Vue.js开发中,我们每天都在使用组件。但你是否想过: // 当我们这样定义一个组件 const MyComponent = { data() { return { count: 0 } }, template: '<button @click="count++">{{ count }}</button>' } // 并使用它时 new Vue({ components: { MyComponent }, template:

Vue.js状态管理模式:构建可扩展的应用架构

本文目标 学完本文,你将能够: * 理解为什么大型应用需要状态管理 * 掌握Vuex的核心设计模式和实现原理 * 实现一个简化版的状态管理库 * 理解模块化和命名空间的设计思想 * 掌握大型应用的状态管理最佳实践 * 了解现代状态管理方案的演进 系列导航 上一篇: 组件系统架构 | 下一篇: 性能优化实践 1. 为什么需要状态管理? 1.1 组件通信的困境 在大型Vue.js应用中,组件间的通信会变得异常复杂: // 问题场景:多层级组件的状态共享 // GrandParent.vue <template> <Parent :user="user" @update-user="updateUser" /> </template> // Parent.vue <template> <Child

Vue.js依赖收集与追踪机制深度剖析

本文目标 学完本文,你将能够: * 理解Vue.js如何精确知道哪些组件需要更新 * 掌握Dep、Watcher、Observer三大核心类的协作机制 * 深入理解依赖收集的时机和完整过程 * 能够手写一个完整的依赖收集系统 * 解决实际开发中的依赖追踪问题 系列导航 上一篇: 响应式系统核心原理 | 下一篇: Virtual DOM实现详解 引言:为什么Vue知道哪些组件需要更新? 在使用Vue.js时,你是否想过这样一个问题:当我们修改一个数据时,Vue是如何精确地知道哪些组件用到了这个数据,并只更新这些组件的? // 假设有这样的场景 const app = new Vue({ data: { user: { name: 'John', age: 25 } } }); // 组件A只用到了user.name // 组件B只用到了user.age // 组件C同时用到了name和age // 当我们修改user.name时 app.user.name = 'Jane&