題干與適用場景
一個即時協作頁面需要接收高頻事件並向伺服器傳送編輯操作。請評估 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。