掘金 后端 ( ) • 2024-04-17 10:26

使用 TypeScript 从零搭建自己的 Web 框架:依赖注入

在构建 Web 框架时,依赖注入(Dependency Injection, DI)是一个非常重要的概念。它允许我们在不改变类代码的情况下,将类的依赖从外部注入到类中,从而提高了代码的可测试性和可维护性。本文将介绍依赖注入的基本概念,并通过一个具体的例子,展示如何在 TypeScript 中实现依赖注入。

什么是依赖注入?

依赖注入是一种设计模式,它的核心思想是将一个对象所依赖的其他对象以参数的形式传入,而不是在对象内部通过 new 关键字来创建。这样做的好处是,可以轻松地替换依赖对象,使得代码更加灵活和可测试。

UserController 中注入 UserService 的例子

假设我们有一个 IUserService 接口和一个实现了该接口的 UserService 类,以及一个需要依赖 UserServiceUserController 类。

// IUserService.ts
interface IUserService {
  getUserById(id: number): Promise<User>;
}

// UserService.ts
class UserService implements IUserService {
  async getUserById(id: number): Promise<User> {
    // 模拟从数据库获取用户信息的操作
    return { id, name: `User ${id}` };
  }
}

// IUserController.ts
interface IUserController {
  getUser(id: number): Promise<User>;
}

// UserController.ts
class UserController implements IUserController {
  constructor(private readonly userService: IUserService) {}

  async getUser(id: number): Promise<User> {
    return this.userService.getUserById(id);
  }
}

接下来,我们需要使用 IoC 容器来管理服务的注册和获取。同时,我们将使用 TypeScript 的反射 API 来动态获取 UserController 构造函数的参数类型,并从 IoC 容器中获取对应参数的实例。

// index.ts
import 'reflect-metadata';

function constructController<T>(ControllerClass: new (...args: any[]) => T, iocContainer: typeof IoCContainer): T {
  // 获取 ControllerClass 的构造函数参数类型
  const constructorTypes = Reflect.getMetadata('design:paramtypes', ControllerClass);

  // 假设服务名称与接口名相同(以 I 开头)
  const dependencies = constructorTypes.map(type => {
    const serviceName = type.name.replace(/^I/, ''); // 去掉接口名前的 I
    return iocContainer.resolve(serviceName);
  });

  // 使用 Reflect.construct 创建 Controller 实例
  return Reflect.construct(ControllerClass, dependencies) as T;
}

Reflect.getMetadata('design:paramtypes', ControllerClass) 这行代码的作用是获取 ControllerClass 构造函数的参数类型信息。这里 'design:paramtypes' 是一个特殊的元数据键,它对应的是 TypeScript 编译器在编译时自动添加的关于构造函数参数类型的元数据。

当你使用 TypeScript 定义一个类,并且这个类的构造函数带有参数时,TypeScript 编译器会生成一些额外的元数据来记录这些参数的类型。这些元数据在编译后的 JavaScript 代码中是不可见的,但是可以通过反射 API 在运行时访问。

通过 Reflect.getMetadata('design:paramtypes', ControllerClass),你可以得到一个数组,数组中的每个元素代表 ControllerClass 构造函数的一个参数的类型。这些类型通常是构造函数的函数对象本身(对于类类型),而不是字符串或其他表示形式。

这在你想要动态创建类的实例,并且需要知道构造函数的参数类型以便正确传递依赖时特别有用。通过获取这些类型信息,你可以从 IoC 容器中查找相应的实例,并传递给构造函数。

需要注意的是,为了使用 Reflect.getMetadata 和相关的反射 API,你需要在项目中包含 reflect-metadata 这个库,并且需要在 TypeScript 的配置文件中(通常是 tsconfig.json)启用发射元数据(emitDecoratorMetadata 和 experimentalDecorators)的选项。

现在,我们需要在 IoC 容器中注册 UserService,并使用 constructController 函数来创建 UserController 的实例:

// app.ts
import { IoCContainer } from './IoCContainer';
import { IUserController, UserController } from './UserController';
import { UserService } from './UserService';
import { constructController } from './utils';

// 注册 UserService 到 IoC 容器
IoCContainer.register('UserService', new UserService());

// 创建 UserController 实例并注入 UserService
const userController: IUserController = constructController(UserController, IoCContainer);

// 现在可以使用 userController 实例进行操作了
userController.getUser(1).then(user => {
  console.log(user);
});

在上面的例子中,UserController 构造函数中的 IUserService 类型参数通过反射被获取,然后从 IoC 容器中获取了对应的 UserService 实例,并将其作为参数传递给 Reflect.construct 来创建 UserController 的实例。

通过这种方式,我们可以轻松地替换 UserService 的实现,只需在 IoC 容器中注册新的服务即可,而无需修改 UserController 的代码。这就是依赖注入带来的好处之一,它使得代码更加灵活和可维护。

结合文件扫描和动态导入,我们可以轻松实现自动扫描项目代码中所有的控制器文件并自动注入相关依赖项。