使用基于路由和 DI 编写的装饰器 Node.js 和 Express 应用

在构建现代 Node.js 和 Express 应用程序时,管理路由和依赖注入会变得越来越复杂。处理控制器、服务和中间件时如果没有清晰的分离,通常会导致代码更难维护。但是,使用正确的工具和设计模式,我们可以大大简化这个过程。

在本文中,我将引导您了解如何使用@lemondi 库构建具有**基于装饰器的路由**和**依赖注入**的 Node.js 应用程序。

为什么要使用装饰器和依赖注入?

装饰器是 TypeScript 和 JavaScript 中的一项强大功能,可让您向类、方法和属性添加元数据。借助装饰器,我们可以注释路由方法、定义其 HTTP 方法并处理依赖项注入,而无需编写复杂的样板代码。

依赖注入 (DI) 有助于管理服务实例化和注入其他组件的方式。它将组件彼此分离,使您的应用程序更加模块化且更易于测试。在我们的例子中,我们将使用 DI 来实现数据库连接和路由等服务。

**@lemondi** 库通过自动化 DI、处理装饰器和减少对样板代码的需求来简化流程。让我们深入了解它的工作原理!

1. 项目设置

在我们开始构建之前,让我们确保已经安装了所需的库:

npm init -y
npm install express reflect-metadata @lemondi/core @lemondi/scanner typeorm sqlite3 class-transformer
npm install --save-dev typescript @types/node @types/express

接下来,让我们通过创建 `tsconfig.json` 文件来设置 TypeScript:

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

此配置启用了 TypeScript 对装饰器和元数据反射的支持,这是 `@lemondi` 库所必需的。

2. 路由和注入的装饰器

routing.ts - 创建装饰器

首先,让我们创建两个装饰器:一个用于类(`@Router`)来定义路由器,另一个用于方法(`@Route`)来定义 HTTP 路由。

// file: src/decorators/routing.ts
import { createClassDecorator, createMethodDecorator } from "@lemondi/scanner";

// Enum for HTTP methods
export enum HttpMethod {
  GET = "get",
  POST = "post",
  PUT = "put",
  DELETE = "delete",
  PATCH = "patch",
  OPTIONS = "options",
}

// @Router decorator for class routing
export const Router = createClassDecorator<{ path: string }>("Router");

// @Route decorator for method routing
export const Route = createMethodDecorator<{ path: string; method: HttpMethod }>("Route");

这里我们使用 `@lemondi/scanner` 的 `createClassDecorator` 和 `createMethodDecorator` 来简化路由装饰器的创建。

3.定义数据源

datasource.ts - 数据源工厂

我们需要一种方法来创建和注入“数据源”(例如,用于连接到数据库)。这就是“@lemondi”库的“@Factory”和“@Instantiate”装饰器发挥作用的地方。

// file: src/factories/datasource.ts
import { Factory, FilesLoader, Instantiate } from "@lemondi/core";
import { DataSource } from "typeorm";

@Factory()
export class DataSourceFactory {
  @Instantiate({ qualifiers: [DataSource] })
  async createDatasource() {
    const ds = new DataSource({
      type: "sqlite",
      database: ":memory:",
      synchronize: true,
      entities: [FilesLoader.buildPath(__dirname, "..", "models", "*.entity.{js,ts}")],
    });

    await ds.initialize();
    return ds;
  }
}

`@Factory` 装饰器将 `DataSourceFactory` 标记为组件的提供者,而 `@Instantiate` 将 `createDatasource` 方法标记为 `DataSource` 组件的提供者。DI 将自动解析并注入所需的 `DataSource`。

4. 定义实体

user.entity.ts - TypeORM 实体

这是一个简单的 TypeORM 实体,用于定义“用户”模型。

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

@Entity({ name: "users" })
export class User {
  @PrimaryGeneratedColumn("uuid")
  id?: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  static fromJson(json: User) {
    return plainToClass(User, json);
  }
}

此实体表示数据库中具有字段“firstName”和“lastName”的“User”。我们还提供了一个实用函数“fromJson”,可轻松将 JSON 数据转换为“User”类的实例。

5.创建路由器

UsersRouter.ts - 定义路由

有了装饰器,我们现在可以定义“UsersRouter”类来处理与用户相关的路由。

// file: src/routers/UsersRouter.ts
import { HttpMethod, Route, Router } from "../decorators/routing";
import { UsersService } from "../services/UsersService";
import { Request } from "express";
import { User } from "../models/user.entity";

@Router({ path: "/users" })
export class UsersRouter {
  constructor(private readonly usersService: UsersService) {}

  @Route({ path: "/", method: HttpMethod.GET })
  getUsers() {
    return this.usersService.find();
  }

  @Route({ path: "/", method: HttpMethod.POST })
  createUser(req: Request) {
    const data = User.fromJson(req.body);
    return this.usersService.save(data);
  }
}

这里,`@Router` 装饰器定义基本路径 `/users`,`@Route` 装饰器处理用于检索和创建用户的 GET 和 POST 方法。

6.服务层

UsersService.ts - 处理业务逻辑

我们定义与数据库交互的服务。

// file: src/services/UsersService.ts
import { Component } from "@lemondi/core";
import { DataSource, Repository } from "typeorm";
import { User } from "../models/user.entity";

@Component()
export class UsersService {
  private repository: Repository;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(User);
  }

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

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

`UsersService` 类用 `@Component()` 修饰,其构造函数会自动注入 `DataSource` 实例。这样一来,服务无需任何手动实例化即可执行数据库操作。

7. 引导应用程序

app.ts——整合所有内容

最后,我们使用 `@lemondi` DI 系统初始化应用程序并动态绑定路由。

// file: src/app.ts
import "reflect-metadata";
import { Component, FilesLoader, instantiate, OnInit, start } from "@lemondi/core";
import * as express from "express";
import { findClassDecorators, findMethodDecorators, scan } from "@lemondi/scanner";
import { Route, Router } from "./decorators/routing";

@Component()
class App {
  @OnInit()
  async onStart() {
    const server = express();
    server.use(express.json());

    const routers = scan(Router);

    for (const router of routers) {
      const routerInstance = await instantiate(router);
      const [routerDecorator] = findClassDecorators(router, Router);

      for (const prop of Reflect.ownKeys(router.prototype)) {
        const [props] = findMethodDecorators(router, prop, Route);
        if (props) {
          const url = routerDecorator.decoratorProps.path + props.decoratorProps.path;
          server[props.decoratorProps.method](url, async (...args) => {
            const result = await Promise.resolve(routerInstance[prop].call(routerInstance, ...args));
            args[1].json(result).end();
          });
        }
      }
    }

    server.listen(3000);
  }
}

start({
  importFiles: [
    FilesLoader.buildPath(__dirname, "factories", "**", "*.js"),
    FilesLoader.buildPath(__dirname, "routers", "**", "*.js"),
  ],
  modules: [App],
});

在这里,我们使用 `@OnInit` 装饰器在应用程序实例化后初始化 Express 服务器。我们动态扫描 `@Router` 和 `@Route` 装饰器并在服务器上配置路由。

您现在可以使用以下命令运行该应用程序:

tsc && node ./dist/app.js

结论

使用“@lemondi”提供的装饰器和 DI 系统,我们简化了 Node.js 和 Express 应用程序。这种方法抽象了路由和依赖管理通常所需的大量样板代码,从而产生了更简洁、更易于维护的代码。

如果你厌倦了手动配置路由和服务,这种模式绝对值得探索。通过使用装饰器,我们可以让代码更具声明性、可读性和模块化。