feat(server): add HTTP file-based routing support

- Add file-based HTTP routing with httpDir and httpPrefix config options
- Create defineHttp<TBody>() helper for type-safe route definitions
- Support dynamic routes with [param].ts file naming convention
- Add CORS support for cross-origin requests
- Allow merging file routes with inline http config
- RPC server now supports attaching to existing HTTP server via server option
- Add comprehensive documentation for HTTP routing
This commit is contained in:
yhh
2025-12-31 09:52:45 +08:00
parent 12051d987f
commit d3e489aad3
19 changed files with 1226 additions and 37 deletions

View File

@@ -4,6 +4,7 @@
*/
import * as path from 'node:path'
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http'
import { serve, type RpcServer } from '@esengine/rpc/server'
import { rpc } from '@esengine/rpc'
import type {
@@ -14,18 +15,23 @@ import type {
MsgContext,
LoadedApiHandler,
LoadedMsgHandler,
LoadedHttpHandler,
} from '../types/index.js'
import { loadApiHandlers, loadMsgHandlers } from '../router/loader.js'
import type { HttpRoutes, HttpHandler } from '../http/types.js'
import { loadApiHandlers, loadMsgHandlers, loadHttpHandlers } from '../router/loader.js'
import { RoomManager, type RoomClass, type Room } from '../room/index.js'
import { createHttpRouter } from '../http/router.js'
/**
* @zh 默认配置
* @en Default configuration
*/
const DEFAULT_CONFIG: Required<Omit<ServerConfig, 'onStart' | 'onConnect' | 'onDisconnect'>> = {
const DEFAULT_CONFIG: Required<Omit<ServerConfig, 'onStart' | 'onConnect' | 'onDisconnect' | 'http' | 'cors' | 'httpDir' | 'httpPrefix'>> & { httpDir: string; httpPrefix: string } = {
port: 3000,
apiDir: 'src/api',
msgDir: 'src/msg',
httpDir: 'src/http',
httpPrefix: '/api',
tickRate: 20,
}
@@ -56,12 +62,53 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
const apiHandlers = await loadApiHandlers(path.resolve(cwd, opts.apiDir))
const msgHandlers = await loadMsgHandlers(path.resolve(cwd, opts.msgDir))
// 加载 HTTP 文件路由
const httpDir = config.httpDir ?? opts.httpDir
const httpPrefix = config.httpPrefix ?? opts.httpPrefix
const httpHandlers = await loadHttpHandlers(path.resolve(cwd, httpDir), httpPrefix)
if (apiHandlers.length > 0) {
console.log(`[Server] Loaded ${apiHandlers.length} API handlers`)
}
if (msgHandlers.length > 0) {
console.log(`[Server] Loaded ${msgHandlers.length} message handlers`)
}
if (httpHandlers.length > 0) {
console.log(`[Server] Loaded ${httpHandlers.length} HTTP handlers`)
}
// 合并 HTTP 路由(文件路由 + 内联路由)
const mergedHttpRoutes: HttpRoutes = {}
// 先添加文件路由
for (const handler of httpHandlers) {
const existingRoute = mergedHttpRoutes[handler.route]
if (existingRoute && typeof existingRoute !== 'function') {
(existingRoute as Record<string, HttpHandler>)[handler.method] = handler.definition.handler
} else {
mergedHttpRoutes[handler.route] = {
[handler.method]: handler.definition.handler,
}
}
}
// 再添加内联路由(覆盖文件路由)
if (config.http) {
for (const [route, handlerOrMethods] of Object.entries(config.http)) {
if (typeof handlerOrMethods === 'function') {
mergedHttpRoutes[route] = handlerOrMethods
} else {
const existing = mergedHttpRoutes[route]
if (existing && typeof existing !== 'function') {
Object.assign(existing, handlerOrMethods)
} else {
mergedHttpRoutes[route] = handlerOrMethods
}
}
}
}
const hasHttpRoutes = Object.keys(mergedHttpRoutes).length > 0
// 动态构建协议
const apiDefs: Record<string, ReturnType<typeof rpc.api>> = {
@@ -90,6 +137,7 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
let currentTick = 0
let tickInterval: ReturnType<typeof setInterval> | null = null
let rpcServer: RpcServer<typeof protocol, Record<string, unknown>> | null = null
let httpServer: HttpServer | null = null
// 房间管理器(立即初始化,以便 define() 可在 start() 前调用)
const roomManager = new RoomManager((conn, type, data) => {
@@ -200,26 +248,68 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
}
}
rpcServer = serve(protocol, {
port: opts.port,
createConnData: () => ({}),
onStart: (p) => {
console.log(`[Server] Started on ws://localhost:${p}`)
opts.onStart?.(p)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
// 玩家断线时自动离开房间
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
// 如果有 HTTP 路由,创建 HTTP 服务器
if (hasHttpRoutes) {
const httpRouter = createHttpRouter(mergedHttpRoutes, config.cors ?? true)
await rpcServer.start()
httpServer = createHttpServer(async (req, res) => {
// 先尝试 HTTP 路由
const handled = await httpRouter(req, res)
if (!handled) {
// 未匹配的请求返回 404
res.statusCode = 404
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ error: 'Not Found' }))
}
})
// 使用 HTTP 服务器创建 RPC
rpcServer = serve(protocol, {
server: httpServer,
createConnData: () => ({}),
onStart: () => {
console.log(`[Server] Started on http://localhost:${opts.port}`)
opts.onStart?.(opts.port)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
await rpcServer.start()
// 启动 HTTP 服务器
await new Promise<void>((resolve) => {
httpServer!.listen(opts.port, () => resolve())
})
} else {
// 仅 WebSocket 模式
rpcServer = serve(protocol, {
port: opts.port,
createConnData: () => ({}),
onStart: (p) => {
console.log(`[Server] Started on ws://localhost:${p}`)
opts.onStart?.(p)
},
onConnect: async (conn) => {
await config.onConnect?.(conn as ServerConnection)
},
onDisconnect: async (conn) => {
await roomManager?.leave(conn.id, 'disconnected')
await config.onDisconnect?.(conn as ServerConnection)
},
api: apiHandlersObj as any,
msg: msgHandlersObj as any,
})
await rpcServer.start()
}
// 启动 tick 循环
if (opts.tickRate > 0) {
@@ -238,6 +328,15 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
await rpcServer.stop()
rpcServer = null
}
if (httpServer) {
await new Promise<void>((resolve, reject) => {
httpServer!.close((err) => {
if (err) reject(err)
else resolve()
})
})
httpServer = null
}
},
broadcast(name, data) {

View File

@@ -1,9 +1,9 @@
/**
* @zh API 和消息定义助手
* @en API and message definition helpers
* @zh API、消息和 HTTP 定义助手
* @en API, message, and HTTP definition helpers
*/
import type { ApiDefinition, MsgDefinition } from '../types/index.js'
import type { ApiDefinition, MsgDefinition, HttpDefinition } from '../types/index.js'
/**
* @zh 定义 API 处理器
@@ -49,3 +49,33 @@ export function defineMsg<TMsg, TData = Record<string, unknown>>(
): MsgDefinition<TMsg, TData> {
return definition
}
/**
* @zh 定义 HTTP 路由处理器
* @en Define HTTP route handler
*
* @example
* ```typescript
* // src/http/login.ts
* import { defineHttp } from '@esengine/server'
*
* interface LoginBody {
* username: string
* password: string
* }
*
* export default defineHttp<LoginBody>({
* method: 'POST',
* handler(req, res) {
* const { username, password } = req.body
* // ... validate credentials
* res.json({ token: '...', userId: '...' })
* }
* })
* ```
*/
export function defineHttp<TBody = unknown>(
definition: HttpDefinition<TBody>
): HttpDefinition<TBody> {
return definition
}

View File

@@ -0,0 +1,7 @@
/**
* @zh HTTP 模块导出
* @en HTTP module exports
*/
export * from './types.js';
export { createHttpRouter } from './router.js';

View File

@@ -0,0 +1,263 @@
/**
* @zh HTTP 路由器
* @en HTTP Router
*
* @zh 简洁的 HTTP 路由实现,支持与 WebSocket 共用端口
* @en Simple HTTP router implementation, supports sharing port with WebSocket
*/
import type { IncomingMessage, ServerResponse } from 'node:http';
import type {
HttpRequest,
HttpResponse,
HttpHandler,
HttpRoutes,
CorsOptions,
} from './types.js';
/**
* @zh 创建 HTTP 请求对象
* @en Create HTTP request object
*/
async function createRequest(req: IncomingMessage): Promise<HttpRequest> {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
// 解析查询参数
const query: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
// 解析请求体
let body: unknown = null;
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
body = await parseBody(req);
}
// 获取客户端 IP
const ip =
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
req.socket?.remoteAddress ||
'unknown';
return {
raw: req,
method: req.method ?? 'GET',
path: url.pathname,
query,
headers: req.headers as Record<string, string | string[] | undefined>,
body,
ip,
};
}
/**
* @zh 解析请求体
* @en Parse request body
*/
function parseBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
req.on('end', () => {
const rawBody = Buffer.concat(chunks).toString('utf-8');
if (!rawBody) {
resolve(null);
return;
}
const contentType = req.headers['content-type'] ?? '';
if (contentType.includes('application/json')) {
try {
resolve(JSON.parse(rawBody));
} catch {
resolve(rawBody);
}
} else if (contentType.includes('application/x-www-form-urlencoded')) {
const params = new URLSearchParams(rawBody);
const result: Record<string, string> = {};
params.forEach((value, key) => {
result[key] = value;
});
resolve(result);
} else {
resolve(rawBody);
}
});
req.on('error', () => {
resolve(null);
});
});
}
/**
* @zh 创建 HTTP 响应对象
* @en Create HTTP response object
*/
function createResponse(res: ServerResponse): HttpResponse {
let statusCode = 200;
const response: HttpResponse = {
raw: res,
status(code: number) {
statusCode = code;
return response;
},
header(name: string, value: string) {
res.setHeader(name, value);
return response;
},
json(data: unknown) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.statusCode = statusCode;
res.end(JSON.stringify(data));
},
text(data: string) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.statusCode = statusCode;
res.end(data);
},
error(code: number, message: string) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.statusCode = code;
res.end(JSON.stringify({ error: message }));
},
};
return response;
}
/**
* @zh 应用 CORS 头
* @en Apply CORS headers
*/
function applyCors(res: ServerResponse, req: IncomingMessage, cors: CorsOptions): void {
const origin = req.headers.origin;
// 处理 origin
if (cors.origin === true || cors.origin === '*') {
res.setHeader('Access-Control-Allow-Origin', origin ?? '*');
} else if (typeof cors.origin === 'string') {
res.setHeader('Access-Control-Allow-Origin', cors.origin);
} else if (Array.isArray(cors.origin) && origin && cors.origin.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 允许的方法
if (cors.methods) {
res.setHeader('Access-Control-Allow-Methods', cors.methods.join(', '));
} else {
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
}
// 允许的头
if (cors.allowedHeaders) {
res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders.join(', '));
} else {
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
// 凭证
if (cors.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// 缓存
if (cors.maxAge) {
res.setHeader('Access-Control-Max-Age', String(cors.maxAge));
}
}
/**
* @zh 创建 HTTP 路由器
* @en Create HTTP router
*/
export function createHttpRouter(routes: HttpRoutes, cors?: CorsOptions | boolean) {
// 解析路由
const parsedRoutes: Array<{
method: string;
path: string;
handler: HttpHandler;
}> = [];
for (const [path, handlerOrMethods] of Object.entries(routes)) {
if (typeof handlerOrMethods === 'function') {
// 简单形式:路径 -> 处理器(接受所有方法)
parsedRoutes.push({ method: '*', path, handler: handlerOrMethods });
} else {
// 对象形式:路径 -> { GET, POST, ... }
for (const [method, handler] of Object.entries(handlerOrMethods)) {
if (handler !== undefined) {
parsedRoutes.push({ method, path, handler });
}
}
}
}
// 默认 CORS 配置
const corsOptions: CorsOptions | null =
cors === true
? { origin: true, credentials: true }
: cors === false
? null
: cors ?? null;
/**
* @zh 处理 HTTP 请求
* @en Handle HTTP request
*/
return async function handleRequest(
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
const path = url.pathname;
const method = req.method ?? 'GET';
// 应用 CORS
if (corsOptions) {
applyCors(res, req, corsOptions);
// 处理预检请求
if (method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return true;
}
}
// 查找匹配的路由
const route = parsedRoutes.find(
(r) => r.path === path && (r.method === '*' || r.method === method)
);
if (!route) {
return false; // 未找到路由,让其他处理器处理
}
try {
const httpReq = await createRequest(req);
const httpRes = createResponse(res);
await route.handler(httpReq, httpRes);
return true;
} catch (error) {
console.error('[HTTP] Route handler error:', error);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Internal Server Error' }));
return true;
}
};
}

View File

@@ -0,0 +1,161 @@
/**
* @zh HTTP 路由类型定义
* @en HTTP router type definitions
*/
import type { IncomingMessage, ServerResponse } from 'node:http';
/**
* @zh HTTP 请求上下文
* @en HTTP request context
*/
export interface HttpRequest {
/**
* @zh 原始请求对象
* @en Raw request object
*/
raw: IncomingMessage;
/**
* @zh 请求方法
* @en Request method
*/
method: string;
/**
* @zh 请求路径
* @en Request path
*/
path: string;
/**
* @zh 查询参数
* @en Query parameters
*/
query: Record<string, string>;
/**
* @zh 请求头
* @en Request headers
*/
headers: Record<string, string | string[] | undefined>;
/**
* @zh 解析后的 JSON 请求体
* @en Parsed JSON body
*/
body: unknown;
/**
* @zh 客户端 IP
* @en Client IP
*/
ip: string;
}
/**
* @zh HTTP 响应工具
* @en HTTP response utilities
*/
export interface HttpResponse {
/**
* @zh 原始响应对象
* @en Raw response object
*/
raw: ServerResponse;
/**
* @zh 设置状态码
* @en Set status code
*/
status(code: number): HttpResponse;
/**
* @zh 设置响应头
* @en Set response header
*/
header(name: string, value: string): HttpResponse;
/**
* @zh 发送 JSON 响应
* @en Send JSON response
*/
json(data: unknown): void;
/**
* @zh 发送文本响应
* @en Send text response
*/
text(data: string): void;
/**
* @zh 发送错误响应
* @en Send error response
*/
error(code: number, message: string): void;
}
/**
* @zh HTTP 路由处理器
* @en HTTP route handler
*/
export type HttpHandler = (req: HttpRequest, res: HttpResponse) => void | Promise<void>;
/**
* @zh HTTP 路由定义
* @en HTTP route definition
*/
export interface HttpRoute {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | '*';
path: string;
handler: HttpHandler;
}
/**
* @zh HTTP 路由配置
* @en HTTP routes configuration
*/
export type HttpRoutes = Record<string, HttpHandler | {
GET?: HttpHandler;
POST?: HttpHandler;
PUT?: HttpHandler;
DELETE?: HttpHandler;
PATCH?: HttpHandler;
OPTIONS?: HttpHandler;
}>;
/**
* @zh CORS 配置
* @en CORS configuration
*/
export interface CorsOptions {
/**
* @zh 允许的来源
* @en Allowed origins
*/
origin?: string | string[] | boolean;
/**
* @zh 允许的方法
* @en Allowed methods
*/
methods?: string[];
/**
* @zh 允许的请求头
* @en Allowed headers
*/
allowedHeaders?: string[];
/**
* @zh 是否允许携带凭证
* @en Allow credentials
*/
credentials?: boolean;
/**
* @zh 预检请求缓存时间(秒)
* @en Preflight cache max age
*/
maxAge?: number;
}

View File

@@ -30,7 +30,7 @@
export { createServer } from './core/server.js'
// Helpers
export { defineApi, defineMsg } from './helpers/define.js'
export { defineApi, defineMsg, defineHttp } from './helpers/define.js'
// Room System
export { Room, type RoomOptions } from './room/Room.js'
@@ -46,7 +46,19 @@ export type {
MsgContext,
ApiDefinition,
MsgDefinition,
HttpDefinition,
HttpMethod,
} from './types/index.js'
// HTTP
export { createHttpRouter } from './http/router.js'
export type {
HttpRequest,
HttpResponse,
HttpHandler,
HttpRoutes,
CorsOptions,
} from './http/types.js'
// Re-export useful types from @esengine/rpc
export { RpcError, ErrorCode } from '@esengine/rpc'

View File

@@ -6,7 +6,15 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import { pathToFileURL } from 'node:url'
import type { ApiDefinition, MsgDefinition, LoadedApiHandler, LoadedMsgHandler } from '../types/index.js'
import type {
ApiDefinition,
MsgDefinition,
HttpDefinition,
LoadedApiHandler,
LoadedMsgHandler,
LoadedHttpHandler,
HttpMethod,
} from '../types/index.js'
/**
* @zh 将文件名转换为 API/消息名称
@@ -110,3 +118,106 @@ export async function loadMsgHandlers(msgDir: string): Promise<LoadedMsgHandler[
return handlers
}
/**
* @zh 递归扫描目录获取所有处理器文件
* @en Recursively scan directory for all handler files
*/
function scanDirectoryRecursive(dir: string, baseDir: string = dir): Array<{ filePath: string; relativePath: string }> {
if (!fs.existsSync(dir)) {
return []
}
const files: Array<{ filePath: string; relativePath: string }> = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...scanDirectoryRecursive(fullPath, baseDir))
} else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name)) {
if (entry.name.startsWith('_') || entry.name.startsWith('index.')) {
continue
}
const relativePath = path.relative(baseDir, fullPath)
files.push({ filePath: fullPath, relativePath })
}
}
return files
}
/**
* @zh 将文件路径转换为路由路径
* @en Convert file path to route path
*
* @example
* 'login.ts' -> '/login'
* 'users/profile.ts' -> '/users/profile'
* 'users/[id].ts' -> '/users/:id'
*/
function filePathToRoute(relativePath: string, prefix: string): string {
let route = relativePath
.replace(/\.(ts|js|mts|mjs)$/, '')
.replace(/\\/g, '/')
.replace(/\[([^\]]+)\]/g, ':$1')
if (!route.startsWith('/')) {
route = '/' + route
}
const fullRoute = prefix.endsWith('/')
? prefix.slice(0, -1) + route
: prefix + route
return fullRoute
}
/**
* @zh 加载 HTTP 路由处理器
* @en Load HTTP route handlers
*
* @example
* ```typescript
* // Directory structure:
* // src/http/
* // login.ts -> POST /api/login
* // register.ts -> POST /api/register
* // users/
* // [id].ts -> GET /api/users/:id
*
* const handlers = await loadHttpHandlers('src/http', '/api')
* ```
*/
export async function loadHttpHandlers(
httpDir: string,
prefix: string = '/api'
): Promise<LoadedHttpHandler[]> {
const files = scanDirectoryRecursive(httpDir)
const handlers: LoadedHttpHandler[] = []
for (const { filePath, relativePath } of files) {
try {
const fileUrl = pathToFileURL(filePath).href
const module = await import(fileUrl)
const definition = module.default as HttpDefinition<unknown>
if (definition && typeof definition.handler === 'function') {
const route = filePathToRoute(relativePath, prefix)
const method: HttpMethod = definition.method ?? 'POST'
handlers.push({
route,
method,
path: filePath,
definition,
})
}
} catch (err) {
console.warn(`[Server] Failed to load HTTP handler: ${filePath}`, err)
}
}
return handlers
}

