使用 TypeScript 从零搭建自己的 Web 框架:框架雏形

使用 TypeScript 从零搭建自己的 Web 框架:框架雏形

经过前面几篇文章对 IoC 容器、依赖注入、路由映射等技术的深入剖析,我们积累了丰富的知识和实践经验。如今,我们将这些精心打磨的组件代码进行完善与整合,将它们巧妙地融为一体,构建出一个初步的 Web 框架雏形。这个雏形凝聚了我们的智慧与汗水,它是我们迈向更高峰的起点,也是我们追求卓越的见证。

框架结构概述

我们的框架将遵循经典的 MVC(Model-View-Controller)架构,目前框架的目录结构如下:

bash 复制代码
web-framework/
├── src/                         # 源码目录
│   ├── index.ts                 # 入口文件
│   ├── core/                    # 核心目录
│   │   ├── Application.ts       # 框架核心类
│   │   ├── component/           # 框架组件目录
│   │   │   ├── Container.ts
│   │   │   └── WebServer.ts
│   │   │   └── ...
│   │   ├── constant             # 常量目录
│   │   │   └── metadata.ts
│   │   ├── decorator            # 装饰器目录
│   │   │   ├── Injectable.ts
│   │   │   ├── Inject.ts
│   │   │   ├── Controller.ts
│   │   │   ├── Get.ts
│   │   │   ├── Post.ts
│   │   │   └── ...
│   │   ├── interface            # 接口目录
│   │   │   ├── IContainer.ts
│   │   │   ├── Inject.ts
│   │   │   ├── IWebServer.ts
│   │   │   └── ...
│   │   ├── type                 # 类型定义目录
│   │   │   ├── Application.ts
│   │   │   ├── Container.ts
│   │   │   ├── Http.ts
│   │   │   ├── WebServer.ts
│   │   │   └── ...
│   │   ├── util                 # 工具函数目录
│   │   │   └── ForwardRef.ts
│   │   │   └── ...
│   ├── controller/              # 控制器目录
│   │   ├── HomeController.ts
│   │   └── ...
│   ├── service/                 # 服务层目录
│   │   ├── HomeService.ts
│   │   └── ...
├── dist/                        # 编译后的输出目录
├── node_modules/                # Node.js 依赖包目录
├── package.json                 # 项目配置文件(依赖、脚本等)
├── tsconfig.json                # TypeScript 配置文件
└── ...                          # 其他文件或目录(如 README.md、.gitignore 等)

下面我们分别展示一下各个主要部分的源码

入口文件

作为程序的入口,主要负责启动框架。

typescript 复制代码
// index.ts
import 'reflect-metadata';

import { Application } from '@/core';

async function main() {
  const app = await new Application().initialize();
  await app.listen(3000);
}

main();

核心类

Application.ts 文件是框架的核心类文件,负责扫描文件、导入模块、初始化 IoC 容器、初始化 Web 服务器等

typescript 复制代码
// core/Application.ts
import { glob } from 'fast-glob';
import path from 'path';
import { Container, WebServer } from './component';
import { CONTROLLER_METADATA, INJECTABLE_METADATA, ROUTE_ACTION } from './constant';
import { IContainer, IWebServer } from './interface';
import { ApplicationConfig, Constructor, ListenCallback } from './type';

export class Application {
  private readonly container: IContainer;
  private readonly webServer: IWebServer;
  constructor({ webServer, container }: ApplicationConfig = {}) {
    this.container = container ?? new Container();
    this.webServer = webServer ?? new WebServer();
  }

  async initialize(): Promise<this> {
    const files = await glob(['service/**/*.js', 'controller/**/*.js'], {
      cwd: path.join(process.cwd(), 'dist'),
      absolute: true,
      baseNameMatch: true,
      objectMode: true,
    });

    for (let file of files) {
      const module = await import(file.path);
      const name = file.name.split(path.extname(file.name))[0];
      const resolver = module[name];
      const options = Reflect.getMetadata(INJECTABLE_METADATA, resolver);
      if (options) {
        this.container.register(resolver, { resolver, options });
        const isController = Reflect.getMetadata(CONTROLLER_METADATA, resolver);
        if (isController) {
          this.mapRoutes(resolver);
        }
      }
    }

    return this;
  }

  async listen(
    ...args:
      | [port: number, callback?: ListenCallback]
      | [port: number, host?: string, callback?: ListenCallback]
      | [port: string, callback?: ListenCallback]
  ): Promise<any> {
    return this.webServer.listen(...args);
  }

  private mapRoutes(controller: Constructor<any>) {
    const methods = Object.getOwnPropertyNames(controller.prototype).filter(methodName => methodName !== 'constructor');
    methods.forEach(methodName => {
      const route = Reflect.getMetadata(ROUTE_ACTION, controller.prototype, methodName);
      if (route) {
        const service = this.container.resolve(controller);
        this.webServer.route({
          ...route,
          handler: controller.prototype[methodName].bind(service),
        });
        console.log(`Mapped {${route.path}, ${route.method.toUpperCase()}} route`);
      }
    });
  }
}

