题干与适用场景
一个实时协作页面需要接收高频事件并向服务器发送编辑操作。请评估 WebSocketStream 是否适合,说明如何利用 Streams 背压、处理关闭与取消、避免消息堆积,并设计不支持浏览器的降级路径。
WebSocketStream 是基于 Promise 的实验性、非标准 Web API。它把连接暴露为 ReadableStream 和 WritableStream,能够利用 Streams 的背压调节读写速度;MDN 明确提示生产使用前必须检查浏览器兼容性。题目考察的是流式协议设计与风险判断。
面试官考察点
重点包括:ReadableStream 与 WritableStream 的生命周期、背压如何阻止无界队列、reader/writer 锁定、abort 与 close 的差异、消息顺序和幂等、心跳重连、Worker 使用,以及实验性 API 的能力检测与回退。
30 秒回答框架
“我先确认 WebSocketStream 不是当前标准能力,只在能力检测通过的浏览器启用。打开连接后分别管理 readable 和 writable,消费端处理不过来时让背压自然传播,不用数组无限缓存。关闭、取消和网络错误分别进入可观测状态,并用 AbortSignal 终止请求。生产默认保留传统 WebSocket 适配层,统一消息协议、限长队列、重连退避和重复消息去重。”
分步骤深入解答
第一步:确认能力和边界
先检查当前运行时是否提供 WebSocketStream,再决定是否启用。它目前是实验性且非标准 API,不能因为 Chrome 可用就假设所有浏览器、WebView 或企业环境都支持。能力检测失败时直接选择稳定实现。
第二步:建立连接与流
构造 WebSocketStream 后等待 opened Promise,取得 readable、writable、协议和扩展信息。读取端通过 reader 逐块消费,写入端通过 writer 写入;连接关闭状态由 closed Promise 统一汇报。
const socket = new WebSocketStream(url);
const { readable, writable } = await socket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();第三步:利用背压
不要把所有事件先推入普通数组。让下游 TransformStream 或 WritableStream 的 highWaterMark 约束队列,写入方等待 writer.ready,读取方按处理能力调用 read。背压只能限制流内生产速度,服务器仍需有协议级限流。
第四步:设计消息顺序
为编辑操作定义序列号、文档版本和幂等键。网络重连后不能假设旧流中的最后一个消息已落库;客户端应从服务端请求缺口或快照,再应用新事件。显示层和传输层都要能识别重复消息。
第五步:处理取消与关闭
用户离开页面或切换文档时,先停止读取和写入,再调用 close 或取消相应流。AbortSignal 适合把组件生命周期传给异步任务;正常关闭、主动取消、协议错误和网络断开应进入不同状态,避免把所有情况都当作可重试。
第六步:防止内存和 UI 堵塞
解析、校验和渲染分层执行,必要时把解码或批处理移到 Worker。设置最大消息大小、最大待处理事件数和批次时间;超过阈值时暂停订阅、丢弃可重建的中间事件,或请求服务器发送快照,而不是继续累积。
第七步:设计重连与恢复
重连使用指数退避和抖动,带上最后确认的文档版本。服务端返回缺口事件或快照后,客户端再恢复实时流。写入操作需要客户端生成幂等 ID,防止断线后重复提交。
第八步:实现降级和观测
传统 WebSocket 作为默认回退,使用同一消息 envelope 和状态机。记录能力检测结果、opened/closed 延迟、队列深度、背压等待、重连次数、丢弃事件和快照恢复率;按浏览器、网络类型和页面版本切分,决定是否扩大实验范围。
设计取舍与边界
WebSocketStream 还是 WebSocket
WebSocketStream 提供流式背压和 Promise 生命周期,但兼容性与标准化状态不足。传统 WebSocket 兼容性更广,却要求应用自行限制 onmessage 堆积和发送队列。可以用同一协议抽象两种传输实现。
丢弃事件还是请求快照
光标移动等可重建事件可以在队列超限时合并或丢弃;文档变更不能静默丢失,应暂停消费并请求带版本的快照。选择取决于事件是否可交换、是否有序以及服务端能否重放。
主线程还是 Worker
小消息和低频渲染适合主线程;高频二进制解码、压缩和批量合并可放入 Worker。Worker 不能消除总内存上限,仍需传输队列和取消协议。
失败演练与演进计划
浏览器不支持
关闭能力检测分支,确认传统 WebSocket 仍能建立连接、消费同样的消息并上报兼容性指标。
消费端变慢
人为降低渲染速度,验证背压等待增长、队列不无限扩张,并在阈值后触发快照恢复而不是崩溃。
连接中途断开
在发送操作后立即断网,确认幂等 ID、最后确认版本和重连退避能恢复状态,且不会重复应用编辑。
常见误区与追问
误区一:认为 Streams 自动解决服务器限流
追问:背压能控制什么?它控制客户端流内的生产和消费速度,不能替代服务器配额、连接级限流或业务批处理。
误区二:把 close、cancel 和 abort 混为一谈
追问:组件卸载时如何处理?分别记录正常关闭、读取取消和异步任务中止,让重连策略只作用于真正的网络故障。
误区三:只做 WebSocketStream 无回退
追问:WebView 或旧浏览器怎么办?能力检测失败就使用传统 WebSocket 适配器,消息协议和状态机保持一致。
延伸追问与参考答案
为什么要等待 writer.ready?
它让发送方在 WritableStream 背压时等待可写容量,避免应用用无界 Promise 或数组把消息堆在内存中;仍要配合服务端限流和消息上限。
什么时候应该请求快照?
当事件队列超过上限、序列号出现缺口或客户端无法确认连续版本时,请求带版本的快照比继续猜测事件顺序更安全。
如何在实验性 API 上做灰度?
按能力检测、浏览器版本和页面版本分桶,先启用小比例用户;比较队列深度、断线恢复、错误率和渲染延迟,指标退化时关闭开关并回到传统 WebSocket。