View File

@@ -4,6 +4,7 @@
*/
import type { Connection, ProtocolDef } from '@esengine/rpc'
import type { HttpRoutes, CorsOptions, HttpRequest, HttpResponse } from '../http/types.js'
// ============================================================================
// Server Config
@@ -35,6 +36,29 @@ export interface ServerConfig {
*/
msgDir?: string
/**
* @zh HTTP 路由目录路径
* @en HTTP routes directory path
* @default 'src/http'
*
* @zh 文件命名规则:
* - `login.ts` → POST /api/login
* - `users/[id].ts` → /api/users/:id
* - `health.ts` (method: 'GET') → GET /api/health
* @en File naming convention:
* - `login.ts` → POST /api/login
* - `users/[id].ts` → /api/users/:id
* - `health.ts` (method: 'GET') → GET /api/health
*/
httpDir?: string
/**
* @zh HTTP 路由前缀
* @en HTTP routes prefix
* @default '/api'
*/
httpPrefix?: string
/**
* @zh 游戏 Tick 速率 (每秒)
* @en Game tick rate (per second)
@@ -42,6 +66,19 @@ export interface ServerConfig {
*/
tickRate?: number
/**
* @zh HTTP 路由配置(内联定义,与 httpDir 文件路由合并)
* @en HTTP routes configuration (inline definition, merged with httpDir file routes)
*/
http?: HttpRoutes
/**
* @zh CORS 配置
* @en CORS configuration
* @default true
*/
cors?: CorsOptions | boolean
/**
* @zh 服务器启动回调
* @en Server start callback
@@ -232,3 +269,80 @@ export interface LoadedMsgHandler {
path: string
definition: MsgDefinition<any, any>
}
// ============================================================================
// HTTP Definition
// ============================================================================
/**
* @zh HTTP 请求方法
* @en HTTP request method
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
/**
* @zh HTTP 定义选项
* @en HTTP definition options
*
* @example
* ```typescript
* // src/http/login.ts
* import { defineHttp } from '@esengine/server'
*
* export default defineHttp({
* method: 'POST',
* handler: async (req, res) => {
* const { username, password } = req.body
* // ... authentication logic
* res.json({ token: '...', userId: '...' })
* }
* })
* ```
*/
export interface HttpDefinition<TBody = unknown> {
/**
* @zh 请求方法
* @en Request method
* @default 'POST'
*/
method?: HttpMethod
/**
* @zh 处理函数
* @en Handler function
*/
handler: (
req: HttpRequest & { body: TBody },
res: HttpResponse
) => void | Promise<void>
}
/**
* @zh 已加载的 HTTP 处理器
* @en Loaded HTTP handler
*/
export interface LoadedHttpHandler {
/**
* @zh 路由路径(如 /api/login
* @en Route path (e.g., /api/login)
*/
route: string
/**
* @zh 请求方法
* @en Request method
*/
method: HttpMethod
/**
* @zh 源文件路径
* @en Source file path
*/
path: string
/**
* @zh 处理器定义
* @en Handler definition
*/
definition: HttpDefinition<any>
}