掘金 后端 ( ) • 2024-04-17 10:26

使用 TypeScript 从零搭建自己的 Web 框架:路由映射

在构建自己的 Web 框架时,使用装饰器来定义路由是一种非常优雅且强大的方法。通过装饰器,我们可以在控制器类上指定基础路径,并为每个方法定义具体的路由。接着,我们利用 TypeScript 的反射能力,解析这些装饰器提供的路由信息,并将其映射到 Web 服务器上。

在本篇文章中,我们将展示如何使用 @Controller@Get@Post 装饰器来定义路由,并通过反射机制将这些路由映射到 hyper-express Web 服务器上。

一、定义装饰器

首先,我们需要定义 @Controller@Get@Post 装饰器。@Controller 用于标记控制器类,并为其提供一个基础路径。@Get@Post 分别用于标记处理 GET 和 POST 请求的方法。

function Controller(basePath: string) {
  return function (target: Function) {
    Reflect.defineMetadata('basePath', basePath, target);
  };
}

function Get(path: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const basePath = Reflect.getMetadata('basePath', target.constructor);
    const fullPath = `${basePath}${path}`;
    Reflect.defineMetadata('route:get', fullPath, target, propertyKey);
  };
}

function Post(path: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const basePath = Reflect.getMetadata('basePath', target.constructor);
    const fullPath = `${basePath}${path}`;
    Reflect.defineMetadata('route:post', fullPath, target, propertyKey);
  };
}

二、创建控制器类

接下来,我们创建一个控制器类,并使用上面定义的装饰器来定义路由。

@Controller('/api')
class MyController {
  @Get('/hello')
  public async getHello(req: any, res: any) {
    res.end('Hello from /api/hello');
  }

  @Post('/submit')
  public async postSubmit(req: any, res: any) {
    res.end('Data submitted to /api/submit');
  }
}

在上面的代码中,MyController 类被 @Controller 装饰器标记,并指定了基础路径 /apigetHello 方法被 @Get 装饰器标记,并指定了路由路径 /hello,因此它的完整路径是 /api/hello。同样地,postSubmit 方法被 @Post 装饰器标记,并指定了路由路径 /submit,完整路径为 /api/submit

三、解析并映射路由

现在,我们需要编写代码来解析控制器类上的路由信息,并将其映射到 hyper-express Web 服务器上。

import { createServer } from 'hyper-express';

const app = createServer();

function mapRoutes(controller: any) {
  const basePath = Reflect.getMetadata('basePath', controller);

  const methods = Object.getOwnPropertyNames(controller.prototype).filter(methodName => methodName !== 'constructor');

  methods.forEach(methodName => {
    const getPath = Reflect.getMetadata('route:get', controller.prototype, methodName);
    const postPath = Reflect.getMetadata('route:post', controller.prototype, methodName);

    if (getPath) {
      app.get(getPath, controller.prototype[methodName].bind(controller));
    }

    if (postPath) {
      app.post(postPath, controller.prototype[methodName].bind(controller));
    }
  });
}

// 假设 MyController 已经定义并可用
mapRoutes(MyController);

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

在上面的代码中,mapRoutes 函数遍历控制器类原型上的所有方法,并检查是否存在 @Get@Post 装饰器定义的路由路径。如果存在,则使用 app.getapp.post 方法将方法映射到相应的路由上。注意,我们使用 bind(controller) 来确保方法中的 this 指向控制器实例。

四、总结

结合前文的文件扫描和自动导入,我们可以轻松的将项目中约定或配置的路径下的控制器文件映射到 Web 服务器的路由系统,并且可以使用 IoC 容器实现依赖注入。