JavaScript 实现 HTTPS SSE 连接
一、SSE 简介
SSE(Server-Sent Events,服务器推送事件)是一种基于 HTTP 的单向通信协议,允许服务器主动向客户端推送数据,适用于实时通知、消息提醒、状态同步等场景。与 WebSocket 双向通信不同,SSE 仅支持服务器到客户端的单向传输,开销更小,适配 HTTPS 协议更简洁。
本文实现的 SSE 客户端具备以下特性:HTTPS 适配、自动重连、事件监听、单例模式、错误处理、动态拼接用户参数,可直接集成到 Vue3/Vite 等前端项目中。
二、 SSE 客户端代码
核心文件:utils/sseClient.js,采用类封装,提供完整的连接、监听、断开、重连能力,支持自定义事件与参数动态拼接。
javascript
import { getToken, getUserInfo } from '@/utils/tools';
class SSEClient {
constructor() {
this.source = null; // SSE 核心实例
// 基础 URL(从环境变量读取,适配不同环境)
this.baseUrl = import.meta.env.VITE_API_BASE_URL;
this.path = '/sse/talent/adminConnect'; // SSE 接口路径
this.url = `${this.baseUrl}${this.path}`;
// 请求头(携带 Token 做权限校验,适配 HTTPS 鉴权),目前这种方式并不支持自定义headers
this.headers = {
'satoken': getToken(),
};
this.reconnectInterval = 30000; // 自动重连间隔(30秒,可配置)
this.isConnected = false; // 连接状态标识
this.isReconnecting = false; // 重连状态标识(避免重复重连)
this.eventHandlers = new Map(); // 事件处理器映射表(支持多事件)
this.maxReconnectTimes = 10; //最大重连次数(避免无限重连)
this.reconnectCount = 0; // 当前重连次数
}
/**
* 动态拼接 URL(携带用户ID与Token,适配权限校验)
* 优化点:避免重复拼接相同参数,异常时返回基础URL
*/
getUrlWithParams() {
try {
const userInfo = getUserInfo();
// 无用户信息时返回基础URL
if (!userInfo || !userInfo.userId) {
return this.url;
}
const userId = userInfo.userId;
// 拼接参数(含Token与用户ID,双重鉴权)
const params = new URLSearchParams();
params.append('userId', userId);
// 拼接完整URL,避免重复参数
const fullUrl = `${this.url}?${params.toString()}`;
return fullUrl;
} catch (error) {
console.warn('SSE 拼接参数失败,使用基础URL:', error);
return this.url;
}
}
/**
* 初始化 SSE 连接
* @param {Object} options - 配置项
* @param {boolean} options.withCredentials - 是否携带跨域凭证
* @param {number} options.reconnectInterval - 自定义重连间隔
*/
connect(options = {}) {
// 避免重复连接或重连中重复触发
if (this.isConnected || this.isReconnecting) {
return;
}
// 合并配置项(支持自定义重连间隔)
const withCredentials = options.withCredentials || false;
this.reconnectInterval = options.reconnectInterval || this.reconnectInterval;
// 先断开已有连接(防止残留连接)
this.disconnect();
// 更新最新URL(确保参数实时)
this.url = this.getUrlWithParams();
try {
// SSE 初始化配置(适配 HTTPS 与跨域)
const eventSourceInitDict = { withCredentials };
this.source = new EventSource(this.url, eventSourceInitDict);
// 监听连接成功事件
this.source.onopen = () => {
this.isConnected = true;
this.isReconnecting = false;
this.reconnectCount = 0; // 重置重连次数
this.emit('open', { status: 'connected', url: this.url });
console.log('SSE 连接成功:', this.url);
};
// 监听服务器推送的通用消息
this.source.onmessage = (event) => {
try {
// 尝试解析 JSON 格式数据(兼容字符串与JSON)
const data = JSON.parse(event.data);
this.emit('message', data);
} catch (e) {
// 非JSON格式直接返回原始数据
this.emit('message', event.data);
}
};
// 监听连接错误(核心:错误处理与自动重连)
this.source.onerror = (error) => {
this.isConnected = false;
const errorMsg = `SSE 连接错误(状态码:${this.source.readyState})`;
console.error(errorMsg, error);
this.emit('error', { message: errorMsg, error, readyState: this.source.readyState });
// 仅在连接关闭时触发重连(排除临时错误)
if (this.source.readyState === EventSource.CLOSED && !this.isReconnecting) {
this.reconnectCount++;
// 超过最大重连次数停止重连
if (this.reconnectCount > this.maxReconnectTimes) {
this.isReconnecting = false;
this.emit('reconnect-failed', { maxTimes: this.maxReconnectTimes });
console.error(`SSE 重连失败(已超过最大次数 ${this.maxReconnectTimes})`);
return;
}
this.isReconnecting = true;
console.log(`SSE 准备重连(第 ${this.reconnectCount}/${this.maxReconnectTimes} 次),间隔 ${this.reconnectInterval}ms`);
// 延迟重连(避免频繁请求)
setTimeout(() => {
this.connect(options);
}, this.reconnectInterval);
}
};
// 注册已绑定的自定义事件(避免重连后事件丢失)
this.eventHandlers.forEach((handler, eventName) => {
this.bindCustomEvent(eventName, handler);
});
} catch (e) {
this.isReconnecting = false;
const errorMsg = '创建 SSE 连接失败(初始化异常)';
console.error(errorMsg, e);
this.emit('error', { message: errorMsg, error: e });
}
}
/**
* 绑定自定义事件(抽离复用,适配重连场景)
* @param {string} eventName - 自定义事件名
* @param {Function} handler - 事件处理器
*/
bindCustomEvent(eventName, handler) {
// 排除系统内置事件(open/message/error)
const systemEvents = ['open', 'message', 'error'];
if (systemEvents.includes(eventName)) return;
// 移除已有监听(避免重复绑定)
if (this.source) {
this.source.removeEventListener(eventName, handler);
// 绑定新监听
this.source.addEventListener(eventName, (event) => {
try {
const data = JSON.parse(event.data);
handler(data);
} catch (e) {
handler(event.data);
}
});
}
}
/**
* 注册事件监听(支持系统事件与自定义事件)
* @param {string} eventName - 事件名
* @param {Function} handler - 事件处理器
*/
on(eventName, handler) {
if (typeof handler !== 'function') {
console.warn(`SSE 事件 ${eventName} 处理器必须是函数`);
return;
}
// 存储事件处理器
this.eventHandlers.set(eventName, handler);
// 已连接状态下立即绑定事件
if (this.source && this.isConnected) {
this.bindCustomEvent(eventName, handler);
}
}
/**
* 触发事件(供内部调用,分发消息)
* @param {string} eventName - 事件名
* @param {any} data - 事件数据
*/
emit(eventName, data) {
const handler = this.eventHandlers.get(eventName);
if (handler) {
try {
handler(data);
} catch (e) {
console.error(`SSE 触发事件 ${eventName} 失败:`, e);
}
}
}
/**
* 移除事件监听
* @param {string} eventName - 事件名
*/
off(eventName) {
this.eventHandlers.delete(eventName);
if (this.source) {
const handler = this.eventHandlers.get(eventName);
if (handler) {
this.source.removeEventListener(eventName, handler);
}
}
}
/**
* 关闭 SSE 连接(重置状态,避免内存泄漏)
* @param {boolean} isManual - 是否手动关闭(区别于异常断开)
*/
disconnect(isManual = false) {
if (this.source) {
this.source.close();
this.source = null;
}
// 重置状态
this.isConnected = false;
this.isReconnecting = false;
if (isManual) {
this.reconnectCount = 0; // 手动关闭重置重连次数
this.emit('close', { isManual });
console.log('SSE 手动关闭连接');
}
}
/**
* 强制重连(主动触发重连,重置重连次数)
* @param {Object} options - 配置项
*/
forceReconnect(options = {}) {
this.reconnectCount = 0;
this.isReconnecting = false;
this.connect(options);
}
/**
* 更新连接参数(如Token过期后刷新URL)
*/
updateParams() {
this.url = this.getUrlWithParams();
// 已连接状态下重新连接(刷新参数)
if (this.isConnected) {
this.forceReconnect();
}
}
}
// 全局单例模式(确保全项目唯一 SSE 实例,避免多连接冲突)
export const sseClient = new SSEClient();
三、使用示例(Vue3/Vite 集成)
3.1 全局引入(main.js)
将 SSE 实例挂载到全局,便于全项目使用:
javascript
import { createApp } from 'vue';
import App from './App.vue';
import { sseClient } from '@/utils/sseClient';
const app = createApp(App);
// 全局挂载 SSE 实例
app.config.globalProperties.$sse = sseClient;
app.mount('#app');
3.2 组件内使用(页面/组件中)
vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { sseClient } from '@/utils/sseClient';
onMounted(() => {
// 1. 注册事件监听
// 连接成功事件
sseClient.on('open', (res) => {
console.log('SSE 连接成功', res);
// 可在这里处理连接成功后的逻辑(如更新UI状态)
});
// 通用消息事件(服务器推送的所有消息)
sseClient.on('message', (data) => {
console.log('收到 SSE 消息', data);
// 处理业务逻辑(如实时通知、数据更新)
});
// 自定义事件(后端指定事件名,如"notice")
sseClient.on('notice', (data) => {
console.log('收到自定义通知', data);
// 处理自定义消息(如系统通知、业务提醒)
});
// 错误事件
sseClient.on('error', (err) => {
console.error('SSE 异常', err);
// 处理错误(如提示用户、降级处理)
});
// 重连失败事件
sseClient.on('reconnect-failed', (res) => {
console.error('SSE 重连失败', res);
// 提示用户手动刷新页面
});
// 2. 启动 SSE 连接(配置跨域凭证)
sseClient.connect({ withCredentials: true });
});
onUnmounted(() => {
// 组件卸载时移除事件监听、关闭连接(避免内存泄漏)
sseClient.off('open');
sseClient.off('message');
sseClient.off('notice');
sseClient.off('error');
sseClient.off('reconnect-failed');
sseClient.disconnect(true); // 手动关闭连接
});
</script>
3.3
- 可以用pinia全局管理并调用,配合路由守卫也可以全局使用
3.4 特殊场景处理
-
Token 过期刷新 :Token 刷新后调用
sseClient.updateParams(),自动更新URL并重新连接。 -
用户切换 :切换用户时先调用
sseClient.disconnect(true)关闭连接,再调用connect()重新连接。 -
手动触发重连 :需要主动重连时调用
sseClient.forceReconnect()。
四、HTTPS 适配注意事项
-
SSE 基于 HTTP/HTTPS 协议,HTTPS 环境下需确保后端 SSE 接口配置正确的 SSL 证书,避免证书无效导致连接失败。
-
请求头鉴权:HTTPS 场景下建议将 Token 放在 URL 参数或请求头中(本文采用 URL 参数,适配部分后端不支持 SSE 自定义请求头的场景)。
-
跨域配置:若前端与后端跨域,需后端配置 CORS 允许
EventSource请求,同时前端开启withCredentials: true携带跨域凭证。 -
连接超时:SSE 无内置超时机制,可通过后端设置心跳消息(如每30秒推送一次空消息),前端监听心跳判断连接状态。
五、后端配合要求
-
响应头配置:后端需返回正确的 SSE 响应头
Content-Type: text/event-stream,同时设置Cache-Control: no-cache禁止缓存。 -
消息格式:后端推送消息需遵循 SSE 格式规范,如
data: { "msg": "hello" }\n\n(注意结尾双换行)。 -
自定义事件:后端推送自定义事件时需指定事件名,如
event: notice\ndata: { "msg": "通知" }\n\n。 -
连接保持:后端需保持 SSE 连接长驻,避免主动断开,同时处理客户端断开后的资源释放。
六、常见问题排查
-
连接失败(403 无权限):检查 Token 是否有效、用户ID是否正确,后端是否对 SSE 接口做了权限校验。
-
重连频繁触发 :排查后端是否主动断开连接、网络是否不稳定,可调整
reconnectInterval重连间隔。 -
消息无法解析:检查后端推送消息格式是否符合 SSE 规范,是否为合法 JSON(非JSON格式会走字符串解析分支)。
-
跨域错误:协调后端配置 CORS,允许前端域名访问,同时前端开启 withCredentials 配置。
-
内存泄漏:组件卸载时必须移除事件监听并关闭连接,避免残留实例导致内存泄漏。
七、拓展方向
-
添加日志记录:集成日志工具,记录 SSE 连接状态、消息内容、错误信息,便于线上问题排查。
-
心跳检测:前端定时发送心跳请求(或后端定时推送心跳),实现连接状态精准判断。
-
多实例支持:修改单例模式为工厂模式,支持同时创建多个 SSE 实例,适配多接口推送场景。
-
TypeScript 适配:添加类型定义,提升代码提示与类型安全,适配 TS 项目。
-
失败重试策略:支持指数退避重连(重连间隔逐渐延长),减少后端压力。