const express = require('express'); const path = require('path'); const { spawn } = require('child_process'); const cors = require('cors'); const app = express(); const PORT = process.env.NODE_ENV === 'development' ? 8080 : 3000; const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots'); const CLIENT_DIST_DIR = process.env.NODE_ENV === 'development' ? path.join(__dirname, '../client/dist') : path.join(__dirname, './public'); function parseK6StdoutTable(text) { const keysToKeep = new Set([ 'checks_total', 'checks_succeeded', 'checks_failed', 'browser_data_received', 'browser_data_sent', 'browser_http_req_duration', 'browser_http_req_failed', 'browser_web_vital_cls', 'browser_web_vital_fcp', 'browser_web_vital_ttfb', 'checks', 'data_received', 'data_sent', 'iteration_duration', 'iterations', 'vus', 'vus_max' ]); const lines = text.split('\n'); const result = {}; for (let line of lines) { line = line.replace(/^[✓×\s]+/, ''); // 去掉开头 ✓ × 空格 if (!line.includes(':')) continue; let [keyPart, valPart] = line.split(':'); if (!valPart) continue; const key = keyPart.trim().replace(/\.+$/, ''); if (!keysToKeep.has(key)) continue; valPart = valPart.trim(); if (/(\w+\()?=/.test(valPart)) { const stats = {}; const regex = /([^\s=]+)=([^\s]+)/g; let match; while ((match = regex.exec(valPart)) !== null) { stats[match[1]] = match[2]; } result[key] = stats; } else { const parts = valPart.split(/\s+/); if (parts.length === 4 && parts[2].endsWith('/s')) { result[key] = { value: parts[0] + ' ' + parts[1], rate: parts[2] + ' ' + parts[3], }; } else if (parts.length === 2 && parts[1].endsWith('/s')) { result[key] = { value: parts[0], rate: parts[1], }; } else { result[key] = valPart; } } } return result; } // ====== 中间件 ====== app.use(cors()); // 静态资源 app.use('/static', express.static(SCREENSHOTS_DIR)); app.use(express.static(CLIENT_DIST_DIR)); // ====== API 路由 ====== app.get('/api/k6', async (req, res) => { const targetUrl = req.query.targetUrl; const timeStamp = req.query.timeStamp; console.log("targetUrl:", targetUrl); console.log("timeStamp:", timeStamp); if (!targetUrl || !timeStamp) { return res.status(400).json({ error: '缺少 targetUrl 或 timeStamp 参数' }); } // 运行 k6 脚本 const k6Process = spawn('k6', ['run', '--no-color=false', '--env', 'targetUrl=' + targetUrl, '--env', 'timeStamp=' + timeStamp, './script-browser.js']); let outputBuffer = ''; k6Process.stdout.on('data', (chunk) => { outputBuffer += chunk.toString(); console.log(chunk.toString()) }); k6Process.on('close', () => { const parsed = parseK6StdoutTable(outputBuffer); res.json(parsed); // 返回结构化结果给前端 }); }); // ====== history 路由兼容(放在所有 API 路由之后) ====== app.get('*', (req, res) => { res.sendFile(path.join(CLIENT_DIST_DIR, 'index.html')); }); // ====== 启动服务 ====== app.listen(PORT, () => { console.log(`Express server running at http://localhost:${PORT}`); });