/** * 客户端静态资源托管(任务 A1.5)。 * * 构建后的客户端(client/app/dist)由平台服务托管,这样一个端口就能跑起完整平台—— * 桌面端与浏览器都指向同一个服务地址。未构建时返回 false,由调用方按 404 处理。 */ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import type { ServerResponse } from 'node:http'; import { extname, join, normalize, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const MIME_TYPES: Record = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2', }; /** 默认目录:仓库内 client/app/dist;可用 APS_CLIENT_DIST 覆盖 */ export function clientDistDir(): string { const override = process.env.APS_CLIENT_DIST; if (override && override.trim() !== '') return resolve(override); // server/dist/entry/static.js → 仓库根 return fileURLToPath(new URL('../../../client/app/dist', import.meta.url)); } export function createStaticHandler(root = clientDistDir()): (urlPath: string, res: ServerResponse) => Promise { const rootDir = resolve(root); const indexFile = join(rootDir, 'index.html'); return async function serveStatic(urlPath: string, res: ServerResponse): Promise { if (!existsSync(indexFile)) return false; const relativePath = normalize(decodeURIComponent(urlPath)).replace(/^[/\\]+/, ''); const candidate = resolve(rootDir, relativePath); const insideRoot = candidate === rootDir || candidate.startsWith(rootDir + sep); if (!insideRoot) return false; // 目录穿越 const file = relativePath === '' || !existsSync(candidate) || !extname(candidate) ? indexFile : candidate; const body = await readFile(file); res.writeHead(200, { 'content-type': MIME_TYPES[extname(file)] ?? 'application/octet-stream', 'cache-control': file === indexFile ? 'no-store' : 'public, max-age=3600', }); res.end(body); return true; }; }