From ff549f3c2aca127b714b00f9cc36d2e720765809 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 31 Dec 2025 22:53:38 +0800 Subject: [PATCH] docs(network): add HTTP routing documentation (#415) Add comprehensive HTTP routing documentation for the server module: - Create new http.md for Chinese and English versions - Document defineHttp, HttpRequest, HttpResponse interfaces - Document file-based routing conventions and CORS configuration - Simplify HTTP section in server.md with link to detailed docs --- docs/astro.config.mjs | 1 + .../content/docs/en/modules/network/http.md | 490 ++++++++++++++++++ .../content/docs/en/modules/network/server.md | 125 +---- docs/src/content/docs/modules/network/http.md | 490 ++++++++++++++++++ .../content/docs/modules/network/server.md | 125 +---- 5 files changed, 1006 insertions(+), 225 deletions(-) create mode 100644 docs/src/content/docs/en/modules/network/http.md create mode 100644 docs/src/content/docs/modules/network/http.md diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index b9d35c08..b0da21ee 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -267,6 +267,7 @@ export default defineConfig({ { label: '概述', slug: 'modules/network', translations: { en: 'Overview' } }, { label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } }, { label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } }, + { label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } }, { label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } }, { label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } }, { label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } }, diff --git a/docs/src/content/docs/en/modules/network/http.md b/docs/src/content/docs/en/modules/network/http.md new file mode 100644 index 00000000..f6373eda --- /dev/null +++ b/docs/src/content/docs/en/modules/network/http.md @@ -0,0 +1,490 @@ +--- +title: "HTTP Routing" +description: "HTTP REST API routing with WebSocket port sharing support" +--- + +`@esengine/server` includes a lightweight HTTP routing feature that can share the same port with WebSocket services, making it easy to implement REST APIs. + +## Quick Start + +### Inline Route Definition + +The simplest way is to define HTTP routes directly when creating the server: + +```typescript +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 // Enable CORS +}) + +await server.start() +``` + +### File-based Routing + +For larger projects, file-based routing is recommended. Create a `src/http` directory where each file corresponds to a route: + +```typescript +// src/http/login.ts +import { defineHttp } from '@esengine/server' + +interface LoginBody { + username: string + password: string +} + +export default defineHttp({ + method: 'POST', + handler(req, res) { + const { username, password } = req.body as LoginBody + + // Validate user... + if (username === 'admin' && password === '123456') { + res.json({ token: 'jwt-token-here', userId: 'user-1' }) + } else { + res.error(401, 'Invalid username or password') + } + } +}) +``` + +```typescript +// server.ts +import { createServer } from '@esengine/server' + +const server = await createServer({ + port: 3000, + httpDir: './src/http', // HTTP routes directory + httpPrefix: '/api', // Route prefix + cors: true +}) + +await server.start() +// Route: POST /api/login +``` + +## defineHttp Definition + +`defineHttp` is used to define type-safe HTTP handlers: + +```typescript +import { defineHttp } from '@esengine/server' + +interface CreateUserBody { + username: string + email: string + password: string +} + +export default defineHttp({ + // HTTP method (default POST) + method: 'POST', + + // Handler function + handler(req, res) { + const body = req.body as CreateUserBody + // Handle request... + res.status(201).json({ id: 'new-user-id' }) + } +}) +``` + +### Supported HTTP Methods + +```typescript +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' +``` + +## HttpRequest Object + +The HTTP request object contains the following properties: + +```typescript +interface HttpRequest { + /** Raw Node.js IncomingMessage */ + raw: IncomingMessage + + /** HTTP method */ + method: string + + /** Request path */ + path: string + + /** Query parameters */ + query: Record + + /** Request headers */ + headers: Record + + /** Parsed request body */ + body: unknown + + /** Client IP */ + ip: string +} +``` + +### Usage Examples + +```typescript +export default defineHttp({ + method: 'GET', + handler(req, res) { + // Get query parameters + const page = parseInt(req.query.page ?? '1') + const limit = parseInt(req.query.limit ?? '10') + + // Get request headers + const authHeader = req.headers.authorization + + // Get client IP + console.log('Request from:', req.ip) + + res.json({ page, limit }) + } +}) +``` + +### Body Parsing + +The request body is automatically parsed based on `Content-Type`: + +- `application/json` - Parsed as JSON object +- `application/x-www-form-urlencoded` - Parsed as key-value object +- Others - Kept as raw string + +```typescript +export default defineHttp<{ name: string; age: number }>({ + method: 'POST', + handler(req, res) { + // body is already parsed + const { name, age } = req.body as { name: string; age: number } + res.json({ received: { name, age } }) + } +}) +``` + +## HttpResponse Object + +The HTTP response object provides a chainable API: + +```typescript +interface HttpResponse { + /** Raw Node.js ServerResponse */ + raw: ServerResponse + + /** Set status code */ + status(code: number): HttpResponse + + /** Set response header */ + header(name: string, value: string): HttpResponse + + /** Send JSON response */ + json(data: unknown): void + + /** Send text response */ + text(data: string): void + + /** Send error response */ + error(code: number, message: string): void +} +``` + +### Usage Examples + +```typescript +export default defineHttp({ + method: 'POST', + handler(req, res) { + // Set status code and custom headers + res + .status(201) + .header('X-Custom-Header', 'value') + .json({ created: true }) + } +}) +``` + +```typescript +export default defineHttp({ + method: 'GET', + handler(req, res) { + // Send error response + res.error(404, 'Resource not found') + // Equivalent to: res.status(404).json({ error: 'Resource not found' }) + } +}) +``` + +```typescript +export default defineHttp({ + method: 'GET', + handler(req, res) { + // Send plain text + res.text('Hello, World!') + } +}) +``` + +## File Routing Conventions + +### Name Conversion + +File names are automatically converted to route paths: + +| File Path | Route Path (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` | + +### Dynamic Route Parameters + +Use `[param]` syntax to define dynamic parameters: + +```typescript +// src/http/users/[id].ts +import { defineHttp } from '@esengine/server' + +export default defineHttp({ + method: 'GET', + handler(req, res) { + // Get parameter from path + // Note: current version requires manual path parsing + const id = req.path.split('/').pop() + res.json({ userId: id }) + } +}) +``` + +### Skip Rules + +The following files are automatically skipped: + +- Files starting with `_` (e.g., `_helper.ts`) +- `index.ts` / `index.js` files +- Non `.ts` / `.js` / `.mts` / `.mjs` files + +### Directory Structure Example + +``` +src/ +└── http/ + ├── _utils.ts # Skipped (underscore prefix) + ├── index.ts # Skipped (index file) + ├── health.ts # GET /api/health + ├── login.ts # POST /api/login + ├── register.ts # POST /api/register + └── users/ + ├── index.ts # Skipped + ├── list.ts # GET /api/users/list + └── [id].ts # GET /api/users/:id +``` + +## CORS Configuration + +### Quick Enable + +```typescript +const server = await createServer({ + port: 3000, + cors: true // Use default configuration +}) +``` + +### Custom Configuration + +```typescript +const server = await createServer({ + port: 3000, + cors: { + // Allowed origins + origin: ['http://localhost:5173', 'https://myapp.com'], + // Or use wildcard + // origin: '*', + // origin: true, // Reflect request origin + + // Allowed HTTP methods + methods: ['GET', 'POST', 'PUT', 'DELETE'], + + // Allowed request headers + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + + // Allow credentials (cookies) + credentials: true, + + // Preflight cache max age (seconds) + maxAge: 86400 + } +}) +``` + +### CorsOptions Type + +```typescript +interface CorsOptions { + /** Allowed origins: string, string array, true (reflect) or '*' */ + origin?: string | string[] | boolean + + /** Allowed HTTP methods */ + methods?: string[] + + /** Allowed request headers */ + allowedHeaders?: string[] + + /** Allow credentials */ + credentials?: boolean + + /** Preflight cache max age (seconds) */ + maxAge?: number +} +``` + +## Route Merging + +File routes and inline routes can be used together, with inline routes having higher priority: + +```typescript +const server = await createServer({ + port: 3000, + httpDir: './src/http', + httpPrefix: '/api', + + // Inline routes merge with file routes + http: { + '/health': (req, res) => res.json({ status: 'ok' }), + '/api/special': (req, res) => res.json({ special: true }) + } +}) +``` + +## Sharing Port with WebSocket + +HTTP routes automatically share the same port with WebSocket services: + +```typescript +const server = await createServer({ + port: 3000, + // WebSocket related config + apiDir: './src/api', + msgDir: './src/msg', + + // HTTP related config + httpDir: './src/http', + httpPrefix: '/api', + cors: true +}) + +await server.start() + +// Same port 3000: +// - WebSocket: ws://localhost:3000 +// - HTTP API: http://localhost:3000/api/* +``` + +## Complete Examples + +### Game Server Login API + +```typescript +// src/http/auth/login.ts +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({ + method: 'POST', + async handler(req, res) { + const { username, password } = req.body as LoginRequest + + // Validate user + const user = await db.users.findByUsername(username) + if (!user || !await verifyPassword(password, user.passwordHash)) { + res.error(401, 'Invalid username or password') + return + } + + // Generate 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) + } +}) +``` + +### Game Data Query API + +```typescript +// src/http/game/leaderboard.ts +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 } + }) + } +}) +``` + +## Best Practices + +1. **Use defineHttp** - Get better type hints and code organization +2. **Unified Error Handling** - Use `res.error()` to return consistent error format +3. **Enable CORS** - Required for frontend-backend separation +4. **Directory Organization** - Organize HTTP route files by functional modules +5. **Validate Input** - Always validate `req.body` and `req.query` content +6. **Status Code Standards** - Follow HTTP status code conventions (200, 201, 400, 401, 404, 500, etc.) diff --git a/docs/src/content/docs/en/modules/network/server.md b/docs/src/content/docs/en/modules/network/server.md index b2fdc0f5..114571fd 100644 --- a/docs/src/content/docs/en/modules/network/server.md +++ b/docs/src/content/docs/en/modules/network/server.md @@ -90,128 +90,21 @@ await server.start() Supports HTTP API sharing the same port with WebSocket, ideal for login, registration, and similar scenarios. -### File-based Routing - -Create route files in the `httpDir` directory, automatically mapped to HTTP endpoints: - -``` -src/http/ -├── login.ts → POST /api/login -├── register.ts → POST /api/register -├── health.ts → GET /api/health (set method: 'GET') -└── users/ - └── [id].ts → POST /api/users/:id (dynamic route) -``` - -### Define Routes - -Use `defineHttp` to define type-safe route handlers: - ```typescript -// src/http/login.ts -import { defineHttp } from '@esengine/server' +const server = await createServer({ + port: 3000, + httpDir: './src/http', // HTTP routes directory + httpPrefix: '/api', // Route prefix + cors: true, -interface LoginBody { - username: string - password: string -} - -export default defineHttp({ - method: 'POST', // Default POST, options: GET/PUT/DELETE/PATCH - handler(req, res) { - const { username, password } = req.body - - // Validate credentials... - if (!isValid(username, password)) { - res.error(401, 'Invalid credentials') - return - } - - // Generate token... - res.json({ token: '...', userId: '...' }) + // Or inline definition + http: { + '/health': (req, res) => res.json({ status: 'ok' }) } }) ``` -### Request Object (HttpRequest) - -```typescript -interface HttpRequest { - raw: IncomingMessage // Node.js raw request - method: string // Request method - path: string // Request path - query: Record // Query parameters - headers: Record - body: unknown // Parsed JSON body - ip: string // Client IP -} -``` - -### Response Object (HttpResponse) - -```typescript -interface HttpResponse { - raw: ServerResponse // Node.js raw response - status(code: number): HttpResponse // Set status code (chainable) - header(name: string, value: string): HttpResponse // Set header (chainable) - json(data: unknown): void // Send JSON - text(data: string): void // Send text - error(code: number, message: string): void // Send error -} -``` - -### Usage Example - -```typescript -// Complete login server example -import { createServer, defineHttp } from '@esengine/server' -import { createJwtAuthProvider, withAuth } from '@esengine/server/auth' - -const jwtProvider = createJwtAuthProvider({ - secret: process.env.JWT_SECRET!, - expiresIn: 3600 * 24, -}) - -const server = await createServer({ - port: 8080, - httpDir: 'src/http', - httpPrefix: '/api', - cors: true, -}) - -// Wrap with auth (WebSocket connections validate token) -const authServer = withAuth(server, { - provider: jwtProvider, - extractCredentials: (req) => { - const url = new URL(req.url, 'http://localhost') - return url.searchParams.get('token') - }, -}) - -await authServer.start() -// HTTP: http://localhost:8080/api/* -// WebSocket: ws://localhost:8080?token=xxx -``` - -### Inline Routes - -Routes can also be defined directly in configuration (merged with file routes, inline takes priority): - -```typescript -const server = await createServer({ - port: 8080, - http: { - '/health': { - GET: (req, res) => res.json({ status: 'ok' }), - }, - '/webhook': async (req, res) => { - // Accepts all methods - await handleWebhook(req.body) - res.json({ received: true }) - }, - }, -}) -``` +> For detailed documentation, see [HTTP Routing](/en/modules/network/http) ## Room System diff --git a/docs/src/content/docs/modules/network/http.md b/docs/src/content/docs/modules/network/http.md new file mode 100644 index 00000000..80b7824c --- /dev/null +++ b/docs/src/content/docs/modules/network/http.md @@ -0,0 +1,490 @@ +--- +title: "HTTP 路由" +description: "HTTP REST API 路由功能,支持与 WebSocket 共用端口" +--- + +`@esengine/server` 内置了轻量级的 HTTP 路由功能,可以与 WebSocket 服务共用同一端口,方便实现 REST API。 + +## 快速开始 + +### 内联路由定义 + +最简单的方式是在创建服务器时直接定义 HTTP 路由: + +```typescript +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` 目录,每个文件对应一个路由: + +```typescript +// src/http/login.ts +import { defineHttp } from '@esengine/server' + +interface LoginBody { + username: string + password: string +} + +export default defineHttp({ + 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, '用户名或密码错误') + } + } +}) +``` + +```typescript +// server.ts +import { createServer } from '@esengine/server' + +const server = await createServer({ + port: 3000, + httpDir: './src/http', // HTTP 路由目录 + httpPrefix: '/api', // 路由前缀 + cors: true +}) + +await server.start() +// 路由: POST /api/login +``` + +## defineHttp 定义 + +`defineHttp` 用于定义类型安全的 HTTP 处理器: + +```typescript +import { defineHttp } from '@esengine/server' + +interface CreateUserBody { + username: string + email: string + password: string +} + +export default defineHttp({ + // HTTP 方法(默认 POST) + method: 'POST', + + // 处理函数 + handler(req, res) { + const body = req.body as CreateUserBody + // 处理请求... + res.status(201).json({ id: 'new-user-id' }) + } +}) +``` + +### 支持的 HTTP 方法 + +```typescript +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' +``` + +## HttpRequest 对象 + +HTTP 请求对象包含以下属性: + +```typescript +interface HttpRequest { + /** 原始 Node.js IncomingMessage */ + raw: IncomingMessage + + /** HTTP 方法 */ + method: string + + /** 请求路径 */ + path: string + + /** 查询参数 */ + query: Record + + /** 请求头 */ + headers: Record + + /** 解析后的请求体 */ + body: unknown + + /** 客户端 IP */ + ip: string +} +``` + +### 使用示例 + +```typescript +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` - 解析为键值对对象 +- 其他 - 保持原始字符串 + +```typescript +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 对象 + +HTTP 响应对象提供链式 API: + +```typescript +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 +} +``` + +### 使用示例 + +```typescript +export default defineHttp({ + method: 'POST', + handler(req, res) { + // 设置状态码和自定义头 + res + .status(201) + .header('X-Custom-Header', 'value') + .json({ created: true }) + } +}) +``` + +```typescript +export default defineHttp({ + method: 'GET', + handler(req, res) { + // 发送错误响应 + res.error(404, '资源不存在') + // 等价于: res.status(404).json({ error: '资源不存在' }) + } +}) +``` + +```typescript +export default defineHttp({ + method: 'GET', + handler(req, res) { + // 发送纯文本 + res.text('Hello, World!') + } +}) +``` + +## 文件路由规范 + +### 命名转换 + +文件名会自动转换为路由路径: + +| 文件路径 | 路由路径(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` | + +### 动态路由参数 + +使用 `[param]` 语法定义动态参数: + +```typescript +// src/http/users/[id].ts +import { defineHttp } from '@esengine/server' + +export default defineHttp({ + method: 'GET', + handler(req, res) { + // 从路径获取参数 + // 注意:当前版本需要手动解析 path + const id = req.path.split('/').pop() + res.json({ userId: id }) + } +}) +``` + +### 跳过规则 + +以下文件会被自动跳过: + +- 以 `_` 开头的文件(如 `_helper.ts`) +- `index.ts` / `index.js` 文件 +- 非 `.ts` / `.js` / `.mts` / `.mjs` 文件 + +### 目录结构示例 + +``` +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/:id +``` + +## CORS 配置 + +### 快速启用 + +```typescript +const server = await createServer({ + port: 3000, + cors: true // 使用默认配置 +}) +``` + +### 自定义配置 + +```typescript +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 类型 + +```typescript +interface CorsOptions { + /** 允许的来源:字符串、字符串数组、true(反射)或 '*' */ + origin?: string | string[] | boolean + + /** 允许的 HTTP 方法 */ + methods?: string[] + + /** 允许的请求头 */ + allowedHeaders?: string[] + + /** 是否允许携带凭证 */ + credentials?: boolean + + /** 预检请求缓存时间(秒) */ + maxAge?: number +} +``` + +## 路由合并 + +文件路由和内联路由可以同时使用,内联路由优先级更高: + +```typescript +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 共用端口 + +HTTP 路由与 WebSocket 服务自动共用同一端口: + +```typescript +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 + +```typescript +// src/http/auth/login.ts +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({ + 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 + +```typescript +// src/http/game/leaderboard.ts +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 } + }) + } +}) +``` + +## 最佳实践 + +1. **使用 defineHttp** - 获得更好的类型提示和代码组织 +2. **统一错误处理** - 使用 `res.error()` 返回一致的错误格式 +3. **启用 CORS** - 前后端分离时必须配置 +4. **目录组织** - 按功能模块组织 HTTP 路由文件 +5. **验证输入** - 始终验证 `req.body` 和 `req.query` 的内容 +6. **状态码规范** - 遵循 HTTP 状态码规范(200、201、400、401、404、500 等) diff --git a/docs/src/content/docs/modules/network/server.md b/docs/src/content/docs/modules/network/server.md index eeb9aec9..f088f46e 100644 --- a/docs/src/content/docs/modules/network/server.md +++ b/docs/src/content/docs/modules/network/server.md @@ -90,128 +90,35 @@ await server.start() 支持 HTTP API 与 WebSocket 共用端口,适用于登录、注册等场景。 -### 文件路由 - -在 `httpDir` 目录下创建路由文件,自动映射为 HTTP 端点: +```typescript +const server = await createServer({ + port: 3000, + httpDir: './src/http', // HTTP 路由目录 + httpPrefix: '/api', // 路由前缀 + cors: true, + // 或内联定义 + http: { + '/health': (req, res) => res.json({ status: 'ok' }) + } +}) ``` -src/http/ -├── login.ts → POST /api/login -├── register.ts → POST /api/register -├── health.ts → GET /api/health (需设置 method: 'GET') -└── users/ - └── [id].ts → POST /api/users/:id (动态路由) -``` - -### 定义路由 - -使用 `defineHttp` 定义类型安全的路由处理器: ```typescript // src/http/login.ts import { defineHttp } from '@esengine/server' -interface LoginBody { - username: string - password: string -} - -export default defineHttp({ - method: 'POST', // 默认 POST,可选 GET/PUT/DELETE/PATCH +export default defineHttp<{ username: string; password: string }>({ + method: 'POST', handler(req, res) { const { username, password } = req.body - - // 验证凭证... - if (!isValid(username, password)) { - res.error(401, 'Invalid credentials') - return - } - - // 生成 token... - res.json({ token: '...', userId: '...' }) + // 验证并返回 token... + res.json({ token: '...' }) } }) ``` -### 请求对象 (HttpRequest) - -```typescript -interface HttpRequest { - raw: IncomingMessage // Node.js 原始请求 - method: string // 请求方法 - path: string // 请求路径 - query: Record // 查询参数 - headers: Record - body: unknown // 解析后的 JSON 请求体 - ip: string // 客户端 IP -} -``` - -### 响应对象 (HttpResponse) - -```typescript -interface HttpResponse { - raw: ServerResponse // Node.js 原始响应 - status(code: number): HttpResponse // 设置状态码(链式) - header(name: string, value: string): HttpResponse // 设置头(链式) - json(data: unknown): void // 发送 JSON - text(data: string): void // 发送文本 - error(code: number, message: string): void // 发送错误 -} -``` - -### 使用示例 - -```typescript -// 完整的登录服务器示例 -import { createServer, defineHttp } from '@esengine/server' -import { createJwtAuthProvider, withAuth } from '@esengine/server/auth' - -const jwtProvider = createJwtAuthProvider({ - secret: process.env.JWT_SECRET!, - expiresIn: 3600 * 24, -}) - -const server = await createServer({ - port: 8080, - httpDir: 'src/http', - httpPrefix: '/api', - cors: true, -}) - -// 包装认证(WebSocket 连接验证 token) -const authServer = withAuth(server, { - provider: jwtProvider, - extractCredentials: (req) => { - const url = new URL(req.url, 'http://localhost') - return url.searchParams.get('token') - }, -}) - -await authServer.start() -// HTTP: http://localhost:8080/api/* -// WebSocket: ws://localhost:8080?token=xxx -``` - -### 内联路由 - -也可以直接在配置中定义路由(与文件路由合并,内联优先): - -```typescript -const server = await createServer({ - port: 8080, - http: { - '/health': { - GET: (req, res) => res.json({ status: 'ok' }), - }, - '/webhook': async (req, res) => { - // 接受所有方法 - await handleWebhook(req.body) - res.json({ received: true }) - }, - }, -}) -``` +> 详细文档请参考 [HTTP 路由](/modules/network/http) ## Room 系统