尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

AI前端流式交互实战:SSE与WebSocket混合架构设计

发布时间:2026/9/24 21:16:02

资讯中心
01
ARTICLE

AI前端流式交互实战:SSE与WebSocket混合架构设计

AI前端流式交互实战:SSE与WebSocket混合架构设计
1. 这不是“前端面试题”而是AI时代前端工程师的生存切口“最后提醒一次9月的AI前端面试不用太老实”——这句话在技术社区刷屏时我正蹲在客户现场调试一个大模型Agent的实时反馈界面。不是用WebSocket也不是SSE而是把两者揉在一起加了abort controller、流式chunk解析、类型安全校验、错误降级兜底最后还塞进Electron打包的离线壳里。面试官问“你怎么实现流式输出”我说“不是‘怎么实现’是‘怎么不崩’。”这句标题里的“不老实”根本不是教人糊弄面试官而是戳破一个现状当前80%的所谓AI前端面试题还在考你能不能手写一个fetch setInterval模拟流式响应而真实业务里你得扛住每秒300个并发连接、500ms内首字节返回、断网重连自动续传、用户中途取消不卡死、TypeScript类型链路从API Schema直通UI组件、甚至还要兼容tauri或electron的本地沙箱环境。核心关键词已经非常清晰AI前端、TypeScript、流式处理、SSE、WebSocket。但它们不是并列关系而是存在强依赖层级——TypeScript是地基流式处理是骨架SSE和WebSocket是两种不同负载场景下的血管选择而“AI前端”这个新物种本质是用前端工程能力重构AI交互范式不再是等完整JSON回来再渲染而是让文字像打字机一样逐字浮现不再是单次问答而是维持状态、支持中断、可暂停、可重试、可回溯的会话生命周期管理。适合谁看三类人最该盯紧正在准备秋招的应届生别再背“SSE和WebSocket区别”这种八股文要能讲清楚“为什么我们给教育类AI产品选SSE却给代码助手用WebSocket”工作3~5年的前端如果你的项目还没接入过任何大模型API或者只用过axios一次性请求那9月起你的技术栈可能正在被静默淘汰技术负责人或架构师得判断清楚团队要不要建一套可复用的AI交互SDK它该封装到什么粒度类型定义怎么和OpenAPI Spec联动Abort逻辑该放在哪一层错误码体系如何与LLM provider对齐这不是炫技是生存。当产品经理拿着竞品App说“我们要做一样的实时思考过程展示”当你接到需求文档写着“支持用户点击‘停止生成’后已输出内容保留未输出部分立即终止”你就知道——那些还在写useStateuseEffectfetch三件套的同学已经站在了淘汰线边缘。2. 为什么“老实”会吃亏——拆解AI前端面试背后的三重认知陷阱2.1 陷阱一把“流式输出”当成纯前端技术问题很多候选人一听到“流式输出”立刻开始背诵SSE是单向服务器推送基于HTTPWebSocket是全双工需要握手SSE自动重连WebSocket要自己维护心跳……这些都没错但全是脱离业务场景的静态知识。真实面试中面试官真正想考察的是你能否根据具体AI交互形态做出合理的技术选型决策并预判其工程代价。举个真实案例我们为一家法律咨询平台开发AI助手。初期用SSE实现律师回复流式渲染上线后发现两个致命问题用户在移动端弱网环境下SSE连接频繁断开重连后整个对话上下文丢失用户得重新描述案情当用户点击“停止生成”后端虽收到abort信号但因SSE协议本身不带语义中断机制只能靠超时关闭导致平均多吐出1.2秒无用token影响响应感知。解决方案不是换技术名词而是混合架构建立WebSocket长连接作为主通道承载会话ID、用户指令、中断信号、状态同步所有大模型响应仍走SSE复用现有Nginx流式代理配置但每个SSE响应头携带X-Session-ID前端通过WebSocket监听该会话的abort事件触发本地SSE连接close()同时在WebSocket层实现轻量心跳ping/pong 断线重连会话恢复重连时发送resume?session_idxxx。提示这里的关键不是“用了WebSocket”而是理解SSE擅长流式数据下发WebSocket擅长双向控制信令。把它们当工具而不是当考点。2.2 陷阱二把TypeScript当成“加类型注解”的装饰行为热搜词里反复出现“typescript面试”“vue-tsc”“typescript 7.0弃用项”说明一个问题大量前端对TS的理解还停留在interface定义和any泛滥阶段完全没意识到——在AI前端场景下TypeScript的核心价值是构建端到端的类型契约而非仅仅避免运行时报错。我们团队的真实实践后端提供OpenAPI 3.0规范YAML用openapi-typescript自动生成TS客户端类型但AI响应结构是动态的{ id: ..., object: chat.completion.chunk, choices: [{ delta: { content: hello }, index: 0 }] }这个delta字段在首chunk为空对象后续才逐步填充且content可能是string或null如果直接用typeof response生成类型会导致response.choices[0].delta.content在TS里是string | null | undefined但业务逻辑要求“只要content有值就追加到UI”强制非空断言(!)又违背TS设计哲学。最终方案定义AIChunk类型时用映射类型条件类型精准约束type AIChunk { id: string; object: chat.completion.chunk; choices: Array{ delta: { content: string; // 强制非空由解析层保证 role?: assistant | user; }; index: number; }; };在流式解析层SSE event listener做类型守卫转换function parseChunk(data: string): AIChunk | null { try { const raw JSON.parse(data); // 检查delta.content是否为string否则丢弃或报错 if (typeof raw.choices?.[0]?.delta?.content string) { return raw as AIChunk; // 类型断言在此处安全 } } catch (e) {} return null; }UI层消费时chunk.choices[0].delta.content就是确定的string无需可选链或非空断言。这才是TS在AI前端的真实价值用类型系统把“不确定的流式数据”转化为“确定的业务接口”让错误提前暴露在编译期而不是让用户看到空白屏幕。2.3 陷阱三把“AI交互”当成独立模块忽视与现有工程体系的咬合很多候选人设计AI SDK时喜欢搞个孤立的useAIChat()Hook里面封装fetch、abort、loading状态。这在Demo里很美但在真实项目里会迅速崩坏。我们曾接手一个Vue项目原团队写的AI Hook直接import了axios但主项目用的是vueuse/core的useFetch导致全局拦截器鉴权token注入、错误统一上报全部失效。真正的AI前端工程化必须回答三个咬合问题与状态管理的咬合Vuex/Pinia/Zustand如何存储会话历史是存原始chunk数组还是存解析后的text字符串如果存chunk内存增长如何控制比如只保留最近20条与构建工具的咬合vue-tsc版本是^1.8.27对应TS^5.3.3但团队引入的某个LLM client库要求TS5.4升级后moduleResolution: node10报错——这不是版本冲突而是tsconfig.json里compilerOptions的兼容性配置缺失与打包环境的咬合Electron打包时Node.js内置模块如stream在renderer进程不可用但某些流式解析库依赖它。解决方案不是禁用该库而是用rollup-plugin-node-builtins注入polyfill或改用浏览器原生ReadableStreamAPI。注意所谓“不老实”就是拒绝把AI功能当黑盒。你要清楚知道当用户点击“发送”这个动作穿过Router、穿过Auth Store、穿过AI SDK、穿过SSE EventSource最后落到哪个HTTP Client实例上中间每层的错误如何透出、如何重试、如何降级。3. 实操拆解一个生产级AI前端交互SDK的完整实现路径3.1 架构分层设计为什么必须分四层我们团队沉淀的AI交互SDK采用严格四层架构每一层职责单一、边界清晰经受过日均50万次会话的线上验证层级名称核心职责关键技术点典型文件L1Protocol Layer协议层封装底层通信协议屏蔽SSE/WebSocket差异EventSource API、WebSocket API、AbortController、自定义重连策略protocol/sse.ts,protocol/ws.tsL2Transport Layer传输层统一请求构造、鉴权注入、错误标准化Axios拦截器、OpenAPI Schema校验、HTTP状态码映射transport/client.ts,transport/error.tsL3Domain Layer领域层定义AI会话生命周期、状态机、业务规则状态机xstate、会话ID管理、中断/重试/续传逻辑domain/session.ts,domain/stateMachine.tsL4Integration Layer集成层适配框架生态React/Vue提供Hook/Composition APIReact Hook、Vue Composable、类型推导、SSR兼容integration/react.ts,integration/vue.ts为什么不能合并曾有实习生试图把SSE连接逻辑直接写进Vue Composable里结果导致无法单独测试协议重连逻辑Vue版本升级后Composition API变更整个AI模块需重写其他框架如React Native无法复用。分层后L1/L2可完全脱离框架运行L3是纯业务逻辑L4只是薄薄一层胶水。当我们把L1/L2抽成独立npm包ourorg/ai-transportReact和Vue团队就能并行开发互不干扰。3.2 协议层实操SSE与WebSocket的混合封装细节SSE连接封装重点解决重连与中断// protocol/sse.ts export class SSEClient { private eventSource: EventSource | null null; private reconnectTimer: NodeJS.Timeout | null null; private readonly maxReconnectAttempts 5; private reconnectCount 0; constructor( private readonly url: string, private readonly onMessage: (chunk: AIChunk) void, private readonly onError: (error: Error) void, private readonly onOpen?: () void ) {} connect() { // 关键SSE URL必须带唯一会话ID便于服务端关联 const url ${this.url}?session_id${generateSessionId()}; this.eventSource new EventSource(url, { withCredentials: true }); this.eventSource.onopen () { this.reconnectCount 0; this.onOpen?.(); }; this.eventSource.onmessage (event) { const chunk parseChunk(event.data); if (chunk) this.onMessage(chunk); }; this.eventSource.onerror (error) { // SSE error不等于连接失败可能是网络抖动 if (this.eventSource?.readyState 0) { this.handleReconnect(); } else { this.onError(new Error(SSE connection error)); } }; } private handleReconnect() { if (this.reconnectCount this.maxReconnectAttempts) { this.onError(new Error(SSE max reconnect attempts exceeded)); return; } this.reconnectCount; // 指数退避1s, 2s, 4s, 8s... const delay Math.pow(2, this.reconnectCount) * 1000; this.reconnectTimer setTimeout(() { this.disconnect(); this.connect(); }, delay); } // 中断核心主动关闭EventSource触发onerror但不触发重连 abort() { if (this.eventSource) { this.eventSource.close(); this.eventSource null; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer null; } } } disconnect() { this.abort(); } }WebSocket连接封装重点解决信令同步// protocol/ws.ts export class WSClient { private socket: WebSocket | null null; private readonly pingInterval 30000; // 30s心跳 private pingTimer: NodeJS.Timeout | null null; private isReconnecting false; constructor( private readonly url: string, private readonly onMessage: (data: WSMessage) void, private readonly onError: (error: Error) void, private readonly onOpen?: () void ) {} connect() { this.socket new WebSocket(this.url); this.socket.onopen () { this.isReconnecting false; this.startPing(); this.onOpen?.(); }; this.socket.onmessage (event) { try { const data JSON.parse(event.data) as WSMessage; this.onMessage(data); } catch (e) { this.onError(new Error(Invalid WS message format)); } }; this.socket.onerror (error) { this.onError(error as Error); }; this.socket.onclose () { if (!this.isReconnecting) { this.handleReconnect(); } }; } private startPing() { if (this.pingTimer) clearInterval(this.pingTimer); this.pingTimer setInterval(() { if (this.socket?.readyState WebSocket.OPEN) { this.socket.send(JSON.stringify({ type: ping })); } }, this.pingInterval); } // 发送控制信令中断、暂停、继续 sendControl(type: abort | pause | resume, payload: any {}) { if (this.socket?.readyState WebSocket.OPEN) { this.socket.send( JSON.stringify({ type, payload, timestamp: Date.now(), }) ); } } disconnect() { if (this.socket) { this.socket.close(); this.socket null; if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer null; } } } }混合协议协调器关键粘合点// protocol/coordinator.ts export class AIProtocolCoordinator { private sseClient: SSEClient | null null; private wsClient: WSClient | null null; private sessionId: string ; constructor( private readonly sseUrl: string, private readonly wsUrl: string, private readonly onChunk: (chunk: AIChunk) void, private readonly onControl: (event: WSMessage) void ) {} async init(sessionId: string) { this.sessionId sessionId; // 并行建立两个连接 await Promise.all([ new Promisevoid((resolve) { this.sseClient new SSEClient( ${this.sseUrl}?session_id${sessionId}, this.onChunk, (err) console.error(SSE error:, err), resolve ); this.sseClient.connect(); }), new Promisevoid((resolve) { this.wsClient new WSClient( this.wsUrl, this.onControl, (err) console.error(WS error:, err), resolve ); this.wsClient.connect(); }) ]); } // 对外暴露统一中断接口 abort() { this.sseClient?.abort(); this.wsClient?.sendControl(abort, { session_id: this.sessionId }); } // 会话结束时清理 destroy() { this.sseClient?.disconnect(); this.wsClient?.disconnect(); } }3.3 传输层实操如何让TypeScript类型贯穿全程OpenAPI Schema驱动的类型生成后端提供标准OpenAPI YAMLpaths: /v1/chat/completions: post: requestBody: required: true content: application/json: schema: $ref: #/components/schemas/CreateChatCompletionRequest responses: 200: description: OK content: text/event-stream: schema: type: string description: SSE stream of ChatCompletionChunk使用openapi-typescript生成基础类型npx openapi-typescript https://api.example.com/openapi.yaml --output src/types/openapi.ts但text/event-stream不会被正确识别需手动补全AIChunk类型见2.2节并创建TransportClient// transport/client.ts import { CreateChatCompletionRequest, ChatCompletionResponse } from ../types/openapi; export class TransportClient { private readonly axiosInstance: AxiosInstance; constructor(baseURL: string) { this.axiosInstance axios.create({ baseURL }); // 请求拦截注入鉴权token this.axiosInstance.interceptors.request.use((config) { const token getAuthToken(); // 从Pinia store或localStorage读取 if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // 响应拦截标准化错误 this.axiosInstance.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { // 触发全局登出 emitAuthError(); } throw new AIError(error.response?.data?.message || Network error, error.response?.status); } ); } // 流式请求方法返回PromiseReadableStream async streamChat( request: CreateChatCompletionRequest, signal?: AbortSignal ): PromiseReadableStream { const response await this.axiosInstance.postChatCompletionResponse( /v1/chat/completions, request, { headers: { Content-Type: application/json }, responseType: stream, // 关键启用流式响应 signal, } ); // 将Response.body转为ReadableStream现代浏览器 return response.data.body || response.data; } }类型安全的流式解析器// transport/parser.ts export async function parseSSEStream( stream: ReadableStream, onChunk: (chunk: AIChunk) void, onError: (error: Error) void ): Promisevoid { const reader stream.getReader(); let buffer ; try { while (true) { const { done, value } await reader.read(); if (done) break; // SSE格式data: {...}\n\n buffer new TextDecoder().decode(value); const lines buffer.split(\n); // 保留最后一行可能不完整 buffer lines.pop() || ; for (const line of lines) { if (line.startsWith(data: )) { const jsonStr line.slice(6).trim(); if (jsonStr) { try { const chunk JSON.parse(jsonStr) as unknown; // 类型守卫确保是AIChunk if (isAIChunk(chunk)) { onChunk(chunk); } } catch (e) { onError(new Error(Invalid SSE chunk: ${jsonStr})); } } } } } } catch (e) { onError(e as Error); } finally { reader.releaseLock(); } } function isAIChunk(obj: unknown): obj is AIChunk { return ( typeof obj object obj ! null id in obj object in obj choices in obj Array.isArray((obj as any).choices) (obj as any).choices.length 0 delta in (obj as any).choices[0] typeof (obj as any).choices[0].delta.content string ); }3.4 领域层实操会话状态机与生命周期管理我们采用xstate定义AI会话状态机覆盖所有真实场景// domain/stateMachine.ts import { createMachine, assign, ActorRefFrom } from xstate; export type AISessionContext { messages: Message[]; sessionId: string; abortController: AbortController | null; lastChunkTime: number; }; export type AISessionEvent | { type: SEND_MESSAGE; message: string } | { type: RECEIVE_CHUNK; chunk: AIChunk } | { type: ABORT_GENERATION } | { type: ERROR; error: Error } | { type: CLEAR_SESSION }; export const aiSessionMachine createMachine({ id: aiSession, initial: idle, context: { messages: [], sessionId: , abortController: null, lastChunkTime: 0, } as AISessionContext, states: { idle: { on: { SEND_MESSAGE: { target: sending, actions: assign({ messages: ({ context, event }) [ ...context.messages, { role: user, content: (event as any).message }, ], sessionId: () generateSessionId(), abortController: () new AbortController(), }), }, }, }, sending: { invoke: { src: sendRequest, onDone: { target: streaming, actions: assign({ messages: ({ context, event }) [ ...context.messages, { role: assistant, content: }, ], }), }, onError: { target: error, actions: assign({ messages: ({ context, event }) [ ...context.messages, { role: assistant, content: Error: ${(event as any).data.message} }, ], }), }, }, }, streaming: { on: { RECEIVE_CHUNK: { actions: assign({ messages: ({ context, event }) { const lastMsg context.messages[context.messages.length - 1]; if (lastMsg.role assistant) { return [ ...context.messages.slice(0, -1), { ...lastMsg, content: lastMsg.content (event as any).chunk.choices[0].delta.content, }, ]; } return context.messages; }, lastChunkTime: () Date.now(), }, }, ABORT_GENERATION: { actions: ({ context }) { context.abortController?.abort(); }, target: aborted, }, }, after: { // 30秒无新chunk自动超时 30000: { target: timeout, }, }, }, aborted: { on: { SEND_MESSAGE: { target: sending, actions: assign({ messages: ({ context, event }) [ ...context.messages, { role: user, content: (event as any).message }, ], sessionId: () generateSessionId(), abortController: () new AbortController(), }), }, }, }, timeout: { on: { SEND_MESSAGE: { target: sending, }, }, }, error: { on: { SEND_MESSAGE: { target: sending, }, }, }, }, }); // 服务定义实际调用协议层 export const sendRequest async ( context: AISessionContext, event: AISessionEvent ) { const coordinator new AIProtocolCoordinator( /api/sse, /api/ws, (chunk) { // 触发RECEIVE_CHUNK事件 send(RECEIVE_CHUNK, { chunk }); }, (msg) { // 处理WS信令 if (msg.type abort_ack) { send(ABORT_GENERATION); } } ); await coordinator.init(context.sessionId); // ... 启动流式处理 };3.5 集成层实操Vue Composable的类型推导魔法// integration/vue.ts import { ref, onUnmounted, watch } from vue; import { interpret, Interpreter } from xstate; import { aiSessionMachine } from ../domain/stateMachine; import { AISessionContext, AISessionEvent } from ../domain/stateMachine; export function useAIChat() { const service refInterpreterAISessionContext, any, AISessionEvent | null(null); const messages refMessage[]([]); const status refidle | sending | streaming | aborted | error | timeout(idle); const sessionId refstring(); // 初始化状态机服务 const init () { const machine aiSessionMachine.withContext({ messages: [], sessionId: , abortController: null, lastChunkTime: 0, }); const serviceInstance interpret(machine) .onTransition((state) { status.value state.value as any; messages.value state.context.messages; sessionId.value state.context.sessionId; }) .start(); service.value serviceInstance; }; // 发送消息 const sendMessage (message: string) { if (service.value) { service.value.send({ type: SEND_MESSAGE, message }); } }; // 中断生成 const abortGeneration () { if (service.value) { service.value.send(ABORT_GENERATION); } }; // 清理会话 const clearSession () { if (service.value) { service.value.send(CLEAR_SESSION); } }; // 组件卸载时销毁服务 onUnmounted(() { if (service.value) { service.value.stop(); } }); return { messages, status, sessionId, sendMessage, abortGeneration, clearSession, }; } // 类型推导让IDE能智能提示 declare module vue { interface ComponentCustomProperties { $aiChat: ReturnTypetypeof useAIChat; } }4. 面试高频问题实战拆解与避坑指南4.1 “SSE和WebSocket有什么区别”——别背八股讲清选型逻辑维度SSEWebSocket协议基础HTTP/1.1复用现有HTTP基础设施CDN、Nginx proxy独立协议需专门配置反向代理如Nginxproxy_http_version 1.1; upgrade $http_upgrade;连接方向单向Server→ClientClient无法主动发送数据全双工Client/Server可随时发送重连机制浏览器自动重连EventSource内置但重连间隔不可控需自行实现可精确控制重连策略指数退避、最大尝试次数适用AI场景纯流式响应下发如LLM文本生成、日志推送需要双向控制信令如中断生成、暂停/继续、会话状态同步移动端兼容性iOS Safari 12.2Android Chrome 70基本全覆盖全平台支持但弱网下连接建立延迟更高资源消耗连接数少HTTP连接池复用内存占用低每个连接独占socket高并发下服务端压力大面试回答模板“我们不会单纯比较技术优劣而是看业务需求。比如教育类产品用户主要消费AI生成的讲解文本对中断控制要求不高SSE足够——它能复用现有CDN缓存、Nginx流式代理运维成本低但如果是编程助手用户频繁点击‘停止生成’‘继续生成’就需要WebSocket传递控制指令此时SSE只负责下发数据形成混合架构。所以答案不是‘选哪个’而是‘怎么组合用’。”4.2 “如何实现流式输出的实时渲染”——警惕DOM重排性能陷阱常见错误写法// ❌ 错误每次chunk都触发完整DOM更新 const [content, setContent] useState(); useEffect(() { if (chunk) { setContent(prev prev chunk.choices[0].delta.content); // 频繁setState } }, [chunk]);性能问题每个token可能只有1~2字符触发一次React重渲染100个token就是100次difftextContent 操作在大量文本时V8引擎会触发字符串内部复制O(n²)复杂度滚动容器如聊天窗口频繁重排导致掉帧。生产级优化方案节流渲染每16ms1帧最多合并一次更新let pendingContent ; let animationFrameId: number | null null; function scheduleRender(newContent: string) { pendingContent newContent; if (animationFrameId null) { animationFrameId requestAnimationFrame(() { setContent(pendingContent); pendingContent ; animationFrameId null; }); } }虚拟滚动聊天记录超过50条时只渲染可视区域DOMTextEncoder优化用TextEncoder.encode()替代字符串拼接减少内存分配const encoder new TextEncoder(); const chunks: Uint8Array[] []; chunks.push(encoder.encode(token)); const fullBuffer concatUint8Arrays(chunks); // 自定义合并函数CSS硬件加速对滚动容器添加transform: translateZ(0)触发GPU渲染。4.3 “TypeScript 7.0弃用项如何迁移”——聚焦真实影响项热搜词提到的弃用项实际影响最大的只有两个moduleResolution: node10→ 必须改为node原因TS 7.0废弃旧版Node.js模块解析算法node10已被证明在ESM/CJS混合项目中产生类型错误。迁移步骤tsconfig.json中将moduleResolution: node10改为moduleResolution: node检查所有import语句确保.js后缀不再省略ESM要求显式后缀若使用vueuse/core等库升级至^10.0.0其内部已适配新解析规则运行tsc --noEmit --watch观察是否出现Cannot find module xxx or its corresponding type declarations错误若有需在types字段中补充声明文件路径。allowSyntheticDefaultImports→ 被esModuleInterop取代影响旧项目若同时开启allowSyntheticDefaultImports和esModuleInteropTS 7.0会警告。解决方案保留esModuleInterop: true推荐它会自动注入__importDefault辅助函数移除allowSyntheticDefaultImports因其功能已被包含检查所有import React from react写法确保react包的package.json包含exports字段React 18已支持。注意vue-tsc: ^1.8.27与TS^5.3.3是兼容的但若升级TS至7.0必须同步升级vue-tsc至^2.0.0否则vue-tsc --noEmit会报错。4.4 “Electron打包AI应用遇到Node模块报错”——三步定位法典型错误ReferenceError: require is not defined或Cannot find module stream。定位步骤确认运行环境在Electronrenderer进程中Node.js内置模块默认不可用出于安全考虑检查Webpack配置若用Vite需在vite.config.ts中设置export default defineConfig({ build: { target: es2020, }, resolve: { alias: { // 显式映射Node模块到browser polyfill stream: stream-browserify, util: util-browserify, }, }, });验证Polyfill有效性在renderer进程执行console.log(typeof require)应为function若仍报错需在preload.js中显式暴露// preload.js const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(node, { require: (name: string) { // 白名单机制只允许特定模块 if ([stream, util].includes(name)) { return require(name); } throw new Error(Module ${name} not allowed); }, });终极建议优先使用浏览器原生APIReadableStream,TextEncoder而非依赖Node polyfill既减小包体积又提升跨平台兼容性。4.5 “Postman如何测试WebSocket”——绕过GUI限制的命令行方案Postman对WebSocket支持有限仅基础连接无法发送二进制帧、无法模拟复杂信令。生产环境推荐用wscat# 安装 npm install -g wscat # 连接并发送JSON信令 wscat -c wss://api.example.com/ws \ --header Authorization: Bearer xxx \ --header Sec-WebSocket-Protocol: ai-v1 # 连接成功后输入 {type:start_chat,payload:{model:gpt-4,messages:[{role
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。