控制器

controller 目录主要存放各类控制器文件

typescript 复制代码
// HomeController.ts
import { Controller, Get, Post } from '@/core';
import { HomeService } from '@/service/HomeService';
import { Request, Response } from 'hyper-express';

@Controller()
export class HomeController {
  constructor(private readonly homeService: HomeService) {}

  @Get('hello')
  async index(_: Request, res: Response) {
    res.send(await this.homeService.sayHello());
  }

  @Post('submit')
  async submit() {}
}

服务

service 目录主要存放各类服务文件

typescript 复制代码
// HomeService.ts
import { Injectable } from '@/core';

@Injectable()
export class HomeService {
  async sayHello(): Promise<string> {
    return 'Hello!';
  }
}

接口

core/interface 目录用于存放各类接口定义

typescript 复制代码
// core/interface/IContainer.ts
import type { Constructor, Resolveable, Token } from '../type/Container';

export interface IContainer {
  register<T extends Constructor, E = any>(token: Token, resolveable: Resolveable<T, E>): void;
  resolve<T extends Constructor>(token: Token): T;
}

// core/interface/IWebServer.ts
import { ListenCallback, Route } from '../type';

export interface IWebServer {
  route: (route: Route) => this;
  listen: (
    ...args:
      | [port: number, callback?: ListenCallback]
      | [port: number, host?: string, callback?: ListenCallback]
      | [port: string, callback?: ListenCallback]
  ) => Promise<any>;
}

类型定义

core/type 目录用于存放各类类型定义

typescript 复制代码
// core/type/Application.ts
import { IContainer, IWebServer } from '../interface';

export type ApplicationConfig = {
  container?: IContainer;
  webServer?: IWebServer;
};

export type ListenCallback = (socket?: any) => void;

// core/type/Container.ts
export type Constructor<T = any> = {
  new (...args: any[]): T;
};

export type ForwardReference<T = any> = {
  forwardRef: () => T;
};

export type Token = string | symbol | Constructor | ForwardReference;

export type Resolveable<T = any, E = any> = {
  resolver: Constructor<T>;
  options: E;
};

export type Service<T = any> = {
  resolver: Constructor<T>;
  proxy: T;
  instance?: T;
};

export type InjectResolver = {
  type: Token;
  index: number;
};

// ...

常量

core/constant 目录用于存放各类常量定义

typescript 复制代码
// core/constant/metadata.ts
export const INJECTABLE_METADATA = Symbol('INJECTABLE_METADATA');
export const INJECT_METADATA = Symbol('INJECT_METADATA');
export const CONTROLLER_METADATA = Symbol('CONTROLLER_METADATA');
export const ROUTE_BASE = Symbol('ROUTE_BASE');
export const ROUTE_ACTION = Symbol('ROUTE_ACTION');
export const REQ_METADATA = Symbol('REQ_METADATA');
export const RES_METADATE = Symbol('RES_METADATA');

工具 core/util 目录用于存放各类工具函数

typescript 复制代码
// core/util/ForwardRef.ts
import { ForwardReference } from '../type';

export const forwardRef = (fn: () => any): ForwardReference => ({
  forwardRef: fn,
});

装饰器

core/decorator 目录用于存放各类装饰器定义

typescript 复制代码
// core/decorator/Injectable.ts
import { INJECTABLE_METADATA } from '../constant';

export type InjectableOptions = {};

export const Injectable = (options: InjectableOptions = {}): ClassDecorator => {
  return (target: Function) => {
    Reflect.defineMetadata(INJECTABLE_METADATA, options, target);
  };
};

// core/decorator/Controller.ts
import { CONTROLLER_METADATA, INJECTABLE_METADATA, ROUTE_BASE } from '../constant';

export function Controller(route?: string): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata(INJECTABLE_METADATA, {}, target);
    Reflect.defineMetadata(CONTROLLER_METADATA, true, target);
    Reflect.defineMetadata(ROUTE_BASE, route, target);
  };
}

// core/decorator/Get.ts
import { ROUTE_ACTION, ROUTE_BASE } from '../constant';

export function Get(path?: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const basePath = Reflect.getMetadata(ROUTE_BASE, target.constructor);
    const fullPath = [basePath, path].join('/') ?? '/';
    Reflect.defineMetadata(
      ROUTE_ACTION,
      {
        path: fullPath,
        method: 'get',
      },
      target,
      propertyKey
    );
  };
}

// core/decorator/Post.ts
import { ROUTE_ACTION, ROUTE_BASE } from '../constant';

export function Post(path?: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const basePath = Reflect.getMetadata(ROUTE_BASE, target.constructor);
    const fullPath = [basePath, path].join('/') ?? '/';
    Reflect.defineMetadata(
      ROUTE_ACTION,
      {
        path: fullPath,
        method: 'post',
      },
      target,
      propertyKey
    );
  };
}

// ...

组件

core/component 目录用于存放各类组件定义

