尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

突破原生限制:基于Fetch API实现可自定义Header的EventSource实战

突破原生限制:基于Fetch API实现可自定义Header的EventSource实战 1. 项目概述为什么我们需要自定义配置的 EventSource在构建现代Web应用时实时数据推送是一个绕不开的需求。无论是股票行情、即时聊天、还是后台任务进度通知都需要服务器能主动向客户端推送消息。过去我们可能会想到WebSocket它功能强大但有时候我们需要的只是一个简单的、单向的、从服务器到客户端的文本流。这时EventSource或称 Server-Sent Events SSE就成了一个轻量级且优雅的选择。然而很多刚接触EventSource的开发者包括几年前的我自己都会在官方文档的第一个例子后卡住“这玩意儿怎么传自定义Header啊比如我的API需要Authorization令牌进行鉴权。”你会发现new EventSource(url)的构造函数简单到令人发指它没有提供任何设置请求头或方法的选项。这似乎与如今前后端分离、处处需要认证的架构格格不入。这个标题——“EventSource前端使用需要添加header等自定义配置”——恰恰戳中了这个普遍痛点。它不是一个简单的API调用教学而是一个关于如何突破EventSource原生限制使其适应现代工程化需求的实战课题。简单来说原生的EventSource接口是为了保持极简和专注仅处理服务器发送的事件流而设计的因此它屏蔽了许多底层fetch或XMLHttpRequest才有的配置能力。但在真实项目中我们几乎百分之百需要这些能力添加认证头、传递自定义参数、设置超时、处理更复杂的错误状态。本文将彻底拆解这个问题不仅告诉你“怎么做”更会深入分析“为什么”要这么做以及在不同场景下的最佳实践和那些官方文档里不会写的“坑”。2. 核心需求解析何时需要突破原生EventSource的限制在动手解决技术问题之前我们必须先明确需求场景。盲目地给一个工具打补丁不如先确认它是否还是最适合的工具。以下是我在实际项目中总结出的、必须对EventSource进行自定义配置的典型场景2.1 身份认证与授权这是最核心、最普遍的需求。你的后端API网关或业务服务很可能使用JWT、OAuth2.0 Bearer Token或自定义令牌进行鉴权。客户端必须在发起SSE连接时通过HTTP Header通常是Authorization: Bearer token将这个凭证带给服务器。原生EventSource无法设置任何Header这意味着你无法连接到任何受保护的SSE端点。2.2 传递初始化上下文参数有时服务器需要知道客户端是谁、想要订阅什么频道才能推送精准的数据流。例如在客服系统中连接时需要传递userId和conversationId在监控系统中需要传递deviceId。这些参数虽然可以通过URL的查询字符串传递但当参数较多、较复杂或涉及敏感信息时放在请求体或自定义Header中是更规范的做法。原生EventSource仅支持GET请求和URL传参灵活性不足。2.3 自定义网络行为超时控制默认情况下EventSource连接会一直保持直到被手动关闭或发生网络错误。但在某些边缘网络环境下你可能需要设置一个连接建立的超时时间避免长时间挂起。重试策略原生EventSource在连接断开后有自动重试机制但其重试逻辑是固定的基于retry字段。你可能需要实现更智能的重试例如指数退避、在特定HTTP状态码下不重试等。CORS与凭证在跨域场景下你可能需要配置credentials选项如include以便携带Cookie等凭证信息。虽然EventSource构造函数第二个参数有一个withCredentials选项但其浏览器的支持度和行为一致性有时不如fetchAPI 清晰。2.4 处理非标准响应与错误标准的SSE流应以text/event-stream的Content-Type返回。但现实中服务器可能先返回一个错误状态码如401未授权、404未找到其响应体是JSON格式的错误信息。原生EventSource的onerror事件能捕获到网络层错误或流解析错误但很难优雅地获取并处理一个非流格式的、包含业务错误详情的初始HTTP响应。当你面临以上任何一种或多种情况时就意味着你需要一个“增强版”的EventSource解决方案。3. 方案选型四种实现自定义EventSource的路径理解了需求我们来看看有哪些技术路径可以实现目标。每种方案都有其适用场景和优缺点我将结合我的实战经验为你分析。3.1 方案一使用Fetch API 手动解析流推荐这是目前最灵活、最可控的方案也是本文重点详解的方案。其核心思想是用高度可配置的fetch()发起请求获取原始数据流然后自己模拟EventSource的事件解析和分发机制。优点完全的控制权你可以使用fetch()的所有配置项包括method,headers,credentials,signal用于超时和取消等。更好的错误处理可以检查响应的status和ok在连接建立前就处理业务错误。与现代异步编程融合基于ReadableStream和async/await代码结构清晰。无第三方依赖纯浏览器原生API实现。缺点需要手动实现协议解析你需要编写代码来解析data:、event:、id:、retry:这些SSE协议字段。虽然不复杂但增加了初始工作量。需要模拟EventSource的API如果你希望替换现有的EventSource代码可能需要封装成类似的addEventListener、close()等接口。适用场景新项目、对灵活性和可控性要求高的项目、需要精细错误处理和复杂配置的项目。3.2 方案二寻找并评估第三方库社区中存在一些封装好的库例如eventsource-polyfill或一些名为fetch-event-source的库。它们通常内部使用XMLHttpRequest或fetch来弥补原生API的不足。优点开箱即用通常提供了直接设置headers的选项省去自己造轮子的时间。可能处理了兼容性一些库会处理老版本浏览器的兼容问题。缺点依赖风险引入第三方依赖增加包体积且其维护状态不确定。灵活性受限库的封装可能隐藏了某些底层细节当你有特殊需求时可能不如自己实现的方案来得直接。可能过时随着浏览器API的演进一些polyfill库可能不再是最佳选择。实操心得在几年前eventsource-polyfill是一个不错的选择。但在今天fetchReadableStream的支持已经非常广泛IE除外自己实现一个轻量级的、贴合项目需求的封装长期来看更可控也更能加深你对SSE协议的理解。建议仅在项目时间极其紧张或团队技术栈有强制要求时选择此方案。3.3 方案三URL参数与Cookie局限性方案这是一种变通方案并非真正意义上的“自定义配置”。URL参数将认证令牌等信息作为查询字符串附加在URL后如wss://api.example.com/stream?tokenxyz。这种方式简单但存在安全隐患令牌会出现在浏览器历史记录、服务器日志中且不适合传输大量或复杂数据。Cookie如果认证是基于Cookie的且SSE端点与主站同域或正确配置了CORS那么浏览器会自动携带Cookie。这无需任何额外配置。优点极其简单兼容原生EventSource。缺点安全性低URL参数适用范围窄Cookie同域无法传递自定义Header。适用场景仅用于快速原型验证、内部工具或极其简单的场景不推荐用于生产环境。3.4 方案四后端代理架构级方案不在前端解决而是在前端和后端SSE服务之间增加一个同源的后端代理。前端连接到自己域下的代理端点如/api/proxy-sse由这个代理端点负责携带自定义Header去连接真实的SSE服务然后将流透传给前端。优点前端零改动前端可以继续使用最原始、最简单的EventSource。隐藏敏感信息认证令牌等保存在后端不会暴露给客户端。聚合与转换代理层可以做更多事情比如聚合多个流、转换数据格式等。缺点架构复杂引入了新的组件增加了部署和运维成本。额外的网络跳转可能增加延迟且代理本身可能成为性能瓶颈或单点故障。连接管理需要妥善管理后端到真实SSE服务的长连接避免资源泄漏。适用场景微服务架构中前端无法直连SSE服务或者需要严格隐藏后端服务地址和认证信息的企业级应用。结论对于大多数前端开发者需要直接解决的问题方案一Fetch API 手动解析是最具普适性、教育意义和实践价值的方案。接下来我们将深入其实现细节。4. 核心实现基于Fetch API打造可配置的EventSource我们将一步步构建一个名为CustomEventSource的类它模仿原生EventSource的常用API但内部使用fetch实现。4.1 类设计与构造函数首先我们设计这个类的接口。为了保持易用性我们尽量模仿原生API。class CustomEventSource { /** * 创建一个可自定义配置的 EventSource 连接 * param {string} url - SSE 端点URL * param {Object} options - 配置选项 * param {Object} [options.headers] - 自定义请求头如 { Authorization: Bearer xxx } * param {string} [options.methodGET] - 请求方法默认为 GET * param {Object} [options.body] - 请求体对于非GET请求 * param {number} [options.timeout] - 连接超时时间毫秒 * param {boolean} [options.withCredentialsfalse] - 是否发送凭据如Cookie */ constructor(url, options {}) { this.url url; this.options options; this.listeners {}; this.controller null; // 用于 AbortController this.status CONNECTING; // 状态CONNECTING, OPEN, CLOSED // 立即开始连接 this.connect(); } // ... 其他方法 }关键点解析options对象这是我们自定义能力的入口。我们通过它接收headers、method等fetch支持的参数。listeners对象用于存储用户通过addEventListener注册的回调函数键为事件名如message,error, 或自定义事件值为回调函数数组。controller(AbortController)这是实现超时和手动关闭连接的关键。AbortController的signal可以传递给fetch用于中止请求。status模仿原生EventSource的readyState让我们能追踪连接状态。4.2 建立连接与流解析connect方法是核心它负责使用fetch发起请求并处理返回的流。class CustomEventSource { // ... 构造函数 async connect() { // 如果已有连接先关闭 if (this.controller) { this.controller.abort(); } this.controller new AbortController(); const signal this.controller.signal; // 设置超时 if (this.options.timeout) { setTimeout(() { if (this.status CONNECTING) { this.controller.abort(); this.dispatchEvent(error, new Error(Connection timeout)); } }, this.options.timeout); } const fetchOptions { method: this.options.method || GET, headers: this.options.headers || {}, signal: signal, credentials: this.options.withCredentials ? include : same-origin, }; // 如果有请求体例如用于POST请求的SSE if (this.options.body) { fetchOptions.body JSON.stringify(this.options.body); // 如果未指定Content-Type默认设为application/json if (!fetchOptions.headers[Content-Type]) { fetchOptions.headers[Content-Type] application/json; } } try { const response await fetch(this.url, fetchOptions); // 关键检查HTTP状态码原生EventSource做不到这一点 if (!response.ok) { // 尝试读取错误信息可能是JSON格式 let errorMsg HTTP ${response.status}; try { const errorBody await response.text(); errorMsg : ${errorBody}; } catch (e) {} throw new Error(errorMsg); } // 检查Content-Type确保是事件流 const contentType response.headers.get(content-type) || ; if (!contentType.includes(text/event-stream)) { console.warn(Received non-SSE content-type: ${contentType}. Proceeding anyway, but may cause issues.); } this.status OPEN; this.dispatchEvent(open); // 触发 open 事件 // 开始读取并解析流 await this.readStream(response.body); } catch (error) { // 错误处理网络错误、超时、HTTP错误等 this.status CLOSED; // 如果是 abort超时或手动关闭不触发 error 事件 if (error.name ! AbortError) { this.dispatchEvent(error, error); } } } /** * 解析 ReadableStream * param {ReadableStream} body - fetch返回的响应体流 */ async readStream(body) { const reader body.getReader(); const decoder new TextDecoder(); let buffer ; let eventName message; // 默认事件名 let dataBuffer ; let lastEventId ; try { while (this.status OPEN) { const { done, value } await reader.read(); if (done) { // 流被服务器正常关闭 this.close(); break; } // 将二进制数据块解码为文本并追加到缓冲区 buffer decoder.decode(value, { stream: true }); // 按行分割并处理 let lineEndIndex; while ((lineEndIndex buffer.indexOf(\n)) 0) { const line buffer.substring(0, lineEndIndex).replace(/^\s|\s$/g, ); buffer buffer.substring(lineEndIndex 1); // 空行表示一个事件消息的结束 if (line.length 0) { if (dataBuffer.length 0) { // 移除末尾的换行符 const data dataBuffer.replace(/\n$/, ); // 构造事件对象模仿原生Event的data和lastEventId属性 const event { type: eventName, data: data, lastEventId: lastEventId, }; // 分发事件 this.dispatchEvent(eventName, event); // 重置为下一个事件做准备 dataBuffer ; eventName message; } continue; } // 处理包含冒号的行字段行 const colonIndex line.indexOf(:); if (colonIndex 0) { const field line.substring(0, colonIndex); let value line.substring(colonIndex 1).replace(/^ /, ); // 移除值前的第一个空格 switch (field) { case event: eventName value; break; case data: dataBuffer value \n; // 按规范data可以有多行用\n连接 break; case id: lastEventId value; break; case retry: const retryTime parseInt(value, 10); if (!isNaN(retryTime)) { // 这里可以处理自定义重试逻辑例如更新重试间隔 console.log(Server suggested retry interval: ${retryTime}ms); } break; // 忽略未知字段 } } // 以冒号开头的行注释行按规范忽略 } } } catch (error) { this.status CLOSED; this.dispatchEvent(error, error); } finally { reader.releaseLock(); } } // ... 其他方法addEventListener, dispatchEvent, close等 }代码深度解析与注意事项fetch与错误处理这是与原生EventSource最大的区别之一。我们使用response.ok检查HTTP状态码。如果服务器返回401、404、500等我们会抛出一个包含状态码和可能错误信息的异常从而在onerror中能获取到具体的业务错误原因。原生EventSource在这种情况下可能只会触发一个模糊的 error 事件。AbortController用于超时我们创建了一个AbortController将其signal传递给fetch。设置超时定时器后如果超时触发我们调用controller.abort()这会中断fetch请求并抛出AbortError。我们在catch块中通过error.name过滤掉这种“预期内”的中断避免触发error事件干扰业务逻辑。流解析算法这是实现SSE客户端的核心。算法逻辑是不断从流中读取数据块reader.read()。将二进制数据解码为文本追加到buffer。从buffer中按行\n分割。解析每一行空行标志着一个事件消息的结束以field: value格式的行是数据行。根据字段名累积数据event:指定事件类型data:累积数据支持多行id:记录最后的事件IDretry:获取重试建议。当遇到空行时将累积的data和eventName组装成一个事件对象并通过dispatchEvent分发出去。多行data字段的处理SSE协议规定一个data:字段对应一行。如果有多行data:它们最终会拼接成一个字符串中间用换行符\n连接。我们的代码dataBuffer value \n实现了这一点在分发前再移除最后一个多余的\n。4.3 事件管理与辅助方法为了让这个类用起来像原生EventSource我们需要实现事件监听和连接管理的方法。class CustomEventSource { // ... 之前的代码 addEventListener(type, listener) { if (!this.listeners[type]) { this.listeners[type] []; } this.listeners[type].push(listener); } removeEventListener(type, listener) { if (!this.listeners[type]) return; const index this.listeners[type].indexOf(listener); if (index -1) { this.listeners[type].splice(index, 1); } } dispatchEvent(type, event) { // 标准化事件对象 let finalEvent event; if (typeof event ! object || event null) { finalEvent { type, data: event }; } if (!finalEvent.type) { finalEvent.type type; } // 触发对应类型的所有监听器 if (this.listeners[type]) { this.listeners[type].forEach(listener { try { listener.call(this, finalEvent); } catch (err) { console.error(Error in event listener for ${type}:, err); } }); } // 始终触发 on* 属性形式的处理器如果存在 const handlerProp this[on${type}]; if (typeof handlerProp function) { try { handlerProp.call(this, finalEvent); } catch (err) { console.error(Error in on${type} handler:, err); } } } close() { this.status CLOSED; if (this.controller) { this.controller.abort(); this.controller null; } // 可以触发一个自定义的 close 事件 this.dispatchEvent(close); } // 便捷的 onmessage, onerror, onopen 属性设置器/获取器 set onmessage(handler) { this.addEventListener(message, handler); } set onerror(handler) { this.addEventListener(error, handler); } set onopen(handler) { this.addEventListener(open, handler); } // 注意getter 实现略复杂因为一个事件类型可能有多个监听器。 // 简易实现可以只记录最后一个通过属性设置的handler。 }设计要点事件系统我们实现了一个简单的事件发布-订阅模式。addEventListener和removeEventListener提供了标准的监听方式。dispatchEvent这个方法不仅调用通过addEventListener添加的函数还会调用直接赋值给onmessage、onerror等属性的处理器最大程度兼容原生API的使用习惯。close方法它通过AbortController中止正在进行的fetch请求并更新状态。这是正确释放资源的关键。4.4 完整使用示例现在我们可以像使用增强版的EventSource一样使用这个类了。// 创建连接 const eventSource new CustomEventSource(https://api.your-service.com/notifications, { headers: { Authorization: Bearer your-jwt-token-here, X-Custom-Header: CustomValue }, timeout: 10000, // 10秒连接超时 withCredentials: true, // 如果需要发送Cookie }); // 监听标准事件 eventSource.onopen () { console.log(SSE连接已建立); }; eventSource.onmessage (event) { console.log(收到消息:, event.data); try { const data JSON.parse(event.data); // 假设服务器发送的是JSON字符串 // 处理业务数据... } catch (e) { console.error(解析消息数据失败:, e); } }; eventSource.onerror (error) { console.error(SSE连接错误:, error); // 这里可以根据 error.message 判断是网络错误、超时还是HTTP 401等业务错误 if (error.message.includes(401)) { console.log(认证失败需要重新登录); // 跳转到登录页... } }; // 监听自定义事件服务器发送了 event: update eventSource.addEventListener(update, (event) { console.log(收到自定义更新事件:, event.data); }); // 在组件卸载或不再需要时务必关闭连接 // someComponent.onUnmount(() { // eventSource.close(); // });5. 进阶优化与生产环境实践一个基础的CustomEventSource类已经完成但要用于生产环境我们还需要考虑更多。5.1 实现自动重连与退避策略原生EventSource有简单的重试机制我们可以实现一个更智能的版本。class CustomEventSource { constructor(url, options {}) { this.url url; this.options options; this.listeners {}; this.controller null; this.status CONNECTING; this.reconnectAttempts 0; this.maxReconnectAttempts options.maxReconnectAttempts || 5; this.reconnectDelay options.initialReconnectDelay || 1000; // 初始延迟1秒 this.reconnectTimer null; this.connect(); } async connect() { // ... [之前的连接逻辑] ... } catch (error) { this.status CLOSED; if (error.name ! AbortError) { this.dispatchEvent(error, error); // 判断是否需要重连非手动关闭且未超过最大重试次数 if (this.shouldReconnect(error)) { this.scheduleReconnect(); } } } } shouldReconnect(error) { // 可以在这里定义哪些错误需要重连 // 例如网络错误、5xx服务器错误可以重连4xx客户端错误如401通常不重连 if (error.message.includes(401) || error.message.includes(403)) { return false; // 认证错误重连无用 } return this.reconnectAttempts this.maxReconnectAttempts; } scheduleReconnect() { this.reconnectAttempts; // 指数退避延迟时间逐渐增加例如 1s, 2s, 4s, 8s... const delay Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000); // 最大不超过30秒 console.log(连接断开${delay}ms后尝试第${this.reconnectAttempts}次重连...); clearTimeout(this.reconnectTimer); this.reconnectTimer setTimeout(() { if (this.status CLOSED) { this.connect(); } }, delay); } close() { this.status CLOSED; clearTimeout(this.reconnectTimer); // 手动关闭时取消重连定时器 if (this.controller) { this.controller.abort(); this.controller null; } this.dispatchEvent(close); } }5.2 心跳检测与连接健康度长时间连接可能因为网络静默或代理超时而断开。实现一个简单的心跳检测Ping/Pong机制可以保持连接活跃。服务器端需要定期发送一个注释行以冒号开头或一个特殊的事件如event: ping。: ping客户端在解析流时如果收到注释行可以重置一个“无数据超时计时器”。// 在 readStream 方法的行解析部分添加 if (colonIndex 0) { // 这是一个注释行例如 “: ping” // 可以在这里重置心跳超时计时器 this.resetHeartbeatTimeout(); continue; }同时在客户端设置一个计时器如果超过一定时间如60秒没有收到任何数据包括注释则主动断开并尝试重连。5.3 性能与内存考量背压Backpressure我们的readStream方法使用await reader.read()循环这天然地遵循了流的背压机制。浏览器会协调数据生产服务器推送和消费我们的事件处理的速度避免内存被过快填满。这是使用ReadableStream的优势之一。事件监听器泄漏务必在组件或页面销毁时如React的useEffect清理函数、Vue的beforeUnmount钩子中调用eventSource.close()并移除所有外部的事件监听器防止内存泄漏。连接数限制浏览器对同一域名下的并发HTTP连接数有限制通常为6个。虽然SSE连接是持久的但仍需注意不要创建过多不必要的连接。5.4 在React/Vue等框架中的封装在实际前端项目中我们通常会在框架组件中使用。以下是一个React Hooks的示例它封装了连接管理、数据状态和自动清理。import { useEffect, useRef, useState, useCallback } from react; function useCustomEventSource(url, options {}) { const [data, setData] useState(null); const [eventHistory, setEventHistory] useState([]); // 存储历史事件 const [status, setStatus] useState(connecting); const [error, setError] useState(null); const eventSourceRef useRef(null); const connect useCallback(() { if (eventSourceRef.current) { eventSourceRef.current.close(); } const es new CustomEventSource(url, { ...options, // 可以在这里注入全局的认证token headers: { Authorization: Bearer ${localStorage.getItem(token)}, ...options.headers }, }); es.onopen () setStatus(open); es.onerror (err) { setStatus(error); setError(err); }; es.onmessage (event) { setData(event.data); // 更新最新数据 setEventHistory(prev [...prev.slice(-9), event]); // 保留最近10条历史 }; es.addEventListener(close, () setStatus(closed)); eventSourceRef.current es; return es; }, [url, options]); // 依赖项 useEffect(() { const es connect(); // 清理函数组件卸载时关闭连接 return () { if (es) { es.close(); } }; }, [connect]); // 当url或options变化时会重新创建连接 const send useCallback((message) { // 注意SSE是单向的此方法仅作为示例实际需要其他方式如fetch发送消息 console.warn(SSE is server-to-client only. Use fetch or WebSocket for client-to-server.); }, []); return { data, eventHistory, status, error, send }; } // 在组件中使用 function NotificationPanel() { const { data, status, error } useCustomEventSource(/api/notifications/stream); if (status error) { return div连接错误: {error?.message}/div; } if (status connecting) { return div连接中.../div; } return ( div h3实时通知/h3 pre{data ? JSON.stringify(JSON.parse(data), null, 2) : 等待数据...}/pre /div ); }这个Hook自动处理了连接的创建和销毁并将连接状态和数据集成到React的状态管理中非常实用。6. 常见问题排查与实战技巧即使有了完善的代码在实际部署和调试中还是会遇到各种问题。以下是我踩过的一些坑和解决方案。6.1 连接立即断开或收到错误事件问题描述onopen事件后立刻触发onerror或者根本连不上。排查步骤检查网络面板打开浏览器开发者工具的Network标签页查看对SSE端点的请求。重点关注状态码是否是200还是401/404/500响应头Content-Type是否为text/event-stream服务器是否设置了Cache-Control: no-cache等防止缓存的头请求头你设置的自定义Header如Authorization是否正确发送出去了检查CORS如果前端与SSE服务器不同源必须确保服务器响应包含了正确的CORS头例如Access-Control-Allow-Origin: *或你的前端域名和Access-Control-Allow-Credentials: true如果你使用了withCredentials。检查服务器端确认你的SSE端点实现正确。一个最简单的Node.jsExpress测试端点如下app.get(/stream, (req, res) { console.log(Received headers:, req.headers); // 查看自定义header是否收到 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, Access-Control-Allow-Origin: * // 处理CORS }); // 发送一个测试事件 res.write(data: {msg: Connected}\n\n); // 定期发送心跳 const intervalId setInterval(() { res.write(: ping\n\n); // 注释行作为心跳 res.write(data: ${JSON.stringify({ time: Date.now() })}\n\n); }, 5000); // 客户端断开连接时清理 req.on(close, () { clearInterval(intervalId); console.log(Client disconnected); }); });6.2 数据接收不完整或解析错误问题描述只能收到第一条消息或者消息被截断或者JSON.parse失败。排查与解决检查SSE格式确保服务器发送的数据严格遵守SSE格式。每个事件必须以两个换行符\n\n结束。data:字段的每一行后面要有\n。一个常见错误是只在最后加一个\n。正确data: {foo: bar}\n\n错误data: {foo: bar}\n(缺少一个换行符客户端会一直等待)多行Data字段如果你发送多行数据每一行都要以data:开头。data: {\n data: id: 1,\n data: name: test\n data: }\n\n客户端会将其拼接为{id: 1, name: test}。编码问题确保服务器和客户端都使用UTF-8编码。在fetch中我们使用TextDecoder默认就是UTF-8。流缓冲区检查我们实现中的buffer处理逻辑。确保在解析完一行后正确地从缓冲区中移除已处理的部分 (buffer buffer.substring(lineEndIndex 1))。6.3 内存泄漏与连接未关闭问题描述页面跳转或组件销毁后SSE连接仍然存在持续消耗服务器资源和客户端内存。解决方案强制关闭在单页应用SPA的路由离开守卫、React组件的useEffect清理函数、Vue组件的beforeUnmount生命周期中务必手动调用eventSource.close()。使用AbortController我们的实现已经使用了AbortControllerclose()方法会调用abort()这能确保底层的fetch请求被正确中止相关的流阅读器也被释放。清理监听器虽然我们的简单实现中监听器存储在实例内部实例被丢弃后监听器也会被GC回收。但在更复杂的场景下如果监听了外部函数最好在close()方法中也清空this.listeners对象。6.4 与WebSocket的抉择这是架构设计时常被问到的问题。简单对比一下特性EventSource (SSE)WebSocket协议HTTP (基于文本)独立的 WebSocket 协议 (二进制或文本)方向仅服务器到客户端(单向)全双工(双向通信)自动重连内置(可自定义)需要手动实现数据格式仅文本(UTF-8)文本或二进制数据浏览器支持广泛 (除IE外)非常广泛 (包括IE10)自定义Header原生不支持(需本文方案)握手阶段可通过HTTP Header设置适用场景通知、日志流、新闻推送、状态订阅聊天、游戏、协同编辑、实时双向数据同步选择建议如果你的应用只需要服务器向客户端推送数据且数据是文本格式SSE是更简单、更省资源的选择。它的HTTP特性使其更容易通过防火墙、代理并且能利用HTTP/2的多路复用等优势。如果需要双向实时通信或者要传输二进制数据如文件、音频则必须选择WebSocket。最后这个自定义EventSource的方案虽然增加了一些初始复杂度但它赋予了你应对各种真实场景的灵活性。理解其原理后你可以根据项目的具体需求进行裁剪或扩展例如集成到状态管理库、添加更复杂的重试逻辑、或者与Service Worker结合实现后台推送。
返回列表