配了三年 WebSocket,今天才发现它和流处理之间从来就没连上过——WebSocketStream 把这件事彻底接上了
写聊天应用写到第三年,你大概已经习惯了这么一套流程:new WebSocket(),然后 onmessage、onerror、onclose 三个回调往上一挂,send() 一调用就开始发数据。这套模式跑了十几年,浏览器和服务器之间终于能建个双向通道了——但这条通道和 JavaScript 世界的Streams API 之间,从来就没连上过。
这就是为什么你写实时数据流的时候,发得太快会丢包,收得太快会积压,而你能做的只有靠 setInterval 硬控速,或者自己套一层队列管理。Streams API 早在 2018 年就给 Fetch 加上了 ReadableStream 和 WritableStream,偏偏 WebSocket 这条线一直孤着。
Chrome 124 把这件事接上了。
WebSocketStream 是什么
WebSocketStream 是 WebSocket API 的流式版本。它不是替代品,而是把 WebSocket 的连接能力和 Streams API 的流处理能力做了一个深度整合——连接本身保持不变,变化的是你怎么读写数据。
核心变化就一个:你拿到的不再是 onmessage 回调,而是一个 ReadableStream 用于收数据,一个 WritableStream 用于发数据。这两件事在旧的 API 里是通过事件和 bufferedAmount 属性勉强调度的,现在变成了标准流。
代码对比:旧写法 vs 新写法
先看旧的回调写法。连接、收消息、发消息分散在三个地方,逻辑是被切碎的:
const ws = new WebSocket('wss://chat.example.com');
ws.addEventListener('message', (event) => {
// 处理每条消息
const data = JSON.parse(event.data);
});
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
ws.addEventListener('close', () => {
console.log('Connection closed');
});
// 发消息靠的是直接写,不带任何背压控制
function sendMessage(msg) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
}
这套写法最大的问题是:send() 不会告诉你”对方还没收完”这件事。你只能靠 ws.bufferedAmount 自己去轮询,而且这个值是异步的、不精确的。发得快了,WebSocket 内部缓冲区一满,数据就丢了。
再看 WebSocketStream 的写法。同样是连接,现在拿到的是两个流对象:
const wss = new WebSocketStream('wss://chat.example.com');
// 等待连接打开,同时拆出读写两条流
const { readable, writable, protocol, extensions } = await wss.opened;
// 读流:每条消息是 ArrayBuffer 或 USVString
const reader = readable.getReader();
const writer = writable.getWriter();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// value 是单条消息,不再是事件对象
const data = JSON.parse(value);
// 处理完了再继续读——背压自动生效
}
// 发消息:write() 在对方窗口没准备好时会暂停
await writer.write(JSON.stringify({ type: 'message', content: 'hello' }));
两件事变得不一样了。第一,reader.read() 是 promise,每次 await 读完一条才去读下一条,如果你的处理逻辑慢,读的动作就会自动暂停,发送方那边对应的 write() 也跟着停——这就是 Streams API 的背压机制,WebSocket 第一次接入这套系统。第二,writer.write() 是带背压的,写入前会检查对方 writable 的状态,如果对方消费速度跟不上,写入动作会等待而不是直接丢数据。
背压是怎么工作的
Streams API 本身有一套完整的背压传递链路:ReadableStream 有个 controller,controller 有一个 desiredSize 属性;WritableStream 有一个 writer,writer.write() 返回的 promise 在底层 writable 还没准备好的时候会 pending。把 WebSocket 接入这套体系,就是让 WebSocket 的发送/接收缓冲区成为这套体系的一环。
旧 WebSocket 的 bufferedAmount 是个轮询值,你得自己算、自己在发之前检查、自己决定要不要等:
// 旧方式:手动轮询 bufferedAmount
function sendWithBackoff(msg) {
if (ws.bufferedAmount > 1024 * 1024) {
// 超过 1MB,等一会儿再试
setTimeout(() => sendWithBackoff(msg), 100);
return;
}
ws.send(msg);
}
WebSocketStream 把这个逻辑内置了。write() 调用在内部会等待 WritableStream 的背压传递,不需要你写任何轮询或者延时逻辑。这就是标准化的好处——背压不再是你自己实现的一个变通方案,而是语言层面定义好的一条管道。
读端同样有背压
读端的背压同样重要。旧 API 里,服务器发得快,你的 onmessage 回调就触发得密,中间没有任何控制手段。WebSocketStream 把读端接入了 ReadableStream 的控制机制:reader.read() 返回的 promise 在没有数据时会 pending,数据来了才会 resolve。如果你处理一条消息要花 50 毫秒,WebSocket 内部就会在这 50 毫秒里暂停从网络拉取数据,不会无限制地往你内存里积压。
对于实时性要求不高的场景——比如日志聚合、消息队列、前端缓存写入——这个背压机制意味着你终于可以在 JavaScript 这层控制消费速度了,而不用靠服务器限速或者自己写一个消息节流队列。
关闭连接和处理错误
连接关闭的感知方式也变了。旧 API 是 onclose 事件,新 API 是 wss.closed 这个 Promise:
// 旧的 onclose
ws.addEventListener('close', (event) => {
console.log(`Closed: code=${event.code}, reason=${event.reason}`);
});
// 新的 closed Promise
try {
const { code, reason } = await wss.closed;
console.log(`Connection closed: code=${code}, reason=${reason}`);
} catch {
// 非正常关闭(比如网络中断),Promise 会被 reject
console.error('Connection failed');
}
异常关闭的场景下 wss.closed 会 reject,这比旧 API 的行为更明确。正常关闭时 resolve,返回的 code 和 reason 和原来的 CloseEvent 一致。
错误处理则保持和原来一样的覆盖面:网络错误、握手失败、超时,这些在新 API 里同样会触发相应的异常,只是异常抛出的方式融入了 Promise 的 reject 流程,不再是独立的事件派发。
渐进增强和当前环境
目前 Chrome 124+ 已经支持 WebSocketStream,Firefox 和 Safari 还没上。检测支持很简单:
if ('WebSocketStream' in window) {
// 支持
} else {
// fallback 到传统 WebSocket
}
从功能支持的角度,这次不是”新 API 完全替代旧 API”,更像是”补全了缺的那块板子”。旧的 WebSocket API 不会被废弃,两套并行,用哪套取决于你需不需要流处理能力。如果你只需要发消息、收消息,旧的够了;如果你要做流量控制、要做流式处理、要把 WebSocket 的数据接入其他 Streams API 的管道,WebSocketStream 才是正确的那条路。
下一步
如果你现在就在维护一个聊天应用、一个实时数据面板、或者任何大量依赖 WebSocket 的项目,可以考虑分两步走。第一步,先做能力检测,给现有代码加一个分支:如果浏览器支持 WebSocketStream,切换到新写法,否则走原来的回调逻辑,两套并行。第二步,新写的功能模块直接用 WebSocketStream,把背压这件事交给标准流来处理,自己不再维护那个手动控速的队列。
Chrome 124 到 154 这三十个版本里,WebSocket 这条十几年的老通道终于和 Streams API 打通了。背压这件事从今以后不需要自己造轮子。
评论区
登录后可评论。