使用 TypeScript 从零搭建自己的 Web 框架:框架雏形
- 2024-04-15 江西
本文字数:6715 字
阅读完需:约 22 分钟
经过前面几篇文章对 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.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 服务器等
// 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 目录主要存放各类控制器文件
// 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 目录主要存放各类服务文件
// HomeService.ts
import { Injectable } from '@/core';
@Injectable()
export class HomeService {
async sayHello(): Promise<string> {
return 'Hello!';
}
}
接口
core/interface 目录用于存放各类接口定义
// 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 目录用于存放各类类型定义
// 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 目录用于存放各类常量定义
// 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 目录用于存放各类工具函数
// core/util/ForwardRef.ts
import { ForwardReference } from '../type';
export const forwardRef = (fn: () => any): ForwardReference => ({
forwardRef: fn,
});
装饰器
core/decorator 目录用于存放各类装饰器定义
// 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 目录用于存放各类组件定义
// 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 服务器,让框架焕发出生机与活力。
目前,框架的雏形已经初具规模,但我们的脚步并未停歇。后续,我们将继续完善代码,增添更多的框架组件,如:管道、守卫、拦截器、中间件、过滤器、事件总线、钩子等等,让框架的功能日益丰富,性能日益卓越。我们坚信,在不断地迭代与优化中,这个框架将逐渐展现出其强大的生命力与广阔的应用前景。
RoyLin
不积跬步 无以至千里 2019-09-01 加入
还未添加个人简介
评论