使用装饰器模式设计一个 Web 框架
在讲解了装饰器模式后,下面我们基于这个模式来设计一个简单的 Web 框架。框架基于 Exoress、加入了装饰器模式的设计。
之所以选择 Web 框架是因为问 AI 问题的时候它举了这个的例子,因此就试着写了一个简单的。
1. 总体设计
基于装饰器模式的核心思想是使用装饰器将核心逻辑托管给元数据,然后在最终的应用层中系统地处理这些元数据、从而实现 Web 逻辑。
下面是 Application Core 的设计、装饰器模块的设计以及 Web Frame 总体的工作流:
下面我们开始搭建这个 Web Frame。首先是装饰器模块。
2. 装饰器模块
常量定义
在开始实现具体装饰器逻辑前,我们先把存储元数据使用的键和类型定义好:
export const METADATA_KEYS = {
CONTROLLER_PREFIX: "controller:prefix",
ROUTE_METHOD: "route:method",
ROUTE_PATH: "route:path",
PARAM_TYPES: "design:paramtypes",
PARAM_BODY: "param:body",
PARAM_QUERY: "param:query",
PARAM_PARAM: "param:param",
MIDDLEWARE: "middleware",
PIPES: "pipes",
} as const;
export type MetadataKeyType =
(typeof METADATA_KEYS)[keyof typeof METADATA_KEYS];
export type HttpMethod = "get" | "post" | "put" | "delete";
export interface RouteDefinition {
method: HttpMethod;
path: string;
handlerName: string;
}
export interface ParameterDefinition {
index: number;
type: "body" | "query" | "param";
name?: string;
}
管道相关类型我们放到管道装饰器部分详细讲解。
这里使用
as const方法来实现枚举,原因在”TypeScript as const”笔记用有详细阐释。
然后再提前写好一些操作元数据的一些帮助方法:
import "reflect-metadata";
import { MetadataKeyType, METADATA_KEYS } from "../decorators/constants";
/**
* Get metadata with type-safe key access
*/
export function getMetadata<T>(
key: MetadataKeyType,
target: any,
propertyKey?: string | symbol,
): T | undefined {
return Reflect.getMetadata(key, target, propertyKey as string | symbol);
}
/**
* Define metadata with type-safe key access
*/
export function defineMetadata<T>(
key: MetadataKeyType,
value: T,
target: any,
propertyKey?: string | symbol,
): void {
Reflect.defineMetadata(key, value, target, propertyKey as string | symbol);
}
/**
* Check if metadata exists with type-safe key access
*/
export function hasMetadata(
key: MetadataKeyType,
target: any,
propertyKey?: string | symbol,
): boolean {
return Reflect.hasMetadata(key, target, propertyKey as string | symbol);
}
/**
* Get metadata with default value - accepts both MetadataKeyType and string values
*/
export function getMetadataOrDefault<T>(
key: MetadataKeyType | string,
target: any,
defaultValue: T,
propertyKey?: string | symbol,
): T {
return (
Reflect.getMetadata(key, target, propertyKey as string | symbol) ??
defaultValue
);
}
/**
* Define metadata - accepts both MetadataKeyType and string values
*/
export function defineMetadataValue<T>(
key: MetadataKeyType | string,
value: T,
target: any,
propertyKey?: string | symbol,
): void {
Reflect.defineMetadata(key, value, target, propertyKey as string | symbol);
}
Controller
Controller 负责处理路由前缀,我们把路由前缀信息存储到对应 Metadata 的 key 中。这是一个类装饰器:
export function Controller(prefix: string = ""): ClassDecorator {
return function (target: any) {
defineMetadataValue(METADATA_KEYS.CONTROLLER_PREFIX, prefix, target);
};
}
HTTP 方法装饰器
同样地,我们将对应路由的信息存储到 Metadata 中。这里我们可以写一个装饰器工厂,然后生产出不同 HTTP 方法装饰器:
import "reflect-metadata";
import { HttpMethod, RouteDefinition, METADATA_KEYS } from "./constants";
import { getMetadataOrDefault, defineMetadataValue } from "../utils/metadata";
function createHttpMethodDecorator(method: HttpMethod) {
return function (path: string = ""): MethodDecorator {
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) {
const existingRoutes = getMetadataOrDefault<RouteDefinition[]>(
METADATA_KEYS.ROUTE_METHOD,
target.constructor,
[],
);
existingRoutes.push({
method,
path,
handlerName: propertyKey.toString(),
});
defineMetadataValue(
METADATA_KEYS.ROUTE_METHOD,
existingRoutes,
target.constructor,
);
};
};
}
export const Get = createHttpMethodDecorator("get");
这里有需要注意的地方:我们要将这个 Metadata 记录到 target.constructor 上。我们举一个具体的例子说明这件事。假设我们的框架看到了下面的东西:
@Controller("/users")
class UserController {
@Get("/")
getAllUsers() {
/* ... */
}
@Get("/:id")
getUserById() {
/* ... */
}
@Post("/")
createUser() {
/* ... */
}
}
我们希望 UserController 最后得到一个这样的路由清单:
[
{ "method": "get", "path": "/", "handlerName": "getAllUsers" },
{ "method": "get", "path": "/:id", "handlerName": "getUserById" },
{ "method": "post", "path": "/", "handlerName": "createUser" }
]
如果我们把 @Get 的信息记录在 target 上,也就是 getUserById 等方法上,我们的框架在处理 UserController 时必须一个一个进入这些方法才能获取这些信息。这非常繁琐。但是如果我们把这些信息记录在 target.constructor,也就是 UserController 的构造函数上,框架就只需要看这一个地方,就可以获得所有信息了。
方法装饰器的元数据附加目标一般都是
target.constructor。
参数装饰器 & 中间件装饰器
参数装饰器和中间件装饰器的实现方法也一样,对于中间件我们同时考虑控制器中间件和方法中间件:
export function Param(name: string): ParameterDecorator {
return function (
target: any,
propertyKey: string | symbol | undefined,
parameterIndex: number,
) {
const existingParameters = getMetadataOrDefault<ParameterDefinition[]>(
METADATA_KEYS.PARAM_PARAM,
target,
[],
propertyKey as string,
);
existingParameters.push({
index: parameterIndex,
type: "param" as const,
name,
});
defineMetadataValue(
METADATA_KEYS.PARAM_PARAM,
existingParameters,
target,
propertyKey as string,
);
};
}
export function Use(
middleware: RequestHandler | RequestHandler[],
): ClassDecorator & MethodDecorator {
return function (
target: any,
propertyKey?: string | symbol,
descriptor?: PropertyDescriptor,
) {
// If propertyKey is undefined, this is a class decorator
if (propertyKey === undefined) {
// Class-level middleware
const existingMiddleware = getMetadataOrDefault<RequestHandler[]>(
METADATA_KEYS.MIDDLEWARE,
target,
[],
);
const middlewareArray = Array.isArray(middleware)
? middleware
: [middleware];
const combinedMiddleware = [...existingMiddleware, ...middlewareArray];
defineMetadataValue(METADATA_KEYS.MIDDLEWARE, combinedMiddleware, target);
} else {
// Method-level middleware
const existingMiddleware = getMetadataOrDefault<RequestHandler[]>(
METADATA_KEYS.MIDDLEWARE,
target.constructor,
[],
propertyKey.toString(),
);
const middlewareArray = Array.isArray(middleware)
? middleware
: [middleware];
const combinedMiddleware = [...existingMiddleware, ...middlewareArray];
defineMetadataValue(
METADATA_KEYS.MIDDLEWARE,
combinedMiddleware,
target.constructor,
propertyKey.toString(),
);
}
};
}
管道装饰器
数据类型定义
一个管道装饰器应该接收两个参数:
value:传入的需要管道处理的值。metadata:可选元数据,这里是 metatype,也就是类本身。有了这个东西,我们可以做一些类上的操作。
然后返回处理后的 value,也就是 any:
export interface PipeTransform {
transform(value: any, metadata: { metatype?: any }): any;
}
装饰器实现
管道装饰器是方法装饰器,我们把管道元数据记录到 target.constructor 上:
export function UsePipes(
pipe: PipeTransform | PipeTransform[],
): MethodDecorator {
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) {
const existingPipes = getMetadataOrDefault<PipeTransform[]>(
METADATA_KEYS.PIPES,
target.constructor,
[],
propertyKey.toString(),
);
const pipeArray = Array.isArray(pipe) ? pipe : [pipe];
const combinedPipes = [...existingPipes, ...pipeArray];
defineMetadataValue(
METADATA_KEYS.PIPES,
combinedPipes,
target.constructor,
propertyKey.toString(),
);
};
}
数据验证管道组件
下面利用定义的接口、实现一个简单的数据验证管道。
在数据验证管道中,我们可以使用 class-validator 和 class-transformer 来操作 metatype 的类,具体流程如下:
- 将我们传入的
value和metatype组合成类的实例。 - 用
class-validator的validate方法进行 validation。这个过程是异步的。 - 返回结果。
import { validate, ValidationError } from "class-validator";
import { plainToInstance } from "class-transformer";
import { PipeTransform } from "../decorators/constants";
export class ValidationPipe implements PipeTransform {
async transform(value: any, metadata: { metatype?: any }): Promise<any> {
if (!metadata.metatype || !this.toValidate(metadata.metatype)) {
return value;
}
const object = plainToInstance(metadata.metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const errorMessages = this.formatErrors(errors);
const validationError = new Error("Validation failed");
validationError.name = "ValidationError";
(validationError as any).details = errorMessages;
(validationError as any).status = 400;
throw validationError;
}
return object;
}
}
3. 依赖注入
实现了基本的装饰器组件后,我们再看看该怎么用装饰器完成依赖注入。
概念与实现原理
首先我们看看什么是依赖注入。假如我们需要给 Controller 传入 Service,一般的做法如下:
// 传统方式:手动创建依赖
class UserController {
private userService: UserService;
constructor() {
// ❌ 硬编码依赖,紧耦合
this.userService = new UserService();
// 如果 UserService 还需要其他依赖,还需要在这里创建
}
getUsers() {
return this.userService.findAll();
}
}
而有了依赖注入组件后,只要把 Controller 装饰成 @Injectable(),我们的框架就可以自动处理这些依赖:
@Injectable()
class UserController {
constructor(private readonly userService: UserService) {}
// ✅ 依赖通过构造函数自动注入,无需手动创建
}
我们的 @Injectable() 装饰器并不使用什么具体的逻辑,只是标记一下这个 Controller 需要执行依赖注入。
然后,在执行依赖注入的时候,我们可以直接读取 TypeScript 记录好的依赖元数据。这个元数据放在 'design:paramtypes' 中。例如对于 UserController,它的依赖元数据就是 UserService。有了这些依赖记录后,我们一个一个实例化这些依赖即可。
具体实现
首先我们实现 Injectable 装饰器,这个装饰器什么都不需要做,直接返回 constructor 即可:
export type ConstructorType<T = {}> = new (...args: any[]) => T;
然后,根据前面所说,我们读取元数据中记录的依赖,并递归实例化。我们使用一个 Container.instance 单例来管理依赖,然后把解析后的实例都存放在 instances 中:
export class Container {
private static readonly instance = new Container();
private readonly instances = new Map<any, any>();
private constructor() {}
public static getInstance(): Container {
return Container.instance;
}
public resolve<T>(target: ConstructorType<T>): T {
// Check if instance already exists (Singleton pattern)
if (this.instances.has(target)) {
return this.instances.get(target);
}
// Get constructor parameter types using reflect-metadata
const paramTypes = Reflect.getMetadata("design:paramtypes", target) || [];
// Resolve dependencies recursively
const resolvedDependencies = paramTypes.map((paramType: any) => {
return this.resolve(paramType);
});
// Create new instance with resolved dependencies
const instance = new target(...resolvedDependencies);
// Store the instance
this.instances.set(target, instance);
return instance;
}
}
4. Application 应用层
实现原理
最后我们要封装我们前面实现的这些装饰器,我们读取装饰器附加在类或方法上的元数据,把它们转化成 express 框架可以理解的真实路由与逻辑。总体流程如下:
- 读取路由定义:消费
@Controller与@http-method - 解析和注入参数:消费
@Param(还有@Body和@Query) - 应用管道:消费
@UsePipes - 执行控制器方法并得到响应。
路由读取
首先我们读取 @Controller 类注释器的 prefix 和 @http-method 的 path,得到路由路径,然后从 DI Container 获取 Controller 实例,把它注册到 express route.method 上:
private registerController(ControllerClass: ConstructorType<any>): void {
const controllerInstance = ContainerInstance.resolve<any>(ControllerClass);
const prefix = getMetadataOrDefault<string>(METADATA_KEYS.CONTROLLER_PREFIX, ControllerClass, '');
const routes = getMetadataOrDefault<RouteDefinition[]>(METADATA_KEYS.ROUTE_METHOD, ControllerClass, []);
const controllerMiddleware = getMetadataOrDefault<RequestHandler[]>(METADATA_KEYS.MIDDLEWARE, ControllerClass, []);
routes.forEach((route: RouteDefinition) => {
const fullPath = prefix + route.path;
const handler = controllerInstance[route.handlerName];
const routeMiddleware = getMetadataOrDefault<RequestHandler[]>(METADATA_KEYS.MIDDLEWARE, ControllerClass, [], route.handlerName);
if (typeof handler === 'function') {
const allMiddleware = [...controllerMiddleware, ...routeMiddleware];
this.app[route.method](fullPath, ...allMiddleware, this.createRouteHandler(handler, controllerInstance));
}
});
}
Controller 到 express handler 的转换可以使用异步节约转换时间,这个过程需要参数解析,具体过程后面讲解:
private createRouteHandler(handler: Function, controllerInstance: any) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const args = await this.resolveParameters(handler, controllerInstance, req);
const result = await handler.apply(controllerInstance, args);
if (result !== undefined && result !== null) {
res.json(result);
} else {
res.status(204).send();
}
} catch (error) {
next(error);
}
};
}
参数解析
然后我们使用 @Param 与管道装饰器,读取参数并使用管道处理,返回最终的参数。参数用一个 Array 存着:
// Process body parameters
for (const param of bodyParams) {
let value = req.body;
// Apply pipes to body parameters
for (const pipe of pipes) {
if (pipe && typeof pipe.transform === "function") {
value = await pipe.transform(value, {
metatype: paramTypes[param.index],
});
}
}
args[param.index] = value;
}
// Process query parameters
for (const param of queryParams) {
let value = param.name ? req.query[param.name] : req.query;
// Apply pipes to query parameters
for (const pipe of pipes) {
if (pipe && typeof pipe.transform === "function") {
value = await pipe.transform(value, {
metatype: paramTypes[param.index],
});
}
}
args[param.index] = value;
}
// Process route parameters
for (const param of routeParams) {
let value = param.name ? req.params[param.name] : undefined;
// Apply pipes to route parameters
for (const pipe of pipes) {
if (pipe && typeof pipe.transform === "function") {
value = await pipe.transform(value, {
metatype: paramTypes[param.index],
});
}
}
args[param.index] = value;
}
return args;
中间件等其他配置
其他的 Application 配置直接使用 express 的即可:
public use(...args: any[]): void {
this.app.use(...args);
}
public listen(port: number, callback?: () => void): void {
this.app.listen(port, callback);
}
public getApp(): Express {
return this.app;
}
5. 使用 Application
现在我们看看如何具体使用我们的 Application 单例!
数据类型定义与管道处理
我们可以给数据类型加上 class-validator 的 validate 装饰器,我们前面的数据验证管道可以把这些装饰器用于数据验证:
import {
IsString,
IsEmail,
IsOptional,
IsInt,
Min,
Max,
Length,
} from "class-validator";
export class CreateUserDto {
@IsString()
@Length(2, 50, { message: "Name must be between 2 and 50 characters" })
name!: string;
@IsEmail({}, { message: "Please provide a valid email address" })
email!: string;
@IsInt({ message: "Age must be an integer" })
@Min(1, { message: "Age must be at least 1" })
@Max(120, { message: "Age must not exceed 120" })
age!: number;
}
export class UpdateUserDto {
@IsOptional()
@IsString()
@Length(2, 50, { message: "Name must be between 2 and 50 characters" })
name?: string;
@IsOptional()
@IsEmail({}, { message: "Please provide a valid email address" })
email?: string;
@IsOptional()
@IsInt({ message: "Age must be an integer" })
@Min(1, { message: "Age must be at least 1" })
@Max(120, { message: "Age must not exceed 120" })
age?: number;
}
export interface User {
id: number;
name: string;
email: string;
age: number;
createdAt: Date;
}
给 Controller 注入 Service 依赖
我们先实现下面这个简单的 UserService:
import { Injectable } from "../../src/decorators/injectable";
import { CreateUserDto, User } from "../dtos/user.dto";
@Injectable()
export class UserService {
private users: User[] = [
{
id: 1,
name: "John Doe",
email: "john@example.com",
age: 30,
createdAt: new Date(),
},
{
id: 2,
name: "Jane Smith",
email: "jane@example.com",
age: 25,
createdAt: new Date(),
},
];
public findAll(page?: number, limit?: number): User[] {
const pageNum = page ? parseInt(page.toString()) : 1;
const limitNum = limit ? parseInt(limit.toString()) : 10;
const startIndex = (pageNum - 1) * limitNum;
return this.users.slice(startIndex, startIndex + limitNum);
}
public findById(id: string): User | undefined {
const userId = parseInt(id);
return this.users.find((user) => user.id === userId);
}
public create(createUserDto: CreateUserDto): User {
if (!createUserDto.name || !createUserDto.email) {
throw new Error("Name and email are required");
}
const newUser: User = {
id: Math.max(...this.users.map((u) => u.id)) + 1,
name: createUserDto.name,
email: createUserDto.email,
age: createUserDto.age || 18,
createdAt: new Date(),
};
this.users.push(newUser);
return newUser;
}
public update(
id: string,
updateDto: Partial<CreateUserDto>,
): User | undefined {
const userId = parseInt(id);
const userIndex = this.users.findIndex((user) => user.id === userId);
if (userIndex === -1) {
throw new Error("User not found");
}
this.users[userIndex] = {
...this.users[userIndex],
...updateDto,
};
return this.users[userIndex];
}
public delete(id: string): { message: string } {
const userId = parseInt(id);
const userIndex = this.users.findIndex((user) => user.id === userId);
if (userIndex === -1) {
throw new Error("User not found");
}
this.users.splice(userIndex, 1);
return { message: "User deleted successfully" };
}
}
这个 Service 是可依赖注入的,直接传给 Controller 即可。然后我们使用前面的装饰器声明路由及其参数等信息:
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Query,
Param,
Use,
UsePipes,
} from "../../src/decorators";
import { Injectable } from "../../src/decorators/injectable";
import { CreateUserDto, User, UpdateUserDto } from "../dtos/user.dto";
import { UserService } from "../services/user.service";
import { ValidationPipe } from "../../src/pipes/validation.pipe";
import { LoggerMiddleware } from "../middleware/logger.middleware";
@Injectable()
@Controller("/api/users")
@Use(LoggerMiddleware)
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
public getUsers(
@Query("page") page?: number,
@Query("limit") limit?: number,
): User[] {
return this.userService.findAll(page, limit);
}
@Get("/:id")
public getUserById(@Param("id") id: string): User | undefined {
return this.userService.findById(id);
}
@Post()
@UsePipes(new ValidationPipe())
public createUser(@Body() createUserDto: CreateUserDto): User {
return this.userService.create(createUserDto);
}
@Put("/:id")
@UsePipes(new ValidationPipe())
public updateUser(
@Param("id") id: string,
@Body() updateDto: UpdateUserDto,
): User | undefined {
return this.userService.update(id, updateDto);
}
@Delete("/:id")
public deleteUser(@Param("id") id: string): { message: string } {
return this.userService.delete(id);
}
}
返回最终的 Application
import "reflect-metadata";
import express from "express";
import { Application } from "../src/core/Application";
import { UserController } from "./controllers/user.controller";
const app = new Application();
// Set up global middleware using the new flexible approach
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Register controllers
app.registerControllers([UserController]);
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
app.listen(PORT, () => {
console.log(
`TypeScript Web Framework Server is running on http://localhost:${PORT}`,
);
});
export default app;