使用基于装饰器的依赖注入创建 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 中的装饰器而创建的原型。您可以在此处找到完整的示例代码。