typescript 复制代码
// core/component/Container.ts
import { isFunction, isSymbol } from 'radash';
import { INJECT_METADATA } from '../constant';
import { IContainer } from '../interface';
import { Constructor, ForwardReference, InjectResolver, Resolveable, Service, Token } from '../type';

export class Container implements IContainer {
  private services: Map<string, Service> = new Map();

  register<T extends Constructor, E = any>(token: Token, resolveable: Resolveable<T, E>): void {
    const key = this.getKey(token);
    if (this.services.has(key)) {
      return;
    }
    this.services.set(key, {
      resolver: resolveable.resolver,
      proxy: new Proxy(new resolveable.resolver(), {
        get: (target, propKey, receiver) => {
          if (
            typeof propKey === 'symbol' ||
            propKey === 'constructor' ||
            propKey === 'prototype' ||
            propKey === 'toJSON' ||
            propKey === 'toString'
          ) {
            return Reflect.get(target, propKey, receiver);
          }
          const service = this.services.get(key);
          if (!service.instance) {
            this.resolveDependencies(token);
          }
          return Reflect.get(service.instance, propKey, receiver);
        },
      }),
    });
  }

  resolve<T extends Constructor>(token: Token): T {
    const key = this.getKey(token);
    if (!this.services.has(key)) {
      throw new Error(`Service ${key} not registered.`);
    }
    return this.services.get(key).proxy;
  }

  private resolveDependencies(token: Token) {
    const key = this.getKey(token);
    const service = this.services.get(key);
    const dependencies = Reflect.getMetadata('design:paramtypes', service.resolver) || [];
    const resolvers: InjectResolver[] = Reflect.getMetadata(INJECT_METADATA, service.resolver) ?? [];
    const params = dependencies.map((dep: string, index: number) => {
      const resolver = resolvers.find(resolver => resolver.index === index);
      return this.resolve(resolver ? resolver.type ?? dep : dep);
    });
    service.instance = Reflect.construct(service.resolver, params);
  }

  private getKey(token: Token): string {
    if (isSymbol(token)) {
      return token.description;
    }
    if (isFunction(token)) {
      return token.name;
    }
    if ((token as ForwardReference)?.forwardRef) {
      return (token as ForwardReference)?.forwardRef().name;
    }
    return token as string;
  }
}

// core/component/WebServer.ts
import { Server } from 'hyper-express';
import { isNumber } from 'radash';
import * as uWebsockets from 'uWebSockets.js';
import { IWebServer } from '../interface';
import { ListenCallback, Route } from '../type';

export class WebServer implements IWebServer {
  private readonly instance: Server;
  constructor() {
    this.instance = new Server();
  }
  route(route: Route) {
    this.instance[route.method](route.path, route.handler);
    return this;
  }

  async listen(
    ...args:
      | [port: number, callback?: ListenCallback]
      | [port: number, host?: string, callback?: ListenCallback]
      | [port: string, callback?: ListenCallback]
  ): Promise<uWebsockets.us_listen_socket> {
    const [port, host, callback] = args;
    if (isNumber(port)) {
      if (!!host) {
        return await this.instance.listen(port, host as string, callback);
      }
      return await this.instance.listen(port, callback);
    }
    return await this.instance.listen(port, callback);
  }
}

总结

首先,我们将框架的各个功能模块精心雕琢为独立的组件,并将它们巧妙地集成于核心类之中。随后,核心类肩负起初始化与管理这些框架组件的重任,确保它们协同工作、井然有序。在编程的过程中,我们始终秉持面向接口的理念,力求降低核心类与各个组件之间的耦合度,使得整个框架更为灵活、可扩展。最终,在入口文件的指引下,我们创建了核心类,并启动了 Web 服务器,让框架焕发出生机与活力。

目前,框架的雏形已经初具规模,但我们的脚步并未停歇。后续,我们将继续完善代码,增添更多的框架组件,如:管道、守卫、拦截器、中间件、过滤器、事件总线、钩子等等,让框架的功能日益丰富,性能日益卓越。我们坚信,在不断地迭代与优化中,这个框架将逐渐展现出其强大的生命力与广阔的应用前景。

相关推荐
郝晨妤11 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
红尘炼心1 天前
一个困扰我许久的TypeScript定义问题
前端·react.js·typescript
baiduguoyun1 天前
TypeScript 中的三斜杠指令语法
typescript
潘敬2 天前
flutter 语法糖库 flutter_magic 发布 1.0.1
开发语言·前端·javascript·flutter·typescript
高木的小天才2 天前
HarmonyOS一次开发多端部署三巨头之功能级一多开发和工程级一多开发
前端·华为·typescript·harmonyos
周三有雨3 天前
vue3 + vite 实现版本更新检查(检测到版本更新时提醒用户刷新页面)
前端·vue.js·typescript
清灵xmf4 天前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
Amd7945 天前
Nuxt.js 应用中的 prepare:types 事件钩子详解
typescript·自定义·配置·nuxt·构建·钩子·类型
王解6 天前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●6 天前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70