215 lines
5.9 KiB
HTML
215 lines
5.9 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>SSE 测试</title>
|
||
<style>
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
max-width: 800px;
|
||
margin: 50px auto;
|
||
padding: 20px;
|
||
}
|
||
.status {
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 4px;
|
||
}
|
||
.status.connected { background: #d4edda; color: #155724; }
|
||
.status.connecting { background: #fff3cd; color: #856404; }
|
||
.status.disconnected { background: #f8d7da; color: #721c24; }
|
||
.events {
|
||
border: 1px solid #ddd;
|
||
padding: 10px;
|
||
height: 400px;
|
||
overflow-y: auto;
|
||
background: #f9f9f9;
|
||
}
|
||
.event-item {
|
||
padding: 8px;
|
||
margin: 5px 0;
|
||
background: #fff;
|
||
border: 1px solid #eee;
|
||
border-radius: 4px;
|
||
}
|
||
.event-item .time {
|
||
color: #888;
|
||
font-size: 12px;
|
||
}
|
||
.event-item .data {
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>SSE / HTTP Stream 测试</h1>
|
||
<div>
|
||
<label>URL: </label>
|
||
<input type="text" id="url" value="http://localhost:4005/root/v3/" style="width: 400px;">
|
||
<label>类型: </label>
|
||
<select id="streamType">
|
||
<option value="sse">SSE (EventSource)</option>
|
||
<option value="stream">HTTP Stream (Fetch)</option>
|
||
</select>
|
||
<button onclick="connect()">连接</button>
|
||
<button onclick="disconnect()">断开</button>
|
||
</div>
|
||
<div id="status" class="status disconnected">未连接</div>
|
||
<h3>事件日志</h3>
|
||
<div id="events" class="events"></div>
|
||
|
||
<script>
|
||
let eventSource = null;
|
||
let abortController = null;
|
||
|
||
async function connect() {
|
||
const url = document.getElementById('url').value;
|
||
const type = document.getElementById('streamType').value;
|
||
|
||
if (!url) {
|
||
alert('请输入 URL');
|
||
return;
|
||
}
|
||
|
||
disconnect();
|
||
const eventsDiv = document.getElementById('events');
|
||
eventsDiv.innerHTML = '';
|
||
updateStatus('connecting', '正在连接...');
|
||
|
||
if (type === 'sse') {
|
||
connectSSE(url);
|
||
} else {
|
||
connectStream(url);
|
||
}
|
||
}
|
||
|
||
function connectSSE(url) {
|
||
try {
|
||
eventSource = new EventSource(url);
|
||
|
||
eventSource.onopen = function() {
|
||
updateStatus('connected', '已连接 (SSE)');
|
||
addEvent('系统', 'SSE 连接已建立');
|
||
};
|
||
|
||
eventSource.onmessage = function(event) {
|
||
addEvent('消息', event.data, 'message');
|
||
};
|
||
|
||
eventSource.onerror = function(error) {
|
||
// 如果 readyState 是 CLOSED,说明是后端主动关闭,不重连
|
||
if (eventSource.readyState === EventSource.CLOSED) {
|
||
updateStatus('disconnected', '连接已关闭');
|
||
addEvent('系统', '后端已关闭连接,无须重连');
|
||
return;
|
||
}
|
||
|
||
// 如果是其他错误状态,显示错误信息,不自动重连
|
||
updateStatus('disconnected', '连接错误');
|
||
addEvent('错误', 'SSE 连接发生错误');
|
||
};
|
||
|
||
eventSource.addEventListener('data', function(event) {
|
||
addEvent('自定义事件', event.data, 'data');
|
||
});
|
||
|
||
} catch (e) {
|
||
updateStatus('disconnected', '连接失败: ' + e.message);
|
||
addEvent('错误', e.message);
|
||
}
|
||
}
|
||
|
||
async function connectStream(url) {
|
||
try {
|
||
abortController = new AbortController();
|
||
updateStatus('connected', '已连接 (Stream)');
|
||
addEvent('系统', 'HTTP Stream 连接已建立');
|
||
|
||
const response = await fetch(url, {
|
||
signal: abortController.signal,
|
||
headers: {
|
||
'Accept': 'text/plain, text/event-stream'
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = '';
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) {
|
||
addEvent('系统', 'Stream 连接已关闭');
|
||
break;
|
||
}
|
||
|
||
// 解码并处理数据
|
||
buffer += decoder.decode(value, { stream: true });
|
||
|
||
// 按行处理
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop() || '';
|
||
|
||
for (const line of lines) {
|
||
if (line.trim()) {
|
||
addEvent('数据', line, 'stream');
|
||
}
|
||
}
|
||
}
|
||
|
||
} catch (e) {
|
||
if (e.name !== 'AbortError') {
|
||
updateStatus('disconnected', '连接错误: ' + e.message);
|
||
addEvent('错误', e.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
function disconnect() {
|
||
if (eventSource) {
|
||
eventSource.close();
|
||
eventSource = null;
|
||
}
|
||
if (abortController) {
|
||
abortController.abort();
|
||
abortController = null;
|
||
}
|
||
updateStatus('disconnected', '已断开');
|
||
addEvent('系统', '连接已关闭');
|
||
}
|
||
|
||
function updateStatus(type, message) {
|
||
const statusDiv = document.getElementById('status');
|
||
statusDiv.className = 'status ' + type;
|
||
statusDiv.textContent = message;
|
||
}
|
||
|
||
function addEvent(type, data, eventType = '') {
|
||
const eventsDiv = document.getElementById('events');
|
||
const item = document.createElement('div');
|
||
item.className = 'event-item';
|
||
|
||
const time = new Date().toLocaleTimeString();
|
||
item.innerHTML = `
|
||
<div class="time">[${time}] ${eventType ? '[' + eventType + '] ' : ''}${type}</div>
|
||
<div class="data">${escapeHtml(data)}</div>
|
||
`;
|
||
|
||
eventsDiv.insertBefore(item, eventsDiv.firstChild);
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|