网站首页 > 技术文章 正文
<!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>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- Tailwind 配置 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
success: '#00B42A',
warning: '#FF7D00',
danger: '#F53F3F',
dark: '#1D2129',
light: '#F2F3F5'
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<!-- 自定义工具类 -->
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.card-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
}
</style>
</head>
<body class="font-inter bg-light min-h-screen flex flex-col">
<!-- 导航栏 -->
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fa fa-bolt text-primary text-2xl"></i>
<h1 class="text-xl font-bold text-dark">SSE 实时数据客户端</h1>
</div>
<div class="flex items-center space-x-4">
<div id="connection-status" class="flex items-center">
<span id="status-indicator" class="w-3 h-3 rounded-full bg-gray-400 mr-2"></span>
<span id="status-text" class="text-sm text-gray-600">未连接</span>
</div>
<button id="connect-btn" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">
<i class="fa fa-plug mr-1"></i> 连接
</button>
<button id="disconnect-btn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors" disabled>
<i class="fa fa-power-off mr-1"></i> 断开
</button>
</div>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-grow container mx-auto px-4 py-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左侧面板 - 配置区 -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl p-5 card-shadow h-full">
<h2 class="text-lg font-semibold text-dark mb-4 flex items-center">
<i class="fa fa-sliders text-primary mr-2"></i> 配置
</h2>
<div class="space-y-4">
<div>
<label for="sse-url" class="block text-sm font-medium text-gray-700 mb-1">SSE 服务器 URL</label>
<input type="text" id="sse-url" name="sse-url"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
value="http://localhost:8080/sse" placeholder="输入 SSE 服务器地址">
</div>
<div>
<label for="event-types" class="block text-sm font-medium text-gray-700 mb-1">事件类型</label>
<select id="event-types" name="event-types" multiple
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors">
<option value="message" selected>message (默认消息)</option>
<option value="notification" selected>notification (通知)</option>
<option value="update" selected>update (更新)</option>
<option value="error">error (错误)</option>
</select>
<p class="text-xs text-gray-500 mt-1">按住 Ctrl/Command 选择多个事件类型</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">显示设置</label>
<div class="flex items-center space-x-4">
<label class="inline-flex items-center">
<input type="checkbox" id="auto-scroll" name="auto-scroll" class="rounded text-primary focus:ring-primary/30" checked>
<span class="ml-2 text-sm text-gray-700">自动滚动</span>
</label>
<label class="inline-flex items-center">
<input type="checkbox" id="timestamp" name="timestamp" class="rounded text-primary focus:ring-primary/30" checked>
<span class="ml-2 text-sm text-gray-700">显示时间戳</span>
</label>
</div>
</div>
<div>
<label for="max-messages" class="block text-sm font-medium text-gray-700 mb-1">最大消息数</label>
<input type="number" id="max-messages" name="max-messages" min="10" max="1000" value="100"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors">
<p class="text-xs text-gray-500 mt-1">超过此数量时自动清除最早的消息</p>
</div>
<div class="pt-2">
<button id="clear-messages" class="w-full px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">
<i class="fa fa-trash mr-1"></i> 清除消息
</button>
</div>
</div>
</div>
</div>
<!-- 右侧面板 - 数据显示区 -->
<div class="lg:col-span-2">
<div class="bg-white rounded-xl p-5 card-shadow h-full flex flex-col">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-dark flex items-center">
<i class="fa fa-comments text-primary mr-2"></i> 实时消息
</h2>
<div class="text-sm text-gray-500">
<span id="message-count">0</span> 条消息
</div>
</div>
<!-- 消息过滤器 -->
<div class="mb-4">
<div class="flex items-center space-x-2">
<input type="text" id="message-filter" name="message-filter"
class="flex-grow px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
placeholder="过滤消息内容...">
<button id="apply-filter" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors">
<i class="fa fa-filter mr-1"></i> 应用
</button>
<button id="reset-filter" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors">
<i class="fa fa-refresh mr-1"></i> 重置
</button>
</div>
</div>
<!-- 消息显示区 -->
<div id="messages-container" class="flex-grow bg-gray-50 rounded-lg p-3 overflow-y-auto scrollbar-hide max-h-[calc(100vh-280px)]">
<div id="messages" class="space-y-3">
<div class="text-center text-gray-500 py-8">
<i class="fa fa-info-circle text-xl mb-2 block"></i>
<p>连接后将显示实时消息</p>
</div>
</div>
</div>
<!-- 错误提示区 -->
<div id="error-container" class="mt-4 hidden">
<div class="bg-red-50 border border-red-200 rounded-lg p-3 text-red-700">
<div class="flex items-start">
<div class="flex-shrink-0">
<i class="fa fa-exclamation-circle text-red-500"></i>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">连接错误</h3>
<div class="mt-2 text-sm">
<p id="error-message">发生未知错误,请检查连接</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 py-4 mt-6">
<div class="container mx-auto px-4 text-center text-gray-500 text-sm">
<p>SSE 实时数据客户端 © 2023</p>
</div>
</footer>
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// 元素引用
const connectBtn = document.getElementById('connect-btn');
const disconnectBtn = document.getElementById('disconnect-btn');
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
const sseUrlInput = document.getElementById('sse-url');
const messagesContainer = document.getElementById('messages-container');
const messagesElement = document.getElementById('messages');
const messageCountElement = document.getElementById('message-count');
const clearMessagesBtn = document.getElementById('clear-messages');
const autoScrollCheckbox = document.getElementById('auto-scroll');
const timestampCheckbox = document.getElementById('timestamp');
const maxMessagesInput = document.getElementById('max-messages');
const messageFilterInput = document.getElementById('message-filter');
const applyFilterBtn = document.getElementById('apply-filter');
const resetFilterBtn = document.getElementById('reset-filter');
const errorContainer = document.getElementById('error-container');
const errorMessageElement = document.getElementById('error-message');
const eventTypesSelect = document.getElementById('event-types');
// 全局变量
let eventSource = null;
let messageCount = 0;
let filteredCount = 0;
let currentFilter = '';
let isConnected = false;
// 更新连接状态UI
function updateConnectionStatus(connected) {
isConnected = connected;
if (connected) {
connectBtn.disabled = true;
connectBtn.classList.add('opacity-70', 'cursor-not-allowed');
disconnectBtn.disabled = false;
disconnectBtn.classList.remove('opacity-70', 'cursor-not-allowed');
statusIndicator.classList.remove('bg-gray-400', 'bg-danger');
statusIndicator.classList.add('bg-success');
statusText.textContent = '已连接';
statusText.classList.remove('text-gray-600', 'text-danger');
statusText.classList.add('text-success');
errorContainer.classList.add('hidden');
} else {
connectBtn.disabled = false;
connectBtn.classList.remove('opacity-70', 'cursor-not-allowed');
disconnectBtn.disabled = true;
disconnectBtn.classList.add('opacity-70', 'cursor-not-allowed');
statusIndicator.classList.remove('bg-success', 'bg-danger');
statusIndicator.classList.add('bg-gray-400');
statusText.textContent = '未连接';
statusText.classList.remove('text-success', 'text-danger');
statusText.classList.add('text-gray-600');
}
}
// 显示错误消息
function showError(message) {
errorMessageElement.textContent = message;
errorContainer.classList.remove('hidden');
statusIndicator.classList.remove('bg-success', 'bg-gray-400');
statusIndicator.classList.add('bg-danger');
statusText.textContent = '连接错误';
statusText.classList.remove('text-success', 'text-gray-600');
statusText.classList.add('text-danger');
}
// 格式化时间戳
function formatTimestamp() {
const now = new Date();
return now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
// 添加消息到容器
function addMessage(type, data, timestamp) {
// 检查是否超过最大消息数
if (messageCount >= parseInt(maxMessagesInput.value)) {
const firstMessage = messagesElement.querySelector('.message-item');
if (firstMessage) {
messagesElement.removeChild(firstMessage);
messageCount--;
if (!currentFilter) {
filteredCount--;
}
}
}
// 创建消息元素
const messageElement = document.createElement('div');
messageElement.className = 'message-item p-3 rounded-lg card-shadow transition-all duration-300';
// 根据事件类型设置样式
let bgColor, textColor, iconClass;
switch (type) {
case 'notification':
bgColor = 'bg-blue-50';
textColor = 'text-blue-800';
iconClass = 'fa-bell';
break;
case 'update':
bgColor = 'bg-green-50';
textColor = 'text-green-800';
iconClass = 'fa-refresh';
break;
case 'error':
bgColor = 'bg-red-50';
textColor = 'text-red-800';
iconClass = 'fa-exclamation-triangle';
break;
default: // message
bgColor = 'bg-gray-50';
textColor = 'text-gray-800';
iconClass = 'fa-comment';
}
messageElement.classList.add(bgColor, textColor);
// 消息内容
let content = '';
try {
// 尝试解析JSON
const jsonData = JSON.parse(data);
content = JSON.stringify(jsonData, null, 2);
messageElement.classList.add('font-mono', 'text-sm');
} catch (e) {
// 普通文本
content = data;
}
// 构建消息HTML
messageElement.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0 mt-0.5">
<i class="fa ${iconClass}"></i>
</div>
<div class="ml-3 flex-grow">
<div class="flex items-center justify-between">
<h4 class="text-sm font-medium capitalize">${type}</h4>
${timestampCheckbox.checked ? `<span class="text-xs opacity-70">${timestamp}</span>` : ''}
</div>
<div class="mt-1 text-sm whitespace-pre-wrap break-words">${content}</div>
</div>
</div>
`;
// 添加到容器
messagesElement.appendChild(messageElement);
messageCount++;
// 如果没有过滤器,增加过滤计数
if (!currentFilter) {
filteredCount++;
}
// 更新计数显示
updateMessageCount();
// 如果启用自动滚动,滚动到底部
if (autoScrollCheckbox.checked) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 添加动画效果
setTimeout(() => {
messageElement.classList.add('opacity-100');
}, 10);
}
// 更新消息计数
function updateMessageCount() {
messageCountElement.textContent = currentFilter ?
`${filteredCount} / ${messageCount}` :
messageCount;
}
// 过滤消息
function filterMessages() {
const filter = messageFilterInput.value.trim().toLowerCase();
currentFilter = filter;
filteredCount = 0;
const messageItems = messagesElement.querySelectorAll('.message-item');
messageItems.forEach(item => {
const content = item.textContent.toLowerCase();
if (filter === '' || content.includes(filter)) {
item.classList.remove('hidden');
filteredCount++;
} else {
item.classList.add('hidden');
}
});
updateMessageCount();
}
// 连接到SSE服务器
function connectToSse() {
const url = sseUrlInput.value.trim();
if (!url) {
showError('请输入有效的SSE服务器URL');
return;
}
try {
// 关闭现有连接
if (eventSource) {
eventSource.close();
}
// 创建新的EventSource
eventSource = new EventSource(url);
updateConnectionStatus(true);
// 监听默认消息事件
eventSource.onmessage = function(event) {
const selectedTypes = Array.from(eventTypesSelect.selectedOptions).map(option => option.value);
if (selectedTypes.includes('message')) {
addMessage('message', event.data, formatTimestamp());
}
};
// 监听自定义事件
Array.from(eventTypesSelect.options).forEach(option => {
const eventType = option.value;
if (eventType !== 'message') {
eventSource.addEventListener(eventType, function(event) {
const selectedTypes = Array.from(eventTypesSelect.selectedOptions).map(option => option.value);
if (selectedTypes.includes(eventType)) {
addMessage(eventType, event.data, formatTimestamp());
}
});
}
});
// 监听连接开启
eventSource.onopen = function() {
console.log('SSE 连接已建立');
addMessage('system', '已成功连接到服务器', formatTimestamp());
};
// 监听错误
eventSource.onerror = function(error) {
console.error('SSE 错误:', error);
if (eventSource.readyState === EventSource.CLOSED) {
showError('连接已关闭,可能是服务器问题或网络连接中断');
updateConnectionStatus(false);
} else {
showError('发生连接错误,请检查服务器URL和网络连接');
}
};
} catch (error) {
console.error('创建 EventSource 失败:', error);
showError(`创建连接失败: ${error.message}`);
updateConnectionStatus(false);
}
}
// 断开SSE连接
function disconnectFromSse() {
if (eventSource) {
eventSource.close();
eventSource = null;
updateConnectionStatus(false);
addMessage('system', '已断开与服务器的连接', formatTimestamp());
}
}
// 清除所有消息
function clearAllMessages() {
messagesElement.innerHTML = '';
messageCount = 0;
filteredCount = 0;
updateMessageCount();
}
// 事件监听
connectBtn.addEventListener('click', connectToSse);
disconnectBtn.addEventListener('click', disconnectFromSse);
clearMessagesBtn.addEventListener('click', clearAllMessages);
applyFilterBtn.addEventListener('click', filterMessages);
resetFilterBtn.addEventListener('click', function() {
messageFilterInput.value = '';
currentFilter = '';
filterMessages();
});
// 输入框回车键触发过滤
messageFilterInput.addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
filterMessages();
}
});
// 自动滚动切换
autoScrollCheckbox.addEventListener('change', function() {
if (this.checked) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
});
// 最大消息数变更
maxMessagesInput.addEventListener('change', function() {
// 确保值在合理范围内
const value = parseInt(this.value);
if (value < 10) {
this.value = 10;
} else if (value > 1000) {
this.value = 1000;
}
// 如果当前消息数超过新的最大值,清理消息
while (messageCount > parseInt(this.value)) {
const firstMessage = messagesElement.querySelector('.message-item');
if (firstMessage) {
messagesElement.removeChild(firstMessage);
messageCount--;
if (!currentFilter) {
filteredCount--;
}
}
}
updateMessageCount();
});
// 事件类型变更
eventTypesSelect.addEventListener('change', function() {
// 如果没有选中任何事件类型,默认选择message
if (this.selectedOptions.length === 0) {
const messageOption = Array.from(this.options).find(option => option.value === 'message');
if (messageOption) {
messageOption.selected = true;
}
}
});
// 初始设置
updateConnectionStatus(false);
});
</script>
</body>
</html>
猜你喜欢
- 2025-06-18 快速云:网站抗DDOS攻击能力如何测试,可以通过哪些平台?
- 2025-06-18 是时候使用iframe延迟加载来提升LCP!
- 2025-06-18 架构师:如何设计一个秒杀系统?(秒杀系统架构图)
- 2025-06-18 负载均衡实战:从入门到精通,掌握高并发系统的核心秘籍
- 2025-06-18 加速进化:网宿正式推出可编程CDN-边缘脚本
- 2025-06-18 如何在 1 秒内加载网站(要实现加载某个网页时弹出一个消息框)
- 2025-06-18 前端开发者也需要了解Redis吗?聊聊API缓存与用户体验
- 2024-10-04 前端开发:探索前沿,追逐未来 前端开发前沿技术
- 2024-10-04 高防CDN神乎其技?它是如何防御DDoS攻击的
- 2024-10-04 75CDN 增加 ES Module 支持 蔚来es6
你 发表评论:
欢迎- 532℃Oracle分析函数之Lag和Lead()使用
- 531℃几个Oracle空值处理函数 oracle处理null值的函数
- 529℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 519℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 513℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 504℃【数据统计分析】详解Oracle分组函数之CUBE
- 484℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 483℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端排序 (47)
- 前端密码加密 (47)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)