用 TypeScript + Express 打造高效的装饰器路由框架

299 阅读3分钟

背景

在日常开发中,我经常会面临路由注册、服务依赖注入以及代码结构复杂等问题。为了让我能够专注于业务逻辑,并减少重复性工作,我利用 TypeScriptExpress 的结合,设计了一套基于装饰器的轻量级路由框架。本文将介绍这套框架的设计思路和实现细节。

核心特性

  1. 装饰器简化路由注册:通过 @Controller@Get 等装饰器,无需手动注册路由。
  2. 动态加载控制器和服务:框架会自动扫描项目目录,实例化服务和控制器。
  3. 依赖注入支持:通过 @InjectService 实现服务的依赖注入。
  4. 模块化设计:分离路由、服务和装饰器逻辑,易于扩展和维护。
  5. 高度可扩展:支持任意 HTTP 方法和中间件的扩展。

核心代码实现

路由装饰器实现

以下代码展示了如何通过装饰器实现 GETPOST 方法的自动注册。

/**
 * 请求方法装饰器生成工厂
 */
const createMethodDecorator = (type: 'get' | 'post' | 'patch' | 'delete') => {
  return (param?: IMethod | string) => {
    return function <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) {
      MethodAttach(target, propertyKey, descriptor, type, param);
    };
  };
};

export const Get = createMethodDecorator('get');
export const Post = createMethodDecorator('post');
export const Patch = createMethodDecorator('patch');
export const Delete = createMethodDecorator('delete');

通过 createMethodDecorator 工厂函数,避免了重复编写装饰器逻辑。

控制器与服务装饰器

控制器和服务的装饰器帮助我们完成类的注册和依赖注入。

/**
 * 控制器装饰器:自动注册路由
 */
export const Controller = (path?: string) => {
  return function (constructor: Function) {
    if (routerList[constructor.name]) {
      Object.keys(routerList[constructor.name]).forEach((element) => {
        let routerElement = routerList[constructor.name][element];
        Router[routerElement.method](
          `${path ? path : ''}${routerElement.propertyKey}`,
          routerElement.descriptor
        );
      });
    }
  };
};

/**
 * 服务装饰器:注册服务
 */
export const Service = (serviceName: string) => {
  return function (constructor: Function) {
    let name = serviceName || constructor.name;
    if (serviceList[name]) {
      throw new Error(`Service ${name} exists.`);
    }
    serviceList[name] = constructor;
  };
};

/**
 * 注入服务
 */
export const InjectService = (serviceNames: string[]) => {
  return function (constructor: Function) {
    serviceNames.forEach((name) => {
      if (!serviceList[name]) {
        throw new Error(`Service ${name} not found.`);
      }
      constructor.prototype[name] = new serviceList[name]();
    });
  };
};

动态加载与实例化

框架支持动态加载服务和控制器,大大减少了手动注册的工作量。

/**
 * 实例化控制器和服务
 */
export async function instantiateInstance(filePath: string, type: string): Promise<void> {
  const files = await fs.readdir(filePath);
  for (const filename of files) {
    const filedir = path.join(filePath, filename);
    const stats = await fs.stat(filedir);
    if (stats.isFile() && filedir.endsWith(`${type}.ts`)) {
      const nowClass = await import(filedir);
      if (nowClass?.default) {
        new nowClass.default();
      }
    } else if (stats.isDirectory()) {
      await instantiateInstance(filedir, type);
    }
  }
}

通过递归扫描文件夹,框架会自动实例化 service.tscontroller.ts 文件中的类。

使用示例

定义服务

@Service('UserService')
class UserService {
  getUser() {
    return { id: 1, name: 'John Doe' };
  }
}

定义控制器

@Controller('/user')
@InjectService(['UserService'])
class UserController {
  private UserService!: UserService;

  @Get('/info')
  getUserInfo(req: Request, res: Response) {
    const user = this.UserService.getUser();
    res.json(user);
  }
}

注册路由

const app = express();
registerRouters({ indexPath: './public/index.html' }).then((router) => {
  app.use(router);
  app.listen(3000, () => console.log('Server started on http://localhost:3000'));
});

