ARTICLE DETAIL

资讯详情

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

AI前端核心能力:TypeScript构建SSE+WebSocket流式数据管道

AI前端核心能力:TypeScript构建SSE+WebSocket流式数据管道 1. 这不是“面试技巧”而是AI时代前端工程师的生存切口“最后提醒一次9月的AI前端面试不用太老实”——这句话在技术社区刷屏时我正用TypeScript写一个SSE流式响应的错误重试逻辑。它听起来像一句调侃但背后是真实到刺骨的行业震感当大模型能30秒生成完整Vue组件、自动补全带类型推导的API调用链、甚至根据Figma设计稿反向输出带Vite配置的工程结构时死记硬背React生命周期、手写防抖节流、背熟Webpack打包原理的候选人正在被系统性淘汰。这不是危言耸听。我上个月参与了7场中高级前端岗位终面其中5个岗位JD里明确写着“需具备AI工具链集成经验”或“熟悉流式数据处理场景”。更关键的是面试官问的问题变了不再问“WebSocket和SSE有什么区别”而是直接甩给你一段后端返回的SSE流式JSON片段让你现场用TypeScript写一个健壮的消费器——要求处理断连重试、事件解析、类型安全校验、内存泄漏防护还要在Chrome 109和Electron 28环境下兼容运行。这已经不是考概念是在考你能否把AI时代的基础设施能力真正焊进自己的工程肌肉记忆里。核心关键词全部指向一个事实AI前端 ≠ 用ChatGPT写代码而是用TypeScript驾驭AI生成的流式数据流并在Electron/Vue/React等真实宿主环境中稳定落地。SSE和WebSocket不是两个并列选项而是分层能力——SSE负责轻量、单向、高并发的AI推理结果推送比如LLM token流WebSocket负责双向、低延迟、状态同步的交互控制比如前端主动发送用户指令、中断生成、切换模型。而TypeScript早已不是“可选的类型检查器”它是整个流式数据管道的类型锚点从SSE EventSource的event.data解析到WebSocket message事件的payload校验再到Vue Composition API中ref的类型推导所有环节都依赖一套精准、可演进、与后端OpenAPI严格对齐的类型定义。适合谁看如果你还在用any糊弄AI生成的接口响应、手动拼接WebSocket URL、把SSE当成“另一个HTTP请求”来处理这篇就是你的止损线如果你已用过vue-tsc1.8.27配合typescript5.3.3做类型检查但遇到stream disconnected before completion: idle timeout waiting for sse就抓瞎那这里拆解的就是你卡点的底层机制如果你正准备9月跳槽别再背八股文了——把下面这套SSEWebSocket双通道、TypeScript强类型、Electron可打包的实战方案吃透比刷100道算法题更接近真实战场。2. 为什么“老实”会输——AI前端面试的底层逻辑重构2.1 面试官到底在筛什么人过去前端面试的核心是“确定性能力验证”你能手写Promise A、能解释Virtual DOM diff、能优化首屏加载——这些能力对应着静态、可控、边界清晰的开发场景。但AI时代前端面对的是不确定性数据流LLM输出没有固定长度、token到达时间不可预测、网络中断频繁、用户随时可能中断生成。面试官要的不再是“你会不会”而是“你如何让不确定变得可控”。我整理了近期7家公司的终面真题发现高频考点高度集中SSE场景给定一段模拟LLM token流的SSE响应event: message\ndata: {id:1,delta:H}\n\n要求用TypeScript实现自动重连指数退避流式内容拼接避免token乱序类型安全解析delta字段必须是stringid必须是number内存泄漏防护EventSource未销毁导致的闭包引用WebSocket场景连接一个AI服务端如Ollama或自建FastAPI服务要求处理subprotocol协商Sec-WebSocket-Protocol: ai-v1实现心跳保活ping/pong帧消息序列化Binary vs Text何时用ArrayBuffer错误降级WebSocket失败时自动fallback到SSETypeScript深度不是考interface和type区别而是考declare global如何扩展EventSource的onmessage类型vue-tsc如何与typescript5.3.3协同做项目级类型检查尤其Vue 3.4的defineModel类型推导如何为动态生成的AI响应字段如response.choices[0].message.content编写可维护的类型守卫提示所有题目都要求“现场编码解释设计理由”。面试官不关心你是否记得readyState有哪几个值但会深挖“为什么选择EventSource而不是fetchReadableStream它的重连机制和你自己写的重试逻辑有何本质区别”2.2 “老实”的三大致命陷阱很多候选人栽在看似基础的环节根源在于对AI前端基础设施的理解还停留在HTTP时代陷阱一把SSE当“长连接HTTP”用忽视其事件驱动本质典型表现用fetch轮询模拟SSE、手动维护连接状态、自己实现事件解析正则匹配data:前缀。问题在于——SSE协议本身内置了Last-Event-ID恢复机制、浏览器原生重连、事件类型分发event: message。你绕过这些等于放弃浏览器最成熟的流式传输能力还要自己填坑。陷阱二WebSocket只关注“连上”忽略协议层细节常见错误new WebSocket(ws://...)后直接send不处理onopen/onclose状态机、不设置binaryType、不监听onerror做降级。更危险的是很多人不知道Chrome 109对WebSocket subprotocol的严格校验——如果服务端声明Sec-WebSocket-Protocol: ai-v1而客户端未在WebSocket构造函数中传入[ai-v1]连接会静默失败。这不是bug是协议强制要求。陷阱三TypeScript类型定义“假强”最典型的例子定义type AIResponse { choices: Array{ message: { content: string } } }然后response.choices[0].message.content.split()——看起来类型安全但实际运行时choices可能为空数组message可能为null。真正的强类型需要类型守卫isAIResponse、非空断言!和运行时校验Zod/Yup而不仅仅是编译期声明。2.3 真正的竞争力构建“流式数据管道”的工程直觉AI前端的核心能力是把SSE/WS这类底层传输协议转化为可组合、可测试、可监控的数据管道。这个管道包含四个关键层连接层Connection处理网络不稳定提供统一的连接管理重连策略、超时控制、协议协商解析层Parse将原始字节流转换为结构化数据注入类型安全SSE事件解析、WebSocket消息反序列化状态层State管理流式数据的生命周期开始/暂停/取消、token累积、错误上下文消费层Consume对接UI框架Vue ref、React useState提供响应式更新和副作用控制这四层不是理论而是你在vue-tsc报错时、在Electron打包后SSE失效时、在Chrome 109 WebSocket连接失败时真正要调试和修复的代码模块。接下来我们就用一个真实可运行的案例把这四层彻底焊死。3. 实战从零构建TypeScript流式AI前端管道SSEWebSocket双备3.1 项目骨架与环境约束我们构建一个最小可行产品MVP一个AI聊天界面支持两种模式SSE模式用于接收LLM token流轻量、单向、高并发WebSocket模式用于发送用户指令、中断生成、切换模型双向、低延迟技术栈约束完全对标热搜词TypeScript 5.3.3typescript: ^5.3.3vue-tsc 1.8.27vue-tsc: ^1.8.27Vue 3.4Composition API script setupElectron 28打包后需在桌面环境运行后端模拟本地Express服务提供/api/chat/sse和/api/chat/ws端点注意Electron打包是硬性门槛。很多候选人能在浏览器跑通SSE但Electron中EventSource默认不支持localhost以外的跨域且Node.js环境无window.EventSource。我们必须提前规避。3.2 连接层统一连接管理器ConnectionManager核心目标屏蔽SSE/WS差异提供一致的连接生命周期控制。// src/lib/connection-manager.ts import { Ref, ref, onUnmounted } from vue // 连接状态枚举 export enum ConnectionStatus { IDLE idle, CONNECTING connecting, CONNECTED connected, DISCONNECTED disconnected, ERROR error } // 连接配置 export interface ConnectionConfig { url: string protocol?: sse | websocket // SSE特有 event?: string // 监听的event类型默认message // WebSocket特有 subprotocols?: string[] // 通用重试 maxRetries?: number initialDelayMs?: number } // 连接实例抽象 export abstract class ConnectionT { protected status: RefConnectionStatus ref(ConnectionStatus.IDLE) protected connection: T | null null protected config: ConnectionConfig constructor(config: ConnectionConfig) { this.config config } abstract connect(): Promisevoid abstract disconnect(): void abstract send(data: any): void abstract onMessage(callback: (data: any) void): void abstract onError(callback: (error: Error) void): void } // SSE连接实现 export class SSEConnection extends ConnectionEventSource { private eventSource: EventSource | null null private retryTimer: NodeJS.Timeout | null null constructor(config: ConnectionConfig) { super(config) // 扩展全局EventSource类型添加onmessage类型 declare global { interface EventSource { onmessage: ((this: EventSource, ev: MessageEvent) any) | null } } } async connect(): Promisevoid { if (this.status.value ! ConnectionStatus.IDLE) return this.status.value ConnectionStatus.CONNECTING try { // 关键Electron中需用node-fetch polyfill或改用XMLHttpRequest // 此处简化实际项目需判断环境 this.eventSource new EventSource(this.config.url, { withCredentials: true }) this.eventSource.onopen () { this.status.value ConnectionStatus.CONNECTED this.retryTimer null } this.eventSource.onerror (error) { console.error(SSE connection error:, error) this.status.value ConnectionStatus.ERROR this.reconnect() } // 绑定事件监听 const event this.config.event || message this.eventSource.addEventListener(event, (e: MessageEvent) { try { const data JSON.parse(e.data) this.onMessageCallback?.(data) } catch (err) { console.warn(Failed to parse SSE data:, e.data) } }) } catch (err) { this.status.value ConnectionStatus.ERROR this.reconnect() } } disconnect(): void { if (this.eventSource) { this.eventSource.close() this.eventSource null } if (this.retryTimer) { clearTimeout(this.retryTimer) this.retryTimer null } } send(_data: any): void { throw new Error(SSE is read-only) } onMessage(callback: (data: any) void): void { this.onMessageCallback callback } onError(callback: (error: Error) void): void { this.onErrorCallback callback } private reconnect(): void { if (this.status.value ConnectionStatus.CONNECTED) return const delay this.config.initialDelayMs || 1000 this.retryTimer setTimeout(() { if (this.status.value ConnectionStatus.ERROR || this.status.value ConnectionStatus.DISCONNECTED) { this.connect() } }, delay) } private onMessageCallback: ((data: any) void) | null null private onErrorCallback: ((error: Error) void) | null null } // WebSocket连接实现 export class WebSocketConnection extends ConnectionWebSocket { private ws: WebSocket | null null private pingInterval: NodeJS.Timeout | null null constructor(config: ConnectionConfig) { super(config) } async connect(): Promisevoid { if (this.status.value ! ConnectionStatus.IDLE) return this.status.value ConnectionStatus.CONNECTING try { // 关键subprotocol必须显式传入否则Chrome 109拒绝连接 const protocols this.config.subprotocols || [] this.ws new WebSocket(this.config.url, protocols) this.ws.onopen () { this.status.value ConnectionStatus.CONNECTED // 启动心跳 this.startPing() } this.ws.onmessage (e) { try { const data typeof e.data string ? JSON.parse(e.data) : e.data // binary data this.onMessageCallback?.(data) } catch (err) { console.warn(Failed to parse WS message:, e.data) } } this.ws.onclose (e) { this.status.value ConnectionStatus.DISCONNECTED this.stopPing() if (e.code ! 1000) { // 正常关闭不重连 this.reconnect() } } this.ws.onerror (error) { console.error(WebSocket error:, error) this.status.value ConnectionStatus.ERROR this.reconnect() } } catch (err) { this.status.value ConnectionStatus.ERROR this.reconnect() } } disconnect(): void { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.close(1000, User disconnected) } this.stopPing() } send(data: any): void { if (this.ws this.ws.readyState WebSocket.OPEN) { const payload typeof data string ? data : JSON.stringify(data) this.ws.send(payload) } } onMessage(callback: (data: any) void): void { this.onMessageCallback callback } onError(callback: (error: Error) void): void { this.onErrorCallback callback } private startPing(): void { this.pingInterval setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: ping })) } }, 30000) // 30s心跳 } private stopPing(): void { if (this.pingInterval) { clearInterval(this.pingInterval) this.pingInterval null } } private reconnect(): void { if (this.status.value ConnectionStatus.CONNECTED) return const delay this.config.initialDelayMs || 1000 setTimeout(() { if (this.status.value ConnectionStatus.ERROR || this.status.value ConnectionStatus.DISCONNECTED) { this.connect() } }, delay) } private onMessageCallback: ((data: any) void) | null null private onErrorCallback: ((error: Error) void) | null null }关键设计解析统一抽象ConnectionT基类定义了所有连接必须实现的契约SSE和WS继承后只需关注协议特有逻辑。Electron兼容SSE实现中注释了EventSource在Electron中的坑——实际项目需用cross-fetch或XMLHttpRequest替代此处为保持代码简洁暂略。Chrome 109适配WebSocket构造函数显式传入subprotocols这是通过协议校验的唯一方式。心跳机制WebSocket必须主动发ping否则服务端可能因idle timeout断连对应热搜词stream disconnected before completion: idle timeout waiting for sse——注意这是SSE的timeout但WS同样存在需主动保活。3.3 解析层类型安全的流式数据解析器核心目标把原始SSE/WS消息转换为强类型、可校验的AI响应对象。// src/lib/ai-parser.ts import { z } from zod // LLM标准响应SchemaOpenAI格式 export const ChatCompletionChunkSchema z.object({ id: z.string(), object: z.literal(chat.completion.chunk), created: z.number(), model: z.string(), choices: z.array( z.object({ index: z.number(), delta: z.object({ role: z.string().optional(), content: z.string().optional(), function_call: z.any().optional() // 简化实际需更细粒度 }), finish_reason: z.string().optional() }) ) }) export type ChatCompletionChunk z.infertypeof ChatCompletionChunkSchema // 类型守卫运行时校验 export function isChatCompletionChunk(data: unknown): data is ChatCompletionChunk { try { ChatCompletionChunkSchema.parse(data) return true } catch { return false } } // SSE消息解析器 export class SSEParser { static parseMessage(data: string): ChatCompletionChunk | null { try { // SSE data字段可能包含换行需trim const cleaned data.trim() if (!cleaned) return null return JSON.parse(cleaned) as ChatCompletionChunk } catch (e) { console.warn(SSE parse failed:, e, data) return null } } } // WebSocket消息解析器支持Text/Binary export class WSParser { static parseMessage(data: string | ArrayBuffer): ChatCompletionChunk | null { try { const jsonStr typeof data string ? data : new TextDecoder().decode(data) return JSON.parse(jsonStr) as ChatCompletionChunk } catch (e) { console.warn(WS parse failed:, e, data) return null } } }关键设计解析Zod Schema优先不依赖interface声明而是用Zod在运行时校验。isChatCompletionChunk类型守卫确保if (isChatCompletionChunk(msg)) { msg.choices[0].delta.content }绝对安全。SSE/WS解析分离SSE的data字段可能有换行符需trim()WS的ArrayBuffer需TextDecoder解码。两者解析逻辑不同但输出类型一致。错误宽容parseMessage返回null而非抛异常避免流式处理中断。上层消费层需处理null情况。3.4 状态层流式会话状态管理器核心目标管理token流的累积、中断、错误上下文提供响应式状态。// src/lib/chat-session.ts import { ref, Ref, onUnmounted } from vue import { Connection, ConnectionStatus } from ./connection-manager import { ChatCompletionChunk, isChatCompletionChunk } from ./ai-parser export interface ChatMessage { id: string role: user | assistant | system content: string } export interface ChatSessionOptions { connection: Connectionany sessionId: string } export class ChatSession { // 响应式状态 messages: RefChatMessage[] ref([]) isLoading: Refboolean ref(false) error: Refstring | null ref(null) status: RefConnectionStatus ref(ConnectionStatus.IDLE) private connection: Connectionany private sessionId: string private accumulatedContent: string private abortController: AbortController | null null constructor(options: ChatSessionOptions) { this.connection options.connection this.sessionId options.sessionId // 同步连接状态 this.connection.onMessage((data) { this.handleMessage(data) }) this.connection.onError((error) { this.error.value error.message this.isLoading.value false }) // 监听连接状态变化 this.connection[status].value this.status.value this.connection[status].effect(() { this.status.value this.connection[status].value if (this.status.value ConnectionStatus.CONNECTED) { this.isLoading.value true } else if (this.status.value ConnectionStatus.DISCONNECTED) { this.isLoading.value false } }) } // 发送用户消息仅WebSocket支持 sendMessage(content: string): void { if (this.connection instanceof WebSocketConnection) { this.connection.send({ type: chat_message, session_id: this.sessionId, content, timestamp: Date.now() }) } } // 中断生成 abort(): void { if (this.abortController) { this.abortController.abort() this.abortController null } // 通知服务端中断 if (this.connection instanceof WebSocketConnection) { this.connection.send({ type: abort_generation, session_id: this.sessionId }) } } // 处理单条消息 private handleMessage(data: any): void { if (isChatCompletionChunk(data)) { const chunk data const choice chunk.choices[0] if (choice.delta.content) { this.accumulatedContent choice.delta.content // 更新messages响应式 const lastMsg this.messages.value[this.messages.value.length - 1] if (lastMsg lastMsg.role assistant) { lastMsg.content this.accumulatedContent } else { this.messages.value.push({ id: chunk.id, role: assistant, content: this.accumulatedContent }) } } if (choice.finish_reason) { this.isLoading.value false this.accumulatedContent } } else { console.warn(Unknown message type:, data) } } // 清理会话 clear(): void { this.messages.value [] this.error.value null this.accumulatedContent } // 生命周期清理 destroy(): void { this.connection.disconnect() this.abort() } }关键设计解析响应式累积accumulatedContent在内存中拼接token每次更新messages时复用同一对象引用避免Vue响应式触发过多更新。AbortController集成abort()方法不仅终止前端请求还通过WS发送中断指令实现端到端控制。finish_reason检测LLM返回finish_reason: stop或length时清空累积内容标志流结束。3.5 消费层Vue Composition API集成核心目标将上述三层封装为可直接在Vue组件中使用的Composable。// src/composables/use-ai-chat.ts import { ref, onUnmounted, Ref } from vue import { ConnectionManager, SSEConnection, WebSocketConnection, ConnectionConfig } from ../lib/connection-manager import { ChatSession, ChatSessionOptions } from ../lib/chat-session export interface UseAIChatReturn { messages: RefChatMessage[] isLoading: Refboolean error: Refstring | null status: RefConnectionStatus sendMessage: (content: string) void abort: () void clear: () void connect: (mode: sse | websocket) Promisevoid } export function useAIChat(sessionId: string default): UseAIChatReturn { // 创建连接管理器 const connectionManager new ConnectionManager() // 创建会话 const chatSession refChatSession | null(null) // 响应式状态 const messages refChatMessage[]([]) const isLoading refboolean(false) const error refstring | null(null) const status refConnectionStatus(ConnectionStatus.IDLE) // 连接函数 const connect async (mode: sse | websocket) { let config: ConnectionConfig if (mode sse) { config { url: /api/chat/sse, protocol: sse, event: message, maxRetries: 3, initialDelayMs: 1000 } const connection new SSEConnection(config) chatSession.value new ChatSession({ connection, sessionId }) } else { config { url: ws://localhost:3000/api/chat/ws, protocol: websocket, subprotocols: [ai-v1], // 关键Chrome 109必需 maxRetries: 3, initialDelayMs: 1000 } const connection new WebSocketConnection(config) chatSession.value new ChatSession({ connection, sessionId }) } // 同步会话状态到composable if (chatSession.value) { messages.value chatSession.value.messages.value isLoading.value chatSession.value.isLoading.value error.value chatSession.value.error.value status.value chatSession.value.status.value } } // 代理会话方法 const sendMessage (content: string) { chatSession.value?.sendMessage(content) } const abort () { chatSession.value?.abort() } const clear () { chatSession.value?.clear() } // 组件卸载时清理 onUnmounted(() { chatSession.value?.destroy() }) return { messages, isLoading, error, status, sendMessage, abort, clear, connect } }关键设计解析模式切换connect(mode)支持SSE/WS一键切换满足不同场景需求SSE用于生产环境流式输出WS用于开发调试。状态代理messages等Ref直接代理chatSession内部状态避免重复定义。Electron打包适配URL使用相对路径/api/chat/sseElectron中可通过webPreferences.webSecurity: false或配置webpack代理解决跨域。4. 高频问题排查与独家避坑指南4.1 SSE相关问题stream disconnected before completion: idle timeout waiting for sse现象SSE连接建立后约30秒无数据自动断开控制台报错stream disconnected before completion: idle timeout waiting for sse。根因分析这不是前端问题而是服务端SSE超时配置。SSE协议要求服务端定期发送:keep-alive\n\n注释行维持连接。Node.js Express中默认无此机制。解决方案服务端必须每25秒发送一次keep-alive// Express服务端示例 app.get(/api/chat/sse, (req, res) { res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }); // 发送keep-alive const keepAlive setInterval(() { res.write(:keep-alive\n\n); }, 25000); req.on(close, () { clearInterval(keepAlive); res.end(); }); });前端配合在SSEConnection.connect()中增加withCredentials: true并确保服务端CORS头允许凭据// 服务端需设置 res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Access-Control-Allow-Origin, http://localhost:5173); // 开发环境注意Electron中EventSource不支持withCredentials必须改用XMLHttpRequest或fetchReadableStream这是Electron打包的硬性改造点。4.2 WebSocket相关问题Chrome 109连接失败无报错现象Chrome 109中new WebSocket(url)后readyState始终为0CONNECTING无任何onerror或onclose回调。根因分析Chrome 109加强了WebSocket subprotocol校验。如果服务端在Sec-WebSocket-Protocol响应头中声明了协议如ai-v1而客户端未在构造函数中指定连接会被静默拒绝。验证方法打开Chrome DevTools → Network → 找到WebSocket连接 → 查看Headers → 检查Request Headers中是否有Sec-WebSocket-Protocol: ai-v1再检查Response Headers中是否有同名响应头。解决方案客户端必须显式传入subprotocols数组// 正确 const ws new WebSocket(ws://localhost:3000, [ai-v1]) // 错误Chrome 109会失败 const ws new WebSocket(ws://localhost:3000)服务端配合Express ws库const WebSocket require(ws); const wss new WebSocket.Server({ port: 3000 }); wss.on(connection, (ws, req) { // 检查客户端请求的subprotocol const clientProtocol req.headers[sec-websocket-protocol]; if (clientProtocol clientProtocol.includes(ai-v1)) { ws.protocol ai-v1; // 设置响应协议 } });4.3 TypeScript类型问题vue-tsc与typescript5.3.3版本冲突现象升级typescript到5.3.3后vue-tsc --noEmit报错Cannot find module vue/compiler-sfc或类型推导异常。根因分析vue-tsc版本需与Vue和TypeScript版本严格匹配。vue-tsc1.8.27是为Vue 3.4和TS 5.3.x设计的但若项目中存在旧版vue/compiler-sfc会导致类型冲突。解决方案执行三步清理锁定依赖版本package.json{ devDependencies: { typescript: ^5.3.3, vue-tsc: ^1.8.27, vue/compiler-sfc: ^3.4.0 } }清除缓存并重装rm -rf node_modules package-lock.json npm install # 或使用pnpm pnpm store prune pnpm install配置tsconfig.json启用Vue插件{ compilerOptions: { plugins: [ { name: volar/vue-language-core, options: { types: [vue/runtime-core] } } ] } }实操心得vue-tsc报错90%源于vue/compiler-sfc版本不匹配。永远用npm ls vue/compiler-sfc检查实际安装版本而非package.json声明。4.4 Electron打包问题SSE在打包后失效现象开发环境SSE正常electron-builder打包后EventSource报错ReferenceError: EventSource is not defined。根因分析Electron主进程是Node.js环境无window.EventSource。即使在渲染进程中若webPreferences.contextIsolation: true默认开启EventSource也可能被隔离。终极解决方案放弃EventSource改用fetchReadableStream兼容性更好且TypeScript类型更清晰// src/lib/electron-sse-adapter.ts export async function createSSEStream(url: string) { const response await fetch(url, { credentials: include }) if (!response.ok) throw new Error(SSE fetch failed: ${response.status}) const reader response.body?.getReader() if (!reader) throw new Error(SSE body is null) return { async next(): Promise{ done: boolean; value: string } | undefined { try { const { done, value } await reader.read() if (done) return { done, value: } const decoder new TextDecoder() const text decoder.decode(value) // 解析SSE格式event: message\ndata: {...}\n\n return { done, value: text } } catch (err) { console.error(SSE read error:, err) return { done: true, value: } } }, cancel() { reader.cancel() } } }优势完全TypeScript友好ReadableStreamDefaultReader有精确类型Electron、浏览器、Node.jsvianode-fetch全平台兼容可轻松集成AbortController实现超时控制5. 9月面试前必须完成的3个实操动作别再背题了。以下三个动作每个耗时不超过2小时但能让你在面试中展现出远超同龄人的工程纵深5.1 动手实现一个“可打断的SSE流式计数器”目标用TypeScript写一个SSE服务端Express和前端消费器支持服务端每秒发送一个数字data: {count: 1}\n\n前端显示当前数字并有一个“暂停”按钮点击暂停时前端停止消费服务端停止发送需传递信号为什么重要这覆盖了SSE最核心的“控制流”能力。90%的候选人只能实现单向推送而面试官想看到你如何用EventSource.close()服务端res.end()实现双向控制。代码写出来你就能解释清楚Last-Event-ID如何用于断点续传。5.2 用Postman调试WebSocket subprotocol目标1
返回列表