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