写点什么

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

作者:RoyLin
  • 2024-04-15
    江西
  • 本文字数:6715 字

    阅读完需:约 22 分钟

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

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


框架结构概述


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


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 等)
复制代码


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


入口文件


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


// index.tsimport 'reflect-metadata';
import { Application } from '@/core';
async function main() { const app = await new Application().initialize(); await app.listen(3000);}
main();
复制代码


核心类


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


// core/Application.tsimport { 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 目录主要存放各类控制器文件


// HomeController.tsimport { 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 目录主要存放各类服务文件


// HomeService.tsimport { Injectable } from '@/core';
@Injectable()export class HomeService { async sayHello(): Promise<string> { return 'Hello!'; }}
复制代码


接口


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


// core/interface/IContainer.tsimport 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.tsimport { 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 目录用于存放各类类型定义


// core/type/Application.tsimport { IContainer, IWebServer } from '../interface';
export type ApplicationConfig = { container?: IContainer; webServer?: IWebServer;};
export type ListenCallback = (socket?: any) => void;
// core/type/Container.tsexport 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 目录用于存放各类常量定义


// core/constant/metadata.tsexport 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 目录用于存放各类工具函数


// core/util/ForwardRef.tsimport { ForwardReference } from '../type';
export const forwardRef = (fn: () => any): ForwardReference => ({ forwardRef: fn,});
复制代码


装饰器


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


// core/decorator/Injectable.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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 目录用于存放各类组件定义


// core/component/Container.tsimport { 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.tsimport { 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 服务器,让框架焕发出生机与活力。


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


用户头像

RoyLin

关注

不积跬步 无以至千里 2019-09-01 加入

还未添加个人简介

评论

发布
暂无评论
使用 TypeScript 从零搭建自己的 Web 框架:框架雏形_typescript_RoyLin_InfoQ写作社区