feat: 添加路由装饰器系统和全局异常处理
- 新增装饰器支持(@Get, @Post, @Put, @Delete, @Patch, @Controller) - 实现路由自动注册机制(RouteScanner) - 添加全局异常处理中间件(Exception) - 实现统一响应体格式(ApiResponse) - 新增请求体解析中间件(BodyParser) - 重构控制器为类模式,支持装饰器路由 - 添加示例用户控制器(UserController) - 更新TypeScript配置支持装饰器 - 添加reflect-metadata依赖 - 完善项目文档 Breaking Changes: - 控制器现在返回数据而不是直接设置ctx.body - 新增统一的API响应格式
This commit is contained in:
166
apps/server/libs/route-scanner.ts
Normal file
166
apps/server/libs/route-scanner.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import type Koa from 'koa';
|
||||
import KoaRouter from '@koa/router';
|
||||
import { getRouteMetadata, getControllerPrefix, type RouteMetadata } from '../decorators/route.ts';
|
||||
import { createAutoSuccessResponse } from '../middlewares/exception.ts';
|
||||
|
||||
/**
|
||||
* 控制器类型
|
||||
*/
|
||||
export interface ControllerClass {
|
||||
new (...args: any[]): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由扫描器,用于自动注册装饰器标注的路由
|
||||
*/
|
||||
export class RouteScanner {
|
||||
private router: KoaRouter;
|
||||
private controllers: ControllerClass[] = [];
|
||||
|
||||
constructor(prefix: string = '/api') {
|
||||
this.router = new KoaRouter({ prefix });
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册控制器类
|
||||
*/
|
||||
registerController(ControllerClass: ControllerClass): void {
|
||||
this.controllers.push(ControllerClass);
|
||||
this.scanController(ControllerClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册多个控制器类
|
||||
*/
|
||||
registerControllers(controllers: ControllerClass[]): void {
|
||||
controllers.forEach(controller => this.registerController(controller));
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描控制器并注册路由
|
||||
*/
|
||||
private scanController(ControllerClass: ControllerClass): void {
|
||||
// 创建控制器实例
|
||||
const controllerInstance = new ControllerClass();
|
||||
|
||||
// 获取控制器的路由前缀
|
||||
const controllerPrefix = getControllerPrefix(ControllerClass);
|
||||
|
||||
// 获取控制器的路由元数据
|
||||
const routes: RouteMetadata[] = getRouteMetadata(ControllerClass);
|
||||
|
||||
// 注册每个路由
|
||||
routes.forEach(route => {
|
||||
const fullPath = this.buildFullPath(controllerPrefix, route.path);
|
||||
const handler = this.wrapControllerMethod(controllerInstance, route.propertyKey);
|
||||
|
||||
// 根据HTTP方法注册路由
|
||||
switch (route.method) {
|
||||
case 'GET':
|
||||
this.router.get(fullPath, handler);
|
||||
break;
|
||||
case 'POST':
|
||||
this.router.post(fullPath, handler);
|
||||
break;
|
||||
case 'PUT':
|
||||
this.router.put(fullPath, handler);
|
||||
break;
|
||||
case 'DELETE':
|
||||
this.router.delete(fullPath, handler);
|
||||
break;
|
||||
case 'PATCH':
|
||||
this.router.patch(fullPath, handler);
|
||||
break;
|
||||
default:
|
||||
console.warn(`未支持的HTTP方法: ${route.method}`);
|
||||
}
|
||||
|
||||
console.log(`注册路由: ${route.method} ${fullPath} -> ${ControllerClass.name}.${route.propertyKey}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的路由路径
|
||||
*/
|
||||
private buildFullPath(controllerPrefix: string, routePath: string): string {
|
||||
// 清理和拼接路径
|
||||
const cleanControllerPrefix = controllerPrefix.replace(/^\/+|\/+$/g, '');
|
||||
const cleanRoutePath = routePath.replace(/^\/+|\/+$/g, '');
|
||||
|
||||
let fullPath = '';
|
||||
if (cleanControllerPrefix) {
|
||||
fullPath += '/' + cleanControllerPrefix;
|
||||
}
|
||||
if (cleanRoutePath) {
|
||||
fullPath += '/' + cleanRoutePath;
|
||||
}
|
||||
|
||||
// 如果路径为空,返回根路径
|
||||
return fullPath || '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装控制器方法,统一处理响应格式
|
||||
*/
|
||||
private wrapControllerMethod(instance: any, methodName: string) {
|
||||
return async (ctx: Koa.Context, next: Koa.Next) => {
|
||||
try {
|
||||
// 调用控制器方法
|
||||
const method = instance[methodName];
|
||||
if (typeof method !== 'function') {
|
||||
throw new Error(`控制器方法 ${methodName} 不存在或不是函数`);
|
||||
}
|
||||
|
||||
// 绑定this并调用方法
|
||||
const result = await method.call(instance, ctx, next);
|
||||
|
||||
// 如果控制器返回了数据,则包装成统一响应格式
|
||||
if (result !== undefined) {
|
||||
ctx.body = createAutoSuccessResponse(result);
|
||||
}
|
||||
// 如果控制器没有返回数据,说明已经自己处理了响应
|
||||
} catch (error) {
|
||||
// 错误由全局异常处理中间件处理
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Koa路由器实例,用于应用到Koa应用中
|
||||
*/
|
||||
getRouter(): KoaRouter {
|
||||
return this.router;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用路由到Koa应用
|
||||
*/
|
||||
applyToApp(app: Koa): void {
|
||||
app.use(this.router.routes());
|
||||
app.use(this.router.allowedMethods());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的路由信息(用于调试)
|
||||
*/
|
||||
getRegisteredRoutes(): Array<{ method: string; path: string; controller: string; action: string }> {
|
||||
const routes: Array<{ method: string; path: string; controller: string; action: string }> = [];
|
||||
|
||||
this.controllers.forEach(ControllerClass => {
|
||||
const controllerPrefix = getControllerPrefix(ControllerClass);
|
||||
const routeMetadata = getRouteMetadata(ControllerClass);
|
||||
|
||||
routeMetadata.forEach(route => {
|
||||
routes.push({
|
||||
method: route.method,
|
||||
path: this.buildFullPath(controllerPrefix, route.path),
|
||||
controller: ControllerClass.name,
|
||||
action: route.propertyKey
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user