/** * OPC 培训站 · 独立静态托管服务 * ------------------------------------------------------------- * 用途:把构建产物 dist/ 静态托管起来,一条命令跑起来。 * 启动:npm run build && node server/start.mjs * 默认端口 8091。 * 说明:本项目不再内置任何模拟 API;全部接口由真实后端提供 * (构建时通过 VITE_API_BASE 注入,默认 https://opc.pinesound.cn,本地联调指向 server-core)。 */ import http from 'node:http'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; const PORT = process.env.PORT || 8091; const DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../dist'); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.webp': 'image/webp', '.json': 'application/json; charset=utf-8', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ico': 'image/x-icon' }; function serveStatic(req, res) { let p = (req.url || '/').split('?')[0]; if (p === '/' || p === '') p = '/index.html'; // 防路径穿越 const file = path.normalize(path.join(DIST, p)); if (!file.startsWith(DIST)) return notFound(res); if (!existsSync(file) || statSync(file).isDirectory()) { // SPA 回退:非文件请求一律给 index.html(仅当请求的不是带扩展名的资源) if (!path.extname(file)) return sendFile(res, path.join(DIST, 'index.html')); return notFound(res); } sendFile(res, file); } function sendFile(res, file) { const ext = path.extname(file).toLowerCase(); res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream'); res.setHeader('Cache-Control', ext === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable'); createReadStream(file).pipe(res); } function notFound(res) { res.statusCode = 404; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.end('Not Found'); } const server = http.createServer((req, res) => { serveStatic(req, res); }); server.listen(PORT, () => { console.log(`▶ OPC 培训站已启动: http://localhost:${PORT}`); console.log(` 静态资源: ${DIST}`); console.log(` API 由真实后端提供(构建时 VITE_API_BASE 注入)`); });