1. 从“等不到”到“造一个”:为什么我们需要一个高质量的SSE接口Mock工具
在前后端分离、微服务架构大行其道的今天,前端开发最头疼的事情之一,莫过于“等接口”。尤其是当你需要对接一个服务器发送事件(Server-Sent Events,简称SSE)接口时,这种等待的痛苦会指数级放大。SSE是一种允许服务器主动向客户端推送数据的HTML5技术,常用于实时数据展示、消息通知、日志流等场景。想象一下,你正在开发一个实时监控仪表盘,后端同事告诉你:“这个接口还在开发,你先用个假数据模拟一下吧。”你点点头,然后打开浏览器,开始写一个简单的EventSource连接,却发现事情没那么简单。
一个真正的SSE接口,不仅仅是返回一段静态的JSON。它需要维持一个长连接,按照特定的text/event-stream格式,持续地、有节奏地发送数据块。数据块之间用两个换行符分隔,每个数据块可以包含event、data、id等字段。你想模拟一个每3秒推送一次CPU使用率、并且偶尔(比如10%概率)推送一条告警消息的接口。用setInterval写个简单的Node.js服务器?可以,但很快你会发现更多需求:如何模拟连接建立时的延迟?如何模拟服务器主动断开连接?如何模拟网络抖动导致的数据流中断?如何方便地测试前端在收到不同event类型(如message、alert、error)时的处理逻辑?更别提,你可能还需要在团队内部共享这个Mock接口,或者集成到自动化测试流水线中。
这就是sse-stuntman诞生的背景。它不是一个简单的“Hello World”式Mock服务器,而是一个旨在提供高质量、全方位模拟能力的SSE接口工具。“Stuntman”意为“特技演员”,在电影里,特技演员能精准、可控地完成各种高难度动作,替代主角完成危险镜头。sse-stuntman的角色也是如此——它精准、可控地模拟真实SSE服务器的各种行为,让前端、测试、甚至后端在联调前,都能在一个高度仿真的环境中工作。它解决了开发者在等待真实接口时,无法进行深度集成和异常测试的痛点。无论你是独立开发者,还是团队中的一员,如果你正在和SSE接口打交道,那么理解并掌握这样一个工具,将极大提升你的开发效率和代码质量。
2. 拆解“高质量全方位”:sse-stuntman的核心能力画像
那么,一个被冠以“高质量全方位”的SSE Mock工具,究竟应该具备哪些能力?我们可以从一次完整的SSE连接生命周期来拆解,这远比一个能返回数据流的简单服务器要复杂得多。
2.1 连接建立阶段:可控的握手与初始化
真实的网络环境充满不确定性。sse-stuntman首先应该能模拟连接建立的各个细节。这包括:
- 可配置的响应延迟:模拟服务器处理请求或网络延迟。你可以设置一个固定的延迟(如500ms),或者一个随机范围(如200-1000ms),甚至模拟一个超时(不响应),来测试前端的超时处理机制。
- 灵活的HTTP状态码:并非所有连接都能成功建立。工具应允许你模拟返回
200 OK、404 Not Found、500 Internal Server Error等不同状态码,验证前端EventSource的onerror回调是否能正确区分网络错误和服务器错误。 - 自定义响应头:除了必备的
Content-Type: text/event-stream和Cache-Control: no-cache,工具应允许注入其他自定义头部,例如Access-Control-Allow-Origin(用于CORS测试)或一些业务相关的头部信息。
2.2 数据推送阶段:动态、多样与可编程的事件流
这是SSE的核心,也是Mock工具的精华所在。高质量意味着数据流不是死板的。
- 动态数据生成:推送的数据内容不应是硬编码的字符串。它应该支持模板变量(如时间戳
{timestamp}、递增ID{index})、引用外部数据文件(JSON、CSV)、甚至执行一小段JavaScript代码来生成数据。例如,模拟股票价格时,数据可以是{ “symbol”: “AAPL”, “price”: 150 + Math.sin(index)*5 }。 - 多事件类型(Event Types):一个SSE连接可以发送多种类型的事件。
sse-stuntman必须能轻松配置和切换不同event字段的事件流。比如,你可以定义一个持续发送event: metrics的事件流来推送性能指标,同时定义一个按条件触发的event: alert事件流来推送告警。 - 可编程的推送逻辑:推送不应只是简单的定时器。它应该支持复杂的逻辑:
- 定时推送:固定频率(如每秒一次)或随机间隔(模拟不规则的数据到达)。
- 条件触发:当某个数据值达到阈值,或推送次数达到某个数量时,触发一个特殊事件。
- 序列与循环:定义一系列数据按顺序发送,发送完毕后可以停止或循环。
- 外部触发:通过一个额外的HTTP API端点(如
POST /trigger)来手动触发一次数据推送,这对于调试和演示非常有用。
2.3 连接生命周期与异常模拟:真实世界的复杂性
这是区分普通工具和“特技演员”的关键。真实的服务可能会中断,网络可能会不稳定。
- 主动断开连接:模拟服务器端主动关闭连接。可以配置在推送N条消息后断开,或者在特定事件发生后断开,并可以控制断开时是否发送一个正式的SSE格式的结束消息。
- 模拟网络中断与重连:
EventSource有自动重连机制。sse-stuntman可以模拟短暂的连接中断(比如断开3秒后自动恢复),测试客户端的重连逻辑和last-event-id头部(用于断点续传)是否正常工作。 - 数据格式异常:故意发送不符合SSE格式的数据(比如缺少换行符、错误的字段名),测试前端代码的健壮性,是否会被异常数据卡死或崩溃。
2.4 可观测性与集成性:让Mock成为工程的一部分
一个好的工具不能是黑盒。
- 完整的日志输出:在控制台或日志文件中清晰记录每一个连接的建立、每一条消息的发送、每一次连接的断开。这对于调试复杂的数据流逻辑至关重要。
- 管理API与UI:提供一个简单的Web管理界面或一套RESTful API,用于在运行时动态查看当前活跃连接、修改推送配置、手动触发事件等。这提升了工具的易用性和可控性。
- 易于集成:能够以命令行工具、Node.js库、Docker容器等多种形式运行,方便集成到本地开发环境、CI/CD流水线或测试套件中。
sse-stuntman的设计目标,就是尽可能覆盖上述所有维度,提供一个功能完备、配置灵活、行为可控的SSE接口模拟环境。它让开发者从“被动等待”变为“主动创造”测试条件。
3. 实战构建:手把手打造你自己的sse-stuntman核心引擎
理解了“是什么”和“为什么”,我们接下来进入“怎么做”。我们将使用Node.js来构建一个具备上述核心能力的sse-stuntman原型。选择Node.js是因为其异步非阻塞特性非常适合处理大量并发长连接,而且生态丰富。我们将从零开始,一步步实现。
3.1 项目初始化与基础服务器搭建
首先,创建一个新目录并初始化项目。
mkdir sse-stuntman-core && cd sse-stuntman-core npm init -y我们主要需要express作为Web框架,cors处理跨域,body-parser解析请求体。同时,我们将使用uuid为每个连接生成唯一ID。
npm install express cors body-parser uuid现在,创建主文件server.js,搭建一个最基础的Express服务器,并创建我们的核心SSE路由。
const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const { v4: uuidv4 } = require('uuid'); const app = express(); const PORT = process.env.PORT || 3001; // 中间件 app.use(cors()); // 允许跨域,方便前端测试 app.use(bodyParser.json()); // 存储所有活跃的SSE客户端连接 const clients = new Map(); // key: clientId, value: res (响应对象) // 基础健康检查 app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'sse-stuntman' }); }); // 核心SSE端点 - 连接建立 app.get('/sse', (req, res) => { // 1. 设置SSE必需的响应头 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', 'Connection': 'keep-alive', // 允许跨域 'Access-Control-Allow-Origin': '*', }); // 2. 发送初始连接确认消息(可选,但很有用) res.write(`event: connected\ndata: ${JSON.stringify({ timestamp: Date.now(), message: 'SSE连接已建立' })}\n\n`); // 3. 生成唯一客户端ID并存储连接 const clientId = uuidv4(); clients.set(clientId, res); console.log(`[${new Date().toISOString()}] 客户端连接建立: ${clientId}`); // 4. 当客户端关闭连接时,清理资源 req.on('close', () => { console.log(`[${new Date().toISOString()}] 客户端连接关闭: ${clientId}`); clients.delete(clientId); res.end(); }); }); // 启动服务器 app.listen(PORT, () => { console.log(`sse-stuntman 模拟服务器运行在 http://localhost:${PORT}`); });现在运行node server.js,访问http://localhost:3001/sse,你会看到一个保持打开状态的连接,并收到一条connected事件。基础框架已经搭好。
3.2 实现可配置、可编程的事件推送器
静态消息没意思,我们需要一个强大的推送引擎。我们在项目根目录创建一个lib/EventScheduler.js模块。这个模块负责管理不同的事件流定义和调度逻辑。
// lib/EventScheduler.js class EventScheduler { constructor() { this.eventConfigs = new Map(); // 存储事件流配置 this.intervals = new Map(); // 存储定时器引用 } // 定义一个事件流 defineEventStream(eventName, config) { // config 结构示例: // { // interval: 1000, // 推送间隔(ms),可以是函数 // dataGenerator: (index) => ({ id: index, value: Math.random() }), // 数据生成函数 // eventType: 'data', // 默认是事件名,可覆盖 // maxCount: 50, // 最大推送次数,无限则为null // } this.eventConfigs.set(eventName, config); console.log(`事件流已定义: ${eventName}`); } // 开始向所有客户端推送一个特定事件流 startEventStream(eventName, clientsMap) { const config = this.eventConfigs.get(eventName); if (!config) { throw new Error(`未找到事件流配置: ${eventName}`); } if (this.intervals.has(eventName)) { console.warn(`事件流 ${eventName} 已在运行中`); return; } let count = 0; const intervalFn = () => { if (config.maxCount !== null && count >= config.maxCount) { this.stopEventStream(eventName); return; } const data = config.dataGenerator(count); const eventType = config.eventType || eventName; const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; // 向所有活跃客户端发送 clientsMap.forEach((clientRes, clientId) => { if (!clientRes.writableEnded) { clientRes.write(message); } }); console.log(`[推送] ${eventName} -> ${JSON.stringify(data)}`); count++; // 动态计算下一次间隔(如果interval是函数) const nextInterval = typeof config.interval === 'function' ? config.interval(count) : config.interval; if (this.intervals.has(eventName)) { clearTimeout(this.intervals.get(eventName)); const timer = setTimeout(intervalFn, nextInterval); this.intervals.set(eventName, timer); } }; // 首次执行 const initialInterval = typeof config.interval === 'function' ? config.interval(0) : config.interval; const timer = setTimeout(intervalFn, initialInterval); this.intervals.set(eventName, timer); console.log(`事件流开始: ${eventName}`); } // 停止事件流 stopEventStream(eventName) { if (this.intervals.has(eventName)) { clearTimeout(this.intervals.get(eventName)); this.intervals.delete(eventName); console.log(`事件流停止: ${eventName}`); } } // 手动触发一次推送(不依赖定时器) triggerEvent(eventName, clientsMap, customData = null) { const config = this.eventConfigs.get(eventName); if (!config) { throw new Error(`未找到事件流配置: ${eventName}`); } const data = customData || config.dataGenerator(-1); // -1 表示手动触发 const eventType = config.eventType || eventName; const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; clientsMap.forEach((clientRes) => { if (!clientRes.writableEnded) { clientRes.write(message); } }); console.log(`[手动触发] ${eventName} -> ${JSON.stringify(data)}`); } } module.exports = EventScheduler;这个EventScheduler类是我们的“导演”,它允许我们定义多种剧本(事件流),然后按剧本向所有“观众”(客户端)推送消息。它支持固定间隔、动态间隔、最大推送次数等特性。
3.3 集成调度器并丰富服务器功能
现在,让我们修改server.js,集成调度器,并增加管理API。
// server.js (续接之前代码) const EventScheduler = require('./lib/EventScheduler'); const scheduler = new EventScheduler(); // --- 定义一些示例事件流 --- // 示例1:稳定的指标流,每秒一次 scheduler.defineEventStream('metrics', { interval: 1000, dataGenerator: (index) => ({ timestamp: Date.now(), cpu: (20 + Math.random() * 60).toFixed(2), // 模拟CPU使用率 20%~80% memory: (40 + Math.random() * 40).toFixed(2), // 模拟内存使用率 40%~80% connections: clients.size, index: index }), eventType: 'metrics' }); // 示例2:随机告警流,间隔2-10秒随机 scheduler.defineEventStream('alert', { interval: () => 2000 + Math.random() * 8000, // 2-10秒随机 dataGenerator: () => { const alerts = ['CPU负载过高', '内存泄漏警告', '网络延迟突增', '服务响应超时']; return { timestamp: Date.now(), level: ['info', 'warning', 'error'][Math.floor(Math.random() * 3)], message: alerts[Math.floor(Math.random() * alerts.length)], id: `alert-${Date.now()}` }; }, eventType: 'alert', maxCount: null // 无限 }); // --- 管理API --- // 获取所有已定义的事件流状态 app.get('/admin/streams', (req, res) => { const streams = []; // 这里需要扩展EventScheduler以暴露内部状态,为简洁起见,我们返回一个简化版 for (let [name] of scheduler.eventConfigs) { streams.push({ name, active: scheduler.intervals.has(name) }); } res.json({ streams, activeConnections: clients.size }); }); // 启动一个事件流 app.post('/admin/streams/:name/start', (req, res) => { try { scheduler.startEventStream(req.params.name, clients); res.json({ success: true, message: `事件流 ${req.params.name} 已启动` }); } catch (error) { res.status(400).json({ success: false, error: error.message }); } }); // 停止一个事件流 app.post('/admin/streams/:name/stop', (req, res) => { scheduler.stopEventStream(req.params.name); res.json({ success: true, message: `事件流 ${req.params.name} 已停止` }); }); // 手动触发一次事件 app.post('/admin/streams/:name/trigger', (req, res) => { try { const customData = req.body; // 允许从请求体传递自定义数据 scheduler.triggerEvent(req.params.name, clients, customData); res.json({ success: true, message: `事件流 ${req.params.name} 已手动触发` }); } catch (error) { res.status(400).json({ success: false, error: error.message }); } }); // 主动断开所有客户端连接(模拟服务器重启) app.post('/admin/connections/disconnect', (req, res) => { const { code = 1000, reason = 'Server initiated disconnect' } = req.body; clients.forEach((clientRes, clientId) => { // 注意:SSE协议没有标准的关闭帧,我们发送一个特殊事件然后结束响应 clientRes.write(`event: shutdown\ndata: ${JSON.stringify({ code, reason })}\n\n`); clientRes.end(); }); const count = clients.size; clients.clear(); console.log(`[管理员操作] 已主动断开所有 (${count}) 个客户端连接`); res.json({ success: true, disconnected: count }); });重启服务器。现在,你的sse-stuntman已经具备了:
- 一个稳定的SSE端点 (
/sse)。 - 两个预定义的事件流:
metrics(每秒)和alert(随机)。 - 一套管理API,可以动态启停事件流、手动触发事件、甚至断开所有连接。
你可以用Postman或curl测试管理API:
# 启动metrics流 curl -X POST http://localhost:3001/admin/streams/metrics/start # 手动触发一次alert curl -X POST http://localhost:3001/admin/streams/alert/trigger -H "Content-Type: application/json" -d '{"message":"手动紧急告警!"}' # 断开所有连接 curl -X POST http://localhost:3001/admin/connections/disconnect4. 前端对接与深度测试:从连接到异常处理
有了功能强大的Mock服务器,前端开发与测试就可以全面展开了。我们创建一个简单的test-client.html来进行全方位测试。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>SSE Stuntman 测试客户端</title> <style> body { font-family: sans-serif; margin: 20px; } .log { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; margin-bottom: 20px; } .event { margin: 5px 0; padding: 5px; border-left: 3px solid #4CAF50; background: #f9f9f9; } .event.alert { border-color: #ff9800; } .event.error { border-color: #f44336; } .event.shutdown { border-color: #9e9e9e; } button { margin: 5px; padding: 8px 15px; } .status { padding: 10px; margin: 10px 0; } .connected { background-color: #d4edda; } .disconnected { background-color: #f8d7da; } </style> </head> <body> <h1>SSE Stuntman 测试客户端</h1> <div id="status" class="status disconnected">状态: 未连接</div> <button onclick="connectSSE()">连接SSE</button> <button onclick="disconnectSSE()" disabled id="btnDisconnect">断开连接</button> <button onclick="clearLog()">清空日志</button> <hr> <h3>管理操作</h3> <button onclick="controlStream('metrics', 'start')">启动 Metrics 流</button> <button onclick="controlStream('metrics', 'stop')">停止 Metrics 流</button> <button onclick="triggerAlert()">手动触发告警</button> <button onclick="serverDisconnect()">模拟服务器断开</button> <hr> <h3>事件日志</h3> <div id="log" class="log"></div> <script> let eventSource = null; const logElement = document.getElementById('log'); const statusElement = document.getElementById('status'); const disconnectBtn = document.getElementById('btnDisconnect'); function logEvent(type, data, eventName = 'message') { const div = document.createElement('div'); div.className = `event ${eventName}`; div.innerHTML = `<strong>[${new Date().toLocaleTimeString()}] ${eventName.toUpperCase()}:</strong> ${JSON.stringify(data)}`; logElement.appendChild(div); logElement.scrollTop = logElement.scrollHeight; } function updateStatus(connected) { statusElement.textContent = `状态: ${connected ? '已连接' : '未连接'}`; statusElement.className = `status ${connected ? 'connected' : 'disconnected'}`; disconnectBtn.disabled = !connected; } function connectSSE() { if (eventSource) { console.warn('SSE连接已存在'); return; } // 使用EventSource API连接 eventSource = new EventSource('http://localhost:3001/sse'); eventSource.onopen = (e) => { console.log('SSE连接已打开'); updateStatus(true); }; // 监听通用消息(未指定event类型,或event: message) eventSource.onmessage = (e) => { logEvent('info', JSON.parse(e.data), 'message'); }; // 监听特定事件类型 eventSource.addEventListener('connected', (e) => { logEvent('info', JSON.parse(e.data), 'connected'); }); eventSource.addEventListener('metrics', (e) => { const data = JSON.parse(e.data); logEvent('info', data, 'metrics'); // 可以在这里更新UI图表 }); eventSource.addEventListener('alert', (e) => { const data = JSON.parse(e.data); logEvent('warn', data, 'alert'); // 可以在这里弹出告警通知 }); eventSource.addEventListener('shutdown', (e) => { const data = JSON.parse(e.data); logEvent('error', data, 'shutdown'); alert(`服务器主动断开连接: ${data.reason}`); disconnectSSE(); // 清理本地连接状态 }); eventSource.onerror = (e) => { console.error('SSE连接错误:', e); logEvent('error', { message: '连接发生错误,可能已断开' }, 'error'); updateStatus(false); // EventSource在错误时会自动尝试重连,这里我们手动清理并提示 if (eventSource.readyState === EventSource.CLOSED) { eventSource = null; } }; } function disconnectSSE() { if (eventSource) { eventSource.close(); eventSource = null; updateStatus(false); logEvent('info', { message: '客户端主动断开连接' }, 'info'); } } function clearLog() { logElement.innerHTML = ''; } // 调用管理API async function controlStream(streamName, action) { try { const resp = await fetch(`http://localhost:3001/admin/streams/${streamName}/${action}`, { method: 'POST' }); const result = await resp.json(); logEvent('info', result, 'admin'); } catch (error) { logEvent('error', { error: error.message }, 'admin'); } } async function triggerAlert() { try { const resp = await fetch(`http://localhost:3001/admin/streams/alert/trigger`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: '前端手动触发的紧急告警!', level: 'critical' }) }); const result = await resp.json(); logEvent('info', result, 'admin'); } catch (error) { logEvent('error', { error: error.message }, 'admin'); } } async function serverDisconnect() { if (!confirm('确定要模拟服务器主动断开所有连接吗?')) return; try { const resp = await fetch(`http://localhost:3001/admin/connections/disconnect`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: '模拟服务器维护' }) }); const result = await resp.json(); logEvent('info', result, 'admin'); } catch (error) { logEvent('error', { error: error.message }, 'admin'); } } </script> </body> </html>这个测试客户端页面功能齐全:
- 连接/断开:基础功能。
- 事件分类显示:将
metrics、alert、shutdown等不同事件用不同颜色区分,直观明了。 - 集成管理操作:直接在页面上调用我们之前写的管理API,控制Mock服务器的行为。
- 完整的错误与状态处理:展示了如何处理
onerror事件和服务器主动发送的shutdown事件。
打开这个HTML文件,点击“连接SSE”,然后通过页面上的按钮操作,你就能完整地体验到sse-stuntman的所有核心功能。你可以观察到稳定的指标流、随机出现的告警,并测试服务器主动断开时前端的反应。
5. 进阶配置与生产级考量
我们构建的原型已经具备了核心功能,但要称之为“高质量全方位”,还需要考虑更多生产级别的特性和优化。
5.1 配置外部化与场景预设
硬编码的事件流配置不利于维护和分享。我们应该支持从配置文件(如YAML或JSON)加载场景。
# scenarios/dashboard.yaml name: "实时监控仪表盘" description: "模拟一个包含CPU、内存指标和随机告警的监控场景" streams: - name: "systemMetrics" eventType: "metrics" interval: 1000 dataGenerator: | (function(index) { return { ts: Date.now(), cpu: (15 + Math.random() * 70).toFixed(1), mem: (30 + Math.random() * 50).toFixed(1), disk: (40 + Math.random() * 40).toFixed(1), reqCount: Math.floor(Math.random() * 1000) }; }) maxCount: null - name: "applicationAlerts" eventType: "alert" interval: "random(5000, 30000)" # 支持解析随机表达式 dataGenerator: "file://./data/alerts.json" # 支持从文件读取数据池 maxCount: 50服务器启动时,可以加载指定场景文件,自动创建并启动所有定义的事件流。这极大提升了工具的复用性和场景还原能力。
5.2 连接级控制与数据隔离
目前的实现是广播模式,所有客户端收到相同的数据。但在某些测试场景,我们需要针对特定连接进行控制。
- 连接标识与定向推送:在连接建立时,可以通过查询参数传递客户端ID或会话ID(如
/sse?clientId=frontend_1)。服务器端记录这个ID,并提供类似POST /admin/clients/:clientId/push的API,实现向特定客户端推送消息。这对于测试多用户、多租户场景非常有用。 - 数据隔离与模拟:可以为每个连接维护独立的状态和上下文。例如,模拟一个聊天室,每个连接代表一个用户,其收到的消息列表应该是私有的。这需要更复杂的状态管理,但
sse-stuntman可以作为基础框架进行扩展。
5.3 性能、稳定性与可观测性增强
- 连接数限制与心跳:为防止资源耗尽,可以设置最大连接数。同时,对于长时间空闲的连接,服务器应定期发送注释行(
:开头的行,SSE规范中的注释,用于保持连接活跃),防止代理或负载均衡器超时断开。 - 更精细的日志:区分不同级别的日志(DEBUG, INFO, WARN, ERROR),并可以输出到文件或日志系统。记录每个连接的生命周期、流量统计(发送消息数、字节数)等。
- 压力测试与基准:使用
autocannon或wrk等工具,对Mock服务器进行压力测试,评估其能承载的并发连接数和消息吞吐量,为生产环境提供参考。
5.4 容器化与集成部署
为了让团队其他成员或CI/CD环境能轻松使用,Docker化是最佳选择。
# Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3001 USER node CMD ["node", "server.js"]可以构建镜像并推送到私有仓库。在团队内部,可以通过docker-compose.yml一键启动包含sse-stuntman和其他依赖服务(如前端应用、测试套件)的完整环境。
6. 避坑指南与最佳实践
在实际使用和扩展sse-stuntman的过程中,我总结了一些容易踩坑的地方和对应的建议。
连接管理与内存泄漏:这是Node.js SSE服务器最常见的坑。务必确保在客户端断开连接(req.on('close'))时,从clientsMap中移除对应的响应对象,并清除可能存在的与该连接相关的定时器或监听器。否则,服务器内存会持续增长。在我们的实现中,EventScheduler的定时器是全局的,不依赖于单个连接,所以只需清理clientsMap即可。但如果你的数据生成器依赖连接上下文,就需要更精细的清理。
SSE协议格式的严格性:SSE要求每条消息以两个换行符(\n\n)结束。务必确保你构建的消息字符串末尾是\n\n,而不是\r\n\r\n或只有一个\n。许多前端库对格式要求严格,格式错误会导致EventSource无法正确解析。一个可靠的构建函数是:
function formatSSEMessage(event, data, id) { let msg = ''; if (event) msg += `event: ${event}\n`; if (id) msg += `id: ${id}\n`; msg += `data: ${JSON.stringify(data)}\n\n`; return msg; }CORS与认证:如果你的前端应用与Mock服务器不在同一个域,浏览器会强制执行CORS策略。我们使用了cors()中间件允许所有来源,这在开发时很方便,但在生产或更严格的环境中,你应该配置具体的来源。另外,如果真实接口需要认证(如JWT Token),你的Mock服务器也应该模拟这一过程,例如要求客户端在连接URL或请求头中携带Token,并在服务端进行验证(或至少解析)。
前端重连与状态恢复:EventSource在连接断开后会默认自动重连。Mock服务器应支持Last-Event-ID头部。当客户端重连时,会发送这个头部,其中包含最后收到的事件ID。服务器可以根据这个ID决定从何处继续发送数据。在我们的EventScheduler中,可以为dataGenerator函数增加一个lastId参数来实现断点续传的逻辑。
模拟网络异常的真实性:简单地断开连接可能不够。更真实的模拟可以包括:在发送某条消息时故意延迟很久(模拟网络拥塞),或者发送一段乱码后再恢复正轨(模拟数据包损坏)。这些边缘情况的模拟,能帮你发现前端代码中隐藏的健壮性问题。
sse-stuntman这样的工具,其价值不仅在于“有”,更在于“精”。通过不断打磨其模拟的真实性、控制的精细度和使用的便捷性,它能从一个简单的Mock工具,进化成为前端开发、测试、甚至架构验证阶段不可或缺的“特技演员”。当你下次再需要等待一个SSE接口时,不妨花点时间,用它来搭建一个高度仿真的沙盒,你会发现,等待的时间变成了高质量开发和测试的时间。