完整代码

import express, { NextFunction, Request, Response } from 'express';
import { promises as fs } from 'fs';
import path from 'path';

const Router = express.Router();

type TRequestType = 'get' | 'post' | 'patch' | 'delete';

interface IMethod {
  path: string;
  auth?: boolean;
}

const routerList: Record<string, Record<string, any>> = {};
const serviceList: Record<string, any> = {};

/**
 * 注册服务
 */
export const Service = (serviceName: string): ClassDecorator => {
  return (constructor) => {
    const name = serviceName || constructor.name;
    if (serviceList[name]) {
      console.error(`Service ${name} already exists.`);
      return;
    }
    serviceList[name] = constructor;
  };
};

/**
 * 注入服务
 */
export const InjectService = (serviceNames: string[]): ClassDecorator => {
  return (constructor) => {
    serviceNames.forEach((serviceName) => {
      if (!serviceList[serviceName]) {
        console.error(`Service ${serviceName} not found.`);
        return;
      }
      constructor.prototype[serviceName] = new serviceList[serviceName]();
    });
  };
};

/**
 * Controller 注册器
 */
export const Controller = (basePath = ''): ClassDecorator => {
  return (constructor) => {
    const routes = routerList[constructor.name] || {};
    Object.values(routes).forEach(({ method, path, handler }) => {
      Router[method as TRequestType](basePath + path, handler);
    });
  };
};

/**
 * 收集方法装饰器的逻辑
 */
const MethodAttach = (
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<any>,
  method: TRequestType,
  param?: IMethod | string
) => {
  const className = target.constructor.name;
  routerList[className] = routerList[className] || {};

  const routePath = typeof param === 'string' ? param : param?.path || `/${String(propertyKey)}`;
  const handler = (req: Request, res: Response, next: NextFunction) => {
    descriptor.value.call(target, req, res, next);
  };

  routerList[className][String(propertyKey)] = { method, path: routePath, handler };
};

/** 请求方法装饰器工厂 */
const createMethodDecorator = (method: TRequestType) => {
  return (param?: IMethod | string): MethodDecorator => {
    return (target, propertyKey, descriptor) => {
      MethodAttach(target, propertyKey, descriptor, method, param);
    };
  };
};

export const Get = createMethodDecorator('get');
export const Post = createMethodDecorator('post');
export const Patch = createMethodDecorator('patch');
export const Delete = createMethodDecorator('delete');

/**
 * 实例化服务或控制器
 */
export const instantiateInstance = async (filePath: string, type: string): Promise<void> => {
  try {
    const files = await fs.readdir(filePath);

    for (const filename of files) {
      const filedir = path.join(filePath, filename);
      const stats = await fs.stat(filedir);

      if (stats.isFile() && filedir.endsWith(`${type}.ts`)) {
        const { default: Class } = await import(filedir);
        if (Class) new Class();
      } else if (stats.isDirectory()) {
        await instantiateInstance(filedir, type);
      }
    }
  } catch (error) {
    console.error(`Error processing path "${filePath}":`, error);
  }
};

/**
 * 注册路由
 */
export const registerRouters = async ({ indexPath }: { indexPath?: string } = {}): Promise<express.Router> => {
  const appPath = path.resolve('./app');

  // 实例化服务和控制器
  await instantiateInstance(appPath, 'service');
  await instantiateInstance(appPath, 'controller');

  // 通配符返回 index.html,实现 history 模式
  Router.get('*', (req, res, next) => {
    if (indexPath) {
      res.sendFile(indexPath);
    } else {
      next();
    }
  });

  return Router;
};

总结

这套框架的设计旨在提升我日常项目的开发效率,通过装饰器和动态加载的方式解放双手,让我能专注于业务逻辑。得益于 TypeScript 的类型支持和装饰器能力,代码更具语义化和可维护性。

当然,这只是一个 Demo,目前我也只在我接的一些小外包单中用过,并没有大项目的使用的例子,欢迎大家提出不同的想法。