SSE前端(sse前端跨域)
zhezhongyun 2025-06-15 20:35 14 浏览
<!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>
相关推荐
- EU Said to Accept a 10% U.S. Universal Tariff while Seeking Exemptions for Key Sectors
-
TMTPOST--TheEuropeanUnionmaymakeconcessionstosecureexemptionsfromtariffsonkeysectors...
- 抖音品质建设 - iOS启动优化《实战篇》
-
前言启动是App给用户的第一印象,启动越慢,用户流失的概率就越高,良好的启动速度是用户体验不可缺少的一环。启动优化涉及到的知识点非常多,面也很广,一篇文章难以包含全部,所以拆分成两部分:原理和实战...
- 荷兰引进美国诗人阿曼达·戈尔曼诗作,因译者肤色遭抵制
-
记者|刘亚光阿曼达在拜登就职典礼上朗诵诗歌。图源:PatrickSemansky/AssociatedPress阿曼达·戈尔曼(AmandaGorman)出生于1998年,自小患有语言障碍,...
- EU and U.S. Upbeat on Trade Deal Ahead of July Deadline
-
TMTPOST--TheEuropeanUnionandtheUnitedStatesseemupbeatontheirtradeagreementtoavoidtr...
- “过期食品”英文怎么说?(过期食品)
-
在购买食品时,我们都会特别留意一下食物的保质期有多久,是否新鲜,以免买到过期的商品。TheafternoonteaspreadatThePeninsulaBoutiqueandCaf...
- 世界首富撩妹露骨短信遭曝光 网友评论亮了
-
原标题:世界首富如何撩妹?亚马逊创始人贝索斯给情妇的露骨短信曝光这周最大的一个瓜,可能就是亚马逊首席执行官杰夫·贝佐斯(JeffBezos)与妻子麦肯齐(MacKenzie)离婚的惊人消息。紧接...
- 征收熊孩子“尖叫费”不合理?店主回怼网友
-
爱尔兰一家很受欢迎的咖啡馆要收“孩童尖叫费”,网友们。。。爱尔兰一咖啡店店主5月4日在脸书发帖,表示要向带有吵闹孩童的顾客多收15%的额外费用,引发了大批网友的议论。原贴内容如下:图viaFaceb...
- Rationality, objectivity and pragmatism win the day in Geneva to benefit of all
-
ApressbriefingisheldbytheChinesesidefollowingtheChina-UShigh-levelmeetingoneconomica...
- Dify「模板转换」节点终极指南:动态文本生成进阶技巧(附代码)Jinja2引擎解析|6大应用场景实战
-
这篇文章是关于Dify「模板转换」节点的终极指南,解析了基于Jinja2模板引擎的动态文本生成技巧,涵盖多源文本整合、知识检索结构化、动态API构建及个性化内容生成等六大应用场景,助力开发者高效利用模...
- 微软 Edge 浏览器 96.0.4664.93 稳定版发布:修复大量安全问题
-
IT之家12月12日消息,据外媒mspoweruser消息,微软12月11日为Edge浏览器推出了96.0.4664.93稳定版。该版本没有增加新功能,而是修复了大量漏洞,...
- HarmonyOS NEXT仓颉开发语言实战案例:健身App
-
各位好,今日分享一个健身app的首页:这个页面看起比之前的案例要稍微复杂一些,主要在于顶部部分,有重叠的背景,还有偏移的部分。重叠布局可以使用Stack容器实现,超出容器范围的偏移可以使用负数间距来实...
- 如果使用vue3.0实现一个modal,你会怎么设计?
-
这是个很好的问题!设计一个Vue3.0Modal时,我建议按照可复用、高扩展、简洁的原则来实现。下面我给你一个清晰的设计思路,涵盖组件拆分、使用方式以及Vue3中特性(如Telepor...
- 在进行APP切图的前,我们需要做什么?
-
切图是个技术活,小伙伴们千万不能忽视切图的重要性噢,前文介绍了设计的七大元素,那么我们现在来看看在切图之前,我们需要做什么呢?。1、和客户端的技术沟通好用不同的框架来实现的时候,图会有不一样的切法。...
- 独立开发问题记录-margin塌陷(独立提出历史问题)
-
一、概述往事如风,一周就过去了。上周在Figma里指点江山,这周在前端代码里卑微搬砖。回想上周,在Figma中排列组合,并且精确到1像素。每设计出一个页面,成就感就蹭蹭往上涨。没想到还没沾沾自喜多久,...
- 循序渐进Vue+Element 前端应用开发(8)—树列表组件的使用
-
在我前面随笔《循序渐进VUE+Element前端应用开发(6)---常规Element界面组件的使用》里面曾经介绍过一些常规的界面组件的处理,主要介绍到单文本输入框、多文本框、下拉列表,以及按钮...
- 一周热门
- 最近发表
-
- EU Said to Accept a 10% U.S. Universal Tariff while Seeking Exemptions for Key Sectors
- 抖音品质建设 - iOS启动优化《实战篇》
- 荷兰引进美国诗人阿曼达·戈尔曼诗作,因译者肤色遭抵制
- EU and U.S. Upbeat on Trade Deal Ahead of July Deadline
- “过期食品”英文怎么说?(过期食品)
- 世界首富撩妹露骨短信遭曝光 网友评论亮了
- 征收熊孩子“尖叫费”不合理?店主回怼网友
- Rationality, objectivity and pragmatism win the day in Geneva to benefit of all
- Dify「模板转换」节点终极指南:动态文本生成进阶技巧(附代码)Jinja2引擎解析|6大应用场景实战
- 微软 Edge 浏览器 96.0.4664.93 稳定版发布:修复大量安全问题
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)
- opacity 属性 (32)