使用基于装饰器的依赖注入创建 Typescript 应用程序💉

作为 Node.js 和 TypeScript 的忠实粉丝,我喜欢这些技术为构建应用程序提供快速灵活的方法。然而,这种灵活性可能是一把双刃剑。代码很快就会变得混乱,随着时间的推移,可维护性和可读性会下降。

在广泛使用 Spring (Java) 和 NestJS (TypeScript) 后,我意识到依赖注入 (DI) 是一种长期维护代码质量的强大模式。考虑到这一点,我开始探索如何创建一个 TypeScript 库作为 Node.js 项目的基础。我的目标是创建一个库,它强制采用基于组件的开发方法,同时保持灵活性并易于扩展以适应各种用例。这就是我想出 🍋 Lemon DI 的原因。

工作原理

Lemon DI 背后的核心思想与 NestJS 非常相似(尽管命名约定不同)。所有用 `@Component` 修饰的类都会自动成为可注入的组件,而非类实体(例如类型或接口)可以使用工厂(`@Factory`)实例化并使用唯一令牌注入。

让我们看一个使用 TypeORM 与 SQLite 数据库集成的示例。

设置

首先安装所需的依赖项:

npm install @lemondi/core reflect-metadata sqlite3 tsc typeorm typescript class-transformer

由于 TypeORM 是一个外部库,我们将使用工厂创建数据源:

// factory/datasource.ts
import {Factory, FilesLoader, Instantiate} from "@lemondi/core";
import {DataSource} from "typeorm";

// @Factory decorator marks this class as a provider of components through functions
@Factory()
export class DataSourceFactory {
    // @Instantiate decorator marks this function as a provider of a component
  @Instantiate({
    qualifiers: [DataSource] // This tells DI that this function creates a DataSource component
  })
  // This is an async function, which means the DI system will wait for it to resolve before using the component
  async createDatasource() {
    // create DataSource instance
    const ds = new DataSource({
      type: "sqlite", // use sqlite for simplicity, but this works perfectly with any other DB
      database: ":memory:",
      synchronize: true, // Automatically create tables on startup
      // load all models
      entities: [FilesLoader.buildPath(__dirname, "..", "models", "*.entity.{js,ts}")],
    });

    await ds.initialize(); // Initialize the DataSource before using it
    return ds;
  }
}

现在我们有了 DataSource 组件,让我们定义一个模型和一个服务来与其交互:

// models/user.entity.ts
import {Column, Entity, PrimaryGeneratedColumn} from "typeorm";
import {plainToClass} from "class-transformer";

// This is a standard TypeORM entity declaration
@Entity({ name: "users" })
export class User {
  @PrimaryGeneratedColumn("uuid")
  id?: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  static fromJson (json: User) {
    return plainToClass(User, json);
  }
}
// services/UsersService.ts
import {Component} from "@lemondi/core";
import {DataSource, Repository} from "typeorm";
import {User} from "../models/user.entity";

// This class is marked as component, it will automatically map itself during the dependency injection step
@Component()
export class UsersService {
  private repository: Repository;

  // The component constructor is where the dependency injection happens
  // For each argument, the DI system will look for a component and provide it (the components are instantiated automatically when needed)
  constructor(
    // Here we tell DI system that we need DataSource instance (which is exported from our factory)
    // It is completely transparent for us that the DataSource component is async
    dataSource: DataSource,
  ) {
    this.repository = dataSource.getRepository(User);
  }

  save(user: User) {
    return this.repository.save(user);
  }

  find() {
    return this.repository.find();
  }
}

现在我们已经有了数据库和用户服务,我们可以启动我们的应用程序了:

import "reflect-metadata"; // this is required to emit classes metadata

import {Component, FilesLoader, OnInit, start} from "@lemondi/core";
import {UsersService} from "./services/users";
import {User} from "./models/user.entity";

@Component()
class App {
  constructor(
    private usersService: UsersService,
  ) { }

  // @OnInit decorator only works for components directly imported in `start`
  // @OnInit decorator tells the system to execute this method after the component is instantiated
  @OnInit()
  async onStart() {
    // create a new entry
    const user = User.fromJson({
      lastName: "Last",
      firstName: "First",
    });

    // save user in DB
    await this.usersService.save(user);

    // fetch user from DB
    const users = await this.usersService.find();
    console.log(users); // will print data fetched from DB
  }
}

// start method is required to start the app
start({
  importFiles: [
    // since there is no need to reference factories in the code, we need to tell our DI system to import those files to make sure they are accessible
    FilesLoader.buildPath(__dirname, "factories", "**", "*.js"),
  ],
  modules: [App], // The entry point; classes listed here will be instantiated automatically
});

TypeScript 配置

为了启用装饰器并确保一切按预期工作,请将以下内容添加到“tsconfig.json”中:

{
  "compilerOptions": {
    "lib": ["es5", "es6", "dom"],
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": "./dist",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

最后,运行以下命令来编译并执行应用程序:

tsc && node ./dist/app.js

最后的想法

⚠️ 重要提示:请注意,此库仍处于早期阶段,尚不应用于生产应用程序。这是我为乐趣和探索 TypeScript 中的装饰器而创建的原型。您可以在此处找到完整的示例代码。