如何手动实现TypeScript HTTP装饰器?基于Node.js+Express技术栈
自行实现NestJS风格的Express装饰器
1. 配置TypeScript编译选项
首先在tsconfig.json中开启装饰器相关编译开关,这是使用TypeScript装饰器的基础:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, "target": "ES2020", "module": "CommonJS", "strict": true, "esModuleInterop": true } }
安装reflect-metadata依赖(TypeScript装饰器的元数据读取必须依赖它):
npm install reflect-metadata --save
在项目入口文件顶部引入该库:
import 'reflect-metadata';
2. 定义元数据常量
用Symbol作为元数据的键,避免命名冲突:
export const CONTROLLER_METADATA = Symbol('controller'); export const ROUTE_METADATA = Symbol('route'); export const PARAMS_METADATA = Symbol('params'); export const BODY_METADATA = Symbol('body');
3. 实现@Controller装饰器
标记类为控制器,同时存储路由前缀:
import { CONTROLLER_METADATA } from './metadata'; export function Controller(prefix: string = ''): ClassDecorator { return (target) => { Reflect.defineMetadata(CONTROLLER_METADATA, prefix, target); }; }
4. 实现HTTP方法装饰器(@Get、@Post等)
通过工厂函数生成不同HTTP方法的装饰器,减少重复代码:
import { ROUTE_METADATA } from './metadata'; type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; function createHttpMethodDecorator(method: HttpMethod) { return (path: string = ''): MethodDecorator => { return (target, propertyKey) => { const existingRoutes = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || []; existingRoutes.push({ method, path, handlerName: propertyKey as string }); Reflect.defineMetadata(ROUTE_METADATA, existingRoutes, target.constructor); }; }; } export const Get = createHttpMethodDecorator('get'); export const Post = createHttpMethodDecorator('post'); export const Put = createHttpMethodDecorator('put'); export const Patch = createHttpMethodDecorator('patch'); export const Delete = createHttpMethodDecorator('delete');
5. 实现参数装饰器(@Params、@Body)
记录参数的类型与索引位置,后续从Express的req对象中提取对应数据:
import { PARAMS_METADATA, BODY_METADATA } from './metadata'; export function Params(): ParameterDecorator { return (target, propertyKey, parameterIndex) => { const paramsMetadata = Reflect.getMetadata(PARAMS_METADATA, target, propertyKey) || []; paramsMetadata.push(parameterIndex); Reflect.defineMetadata(PARAMS_METADATA, paramsMetadata, target, propertyKey); }; } export function Body(): ParameterDecorator { return (target, propertyKey, parameterIndex) => { const bodyMetadata = Reflect.getMetadata(BODY_METADATA, target, propertyKey) || []; bodyMetadata.push(parameterIndex); Reflect.defineMetadata(BODY_METADATA, bodyMetadata, target, propertyKey); }; }
6. 实现路由扫描与Express路由器生成
编写函数扫描控制器类,读取元数据并生成对应Express路由:
import express, { Router, Request, Response } from 'express'; import { CONTROLLER_METADATA, ROUTE_METADATA, PARAMS_METADATA, BODY_METADATA } from './metadata'; export function createRouter(controllers: any[]): Router { const router = express.Router(); controllers.forEach(ControllerClass => { const controllerPrefix = Reflect.getMetadata(CONTROLLER_METADATA, ControllerClass); const controllerInstance = new ControllerClass(); const routes = Reflect.getMetadata(ROUTE_METADATA, ControllerClass) || []; routes.forEach(route => { const { method, path, handlerName } = route; const handler = controllerInstance[handlerName] as Function; // 读取参数注入元数据 const paramsIndices = Reflect.getMetadata(PARAMS_METADATA, controllerInstance, handlerName) || []; const bodyIndices = Reflect.getMetadata(BODY_METADATA, controllerInstance, handlerName) || []; router[method](`${controllerPrefix}${path}`, (req: Request, res: Response) => { const args: any[] = []; // 注入@Params参数 paramsIndices.forEach(index => { args[index] = req.params; // 若需支持@Params('id')单参数提取,可修改Params装饰器存储参数名,此处为通用版本 }); // 注入@Body参数 bodyIndices.forEach(index => { args[index] = req.body; }); // 执行处理函数并响应结果 const result = handler.apply(controllerInstance, args); if (result instanceof Promise) { result.then(data => res.send(data)).catch(err => res.status(500).send(err)); } else { res.send(result); } }); }); }); return router; }
7. 使用示例
编写控制器
import { Controller, Get, Post, Params, Body } from './decorators'; @Controller('/users') export class UserController { @Get('/:id') findOne(@Params() params: { id: string }) { return { userId: params.id, name: 'Test User' }; } @Post('/') create(@Body() userData: { name: string, email: string }) { return { id: '123', ...userData }; } }
启动Express服务器
import express from 'express'; import 'reflect-metadata'; import { createRouter } from './router-creator'; import { UserController } from './user.controller'; const app = express(); app.use(express.json()); const router = createRouter([UserController]); app.use(router); app.listen(3000, () => { console.log('Server running on port 3000'); });
补充说明
- 若需支持
@Params('id')单参数提取,可修改Params装饰器接收参数名,在路由处理函数中提取对应值。 - 可扩展实现
@Query()、@Headers()等装饰器,原理与@Params、@Body一致。 - 可添加全局异常处理逻辑,优化错误响应。
内容的提问来源于stack exchange,提问作者Sabgramar
相关产品推荐
相关产品推荐

