HTTP 路由
@esengine/server 内置了轻量级的 HTTP 路由功能,可以与 WebSocket 服务共用同一端口,方便实现 REST API。
内联路由定义
Section titled “内联路由定义”最简单的方式是在创建服务器时直接定义 HTTP 路由:
import { createServer } from '@esengine/server'
const server = await createServer({ port: 3000, http: { '/api/health': (req, res) => { res.json({ status: 'ok', time: Date.now() }) }, '/api/users': { GET: (req, res) => { res.json({ users: [] }) }, POST: async (req, res) => { const body = req.body as { name: string } res.status(201).json({ id: '1', name: body.name }) } } }, cors: true // 启用 CORS})
await server.start()对于较大的项目,推荐使用文件路由。创建 src/http 目录,每个文件对应一个路由:
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 as LoginBody
// 验证用户... if (username === 'admin' && password === '123456') { res.json({ token: 'jwt-token-here', userId: 'user-1' }) } else { res.error(401, '用户名或密码错误') } }})import { createServer } from '@esengine/server'
const server = await createServer({ port: 3000, httpDir: './src/http', // HTTP 路由目录 httpPrefix: '/api', // 路由前缀 cors: true})
await server.start()// 路由: POST /api/logindefineHttp 定义
Section titled “defineHttp 定义”defineHttp 用于定义类型安全的 HTTP 处理器:
import { defineHttp } from '@esengine/server'
interface CreateUserBody { username: string email: string password: string}
export default defineHttp<CreateUserBody>({ // HTTP 方法(默认 POST) method: 'POST',
// 处理函数 handler(req, res) { const body = req.body as CreateUserBody // 处理请求... res.status(201).json({ id: 'new-user-id' }) }})支持的 HTTP 方法
Section titled “支持的 HTTP 方法”type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'HttpRequest 对象
Section titled “HttpRequest 对象”HTTP 请求对象包含以下属性:
interface HttpRequest { /** 原始 Node.js IncomingMessage */ raw: IncomingMessage
/** HTTP 方法 */ method: string
/** 请求路径 */ path: string
/** 路由参数(从 URL 路径提取,如 /users/:id) */ params: Record<string, string>
/** 查询参数 */ query: Record<string, string>
/** 请求头 */ headers: Record<string, string | string[] | undefined>
/** 解析后的请求体 */ body: unknown
/** 客户端 IP */ ip: string}export default defineHttp({ method: 'GET', handler(req, res) { // 获取查询参数 const page = parseInt(req.query.page ?? '1') const limit = parseInt(req.query.limit ?? '10')
// 获取请求头 const authHeader = req.headers.authorization
// 获取客户端 IP console.log('Request from:', req.ip)
res.json({ page, limit }) }})请求体会根据 Content-Type 自动解析:
application/json- 解析为 JSON 对象application/x-www-form-urlencoded- 解析为键值对对象- 其他 - 保持原始字符串
export default defineHttp<{ name: string; age: number }>({ method: 'POST', handler(req, res) { // body 已自动解析 const { name, age } = req.body as { name: string; age: number } res.json({ received: { name, age } }) }})HttpResponse 对象
Section titled “HttpResponse 对象”HTTP 响应对象提供链式 API:
interface HttpResponse { /** 原始 Node.js ServerResponse */ raw: ServerResponse
/** 设置状态码 */ status(code: number): HttpResponse
/** 设置响应头 */ header(name: string, value: string): HttpResponse
/** 发送 JSON 响应 */ json(data: unknown): void
/** 发送文本响应 */ text(data: string): void
/** 发送错误响应 */ error(code: number, message: string): void}export default defineHttp({ method: 'POST', handler(req, res) { // 设置状态码和自定义头 res .status(201) .header('X-Custom-Header', 'value') .json({ created: true }) }})export default defineHttp({ method: 'GET', handler(req, res) { // 发送错误响应 res.error(404, '资源不存在') // 等价于: res.status(404).json({ error: '资源不存在' }) }})export default defineHttp({ method: 'GET', handler(req, res) { // 发送纯文本 res.text('Hello, World!') }})文件路由规范
Section titled “文件路由规范”文件名会自动转换为路由路径:
| 文件路径 | 路由路径(prefix=/api) |
|---|---|
login.ts | /api/login |
users/profile.ts | /api/users/profile |
users/[id].ts | /api/users/:id |
game/room/[roomId].ts | /api/game/room/:roomId |
动态路由参数
Section titled “动态路由参数”使用 [param] 语法定义动态参数:
import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', handler(req, res) { // 直接从 params 获取路由参数 const { id } = req.params res.json({ userId: id }) }})多个参数的情况:
import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', handler(req, res) { const { userId, postId } = req.params res.json({ userId, postId }) }})以下文件会被自动跳过:
- 以
_开头的文件(如_helper.ts) index.ts/index.js文件- 非
.ts/.js/.mts/.mjs文件
目录结构示例
Section titled “目录结构示例”src/└── http/ ├── _utils.ts # 跳过(下划线开头) ├── index.ts # 跳过(index 文件) ├── health.ts # GET /api/health ├── login.ts # POST /api/login ├── register.ts # POST /api/register └── users/ ├── index.ts # 跳过 ├── list.ts # GET /api/users/list └── [id].ts # GET /api/users/:idCORS 配置
Section titled “CORS 配置”const server = await createServer({ port: 3000, cors: true // 使用默认配置})const server = await createServer({ port: 3000, cors: { // 允许的来源 origin: ['http://localhost:5173', 'https://myapp.com'], // 或使用通配符 // origin: '*', // origin: true, // 反射请求来源
// 允许的 HTTP 方法 methods: ['GET', 'POST', 'PUT', 'DELETE'],
// 允许的请求头 allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// 是否允许携带凭证(cookies) credentials: true,
// 预检请求缓存时间(秒) maxAge: 86400 }})CorsOptions 类型
Section titled “CorsOptions 类型”interface CorsOptions { /** 允许的来源:字符串、字符串数组、true(反射)或 '*' */ origin?: string | string[] | boolean
/** 允许的 HTTP 方法 */ methods?: string[]
/** 允许的请求头 */ allowedHeaders?: string[]
/** 是否允许携带凭证 */ credentials?: boolean
/** 预检请求缓存时间(秒) */ maxAge?: number}文件路由和内联路由可以同时使用,内联路由优先级更高:
const server = await createServer({ port: 3000, httpDir: './src/http', httpPrefix: '/api',
// 内联路由会与文件路由合并 http: { '/health': (req, res) => res.json({ status: 'ok' }), '/api/special': (req, res) => res.json({ special: true }) }})与 WebSocket 共用端口
Section titled “与 WebSocket 共用端口”HTTP 路由与 WebSocket 服务自动共用同一端口:
const server = await createServer({ port: 3000, // WebSocket 相关配置 apiDir: './src/api', msgDir: './src/msg',
// HTTP 相关配置 httpDir: './src/http', httpPrefix: '/api', cors: true})
await server.start()
// 同一端口 3000:// - WebSocket: ws://localhost:3000// - HTTP API: http://localhost:3000/api/*游戏服务器登录 API
Section titled “游戏服务器登录 API”import { defineHttp } from '@esengine/server'import { createJwtAuthProvider } from '@esengine/server/auth'
interface LoginRequest { username: string password: string}
interface LoginResponse { token: string userId: string expiresAt: number}
const jwtProvider = createJwtAuthProvider({ secret: process.env.JWT_SECRET!, expiresIn: 3600})
export default defineHttp<LoginRequest>({ method: 'POST', async handler(req, res) { const { username, password } = req.body as LoginRequest
// 验证用户 const user = await db.users.findByUsername(username) if (!user || !await verifyPassword(password, user.passwordHash)) { res.error(401, '用户名或密码错误') return }
// 生成 JWT const token = jwtProvider.sign({ sub: user.id, name: user.username, roles: user.roles })
const response: LoginResponse = { token, userId: user.id, expiresAt: Date.now() + 3600 * 1000 }
res.json(response) }})游戏数据查询 API
Section titled “游戏数据查询 API”import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', async handler(req, res) { const limit = parseInt(req.query.limit ?? '10') const offset = parseInt(req.query.offset ?? '0')
const players = await db.players.findMany({ sort: { score: 'desc' }, limit, offset })
res.json({ data: players, pagination: { limit, offset } }) }})中间件是在路由处理前后执行的函数:
type HttpMiddleware = ( req: HttpRequest, res: HttpResponse, next: () => Promise<void>) => void | Promise<void>import { requestLogger, bodyLimit, responseTime, requestId, securityHeaders} from '@esengine/server'
const server = await createServer({ port: 3000, http: { /* ... */ }, // 全局中间件通过 createHttpRouter 配置})requestLogger - 请求日志
Section titled “requestLogger - 请求日志”import { requestLogger } from '@esengine/server'
// 记录请求和响应时间requestLogger()
// 同时记录请求体requestLogger({ logBody: true })bodyLimit - 请求体大小限制
Section titled “bodyLimit - 请求体大小限制”import { bodyLimit } from '@esengine/server'
// 限制请求体为 1MBbodyLimit(1024 * 1024)responseTime - 响应时间头
Section titled “responseTime - 响应时间头”import { responseTime } from '@esengine/server'
// 自动添加 X-Response-Time 响应头responseTime()requestId - 请求 ID
Section titled “requestId - 请求 ID”import { requestId } from '@esengine/server'
// 自动生成并添加 X-Request-ID 响应头requestId()
// 自定义头名称requestId('X-Trace-ID')securityHeaders - 安全头
Section titled “securityHeaders - 安全头”import { securityHeaders } from '@esengine/server'
// 添加常用安全响应头securityHeaders()
// 自定义配置securityHeaders({ hidePoweredBy: true, frameOptions: 'DENY', noSniff: true})自定义中间件
Section titled “自定义中间件”import type { HttpMiddleware } from '@esengine/server'
// 认证中间件const authMiddleware: HttpMiddleware = async (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) { res.error(401, 'Unauthorized') return // 不调用 next(),终止请求 }
// 验证 token... (req as any).userId = 'decoded-user-id'
await next() // 继续执行后续中间件和处理器}使用 createHttpRouter
Section titled “使用 createHttpRouter”import { createHttpRouter, requestLogger, bodyLimit } from '@esengine/server'
const router = createHttpRouter({ '/api/users': (req, res) => res.json([]), '/api/admin': { GET: { handler: (req, res) => res.json({ admin: true }), middlewares: [adminAuthMiddleware] // 路由级中间件 } }}, { middlewares: [requestLogger(), bodyLimit(1024 * 1024)], // 全局中间件 timeout: 30000 // 全局超时 30 秒})import { createHttpRouter } from '@esengine/server'
const router = createHttpRouter({ '/api/data': async (req, res) => { // 如果处理超过 30 秒,自动返回 408 Request Timeout await someSlowOperation() res.json({ data: 'result' }) }}, { timeout: 30000 // 30 秒})const router = createHttpRouter({ '/api/quick': (req, res) => res.json({ fast: true }),
'/api/slow': { POST: { handler: async (req, res) => { await verySlowOperation() res.json({ done: true }) }, timeout: 120000 // 这个路由允许 2 分钟 } }}, { timeout: 10000 // 全局 10 秒(被路由级覆盖)})- 使用 defineHttp - 获得更好的类型提示和代码组织
- 统一错误处理 - 使用
res.error()返回一致的错误格式 - 启用 CORS - 前后端分离时必须配置
- 目录组织 - 按功能模块组织 HTTP 路由文件
- 验证输入 - 始终验证
req.body和req.query的内容 - 状态码规范 - 遵循 HTTP 状态码规范(200、201、400、401、404、500 等)
- 使用中间件 - 通过中间件实现认证、日志、限流等横切关注点
- 设置超时 - 避免慢请求阻塞服务器