123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- 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 : 9001;
- 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}`);
- });
|