從 3.x 遷移到 4.0
4.0.0 版本新增許多新功能,詳細內容請見下方,但它也包含一些 API 中斷變更(因此版本號大幅跳升)。
請注意,這些中斷變更只會影響伺服器端的 API。Socket.IO 通訊協定本身並未更新,因此 v3 用戶端可以連線到 v4 伺服器,反之亦然。此外,相容模式 (allowEIO3: true
) 仍可供 Socket.IO v2 用戶端和 Socket.IO v4 伺服器之間使用。
以下是變更的完整清單
重大變更
io.to()
現在不可變
先前,廣播至特定房間(透過呼叫 io.to()
)會變更 io 執行個體,這可能會導致令人驚訝的行為,例如
io.to("room1");
io.to("room2").emit(/* ... */); // also sent to room1
// or with async/await
io.to("room3").emit("details", await fetchDetails()); // random behavior: maybe in room3, maybe to all clients
呼叫 io.to()
(或任何其他廣播修改器)現在會傳回不可變執行個體。
範例
const operator1 = io.to("room1");
const operator2 = operator1.to("room2");
const operator3 = socket.broadcast;
const operator4 = socket.to("room3").to("room4");
operator1.emit(/* ... */); // only to clients in "room1"
operator2.emit(/* ... */); // to clients in "room1" or in "room2"
operator3.emit(/* ... */); // to all clients but the sender
operator4.emit(/* ... */); // to clients in "room3" or in "room4" but the sender
wsEngine
選項
wsEngine
選項的格式已更新,以移除下列錯誤
重大依賴項:依賴項的要求為表達式
在使用 webpack 綑綁伺服器時。
先前
const io = require("socket.io")(httpServer, {
wsEngine: "eiows"
});
之後
const io = require("socket.io")(httpServer, {
wsEngine: require("eiows").Server
});
組態
確保與 Swift v15 客戶端相容
在版本 16.0.0 之前,Swift 客戶端不會在 HTTP 要求中包含 EIO
查詢參數,而 Socket.IO v3 伺服器會預設推論 EIO=4
。
這就是為什麼 Swift 客戶端 v15 無法連線至伺服器,即使已啟用相容模式 (allowEIO3: true
),除非您明確指定查詢參數
let manager = SocketManager(socketURL: URL(string: "http://localhost:8080")!, config: [
.log(true),
.connectParams(["EIO": "3"])
])
let socket = manager.defaultSocket
如果未包含 EIO
查詢參數,Socket.IO v4 伺服器現在會推論 EIO=3
。
pingTimeout
的預設值已增加
pingTimeout
(用於心跳機制)的預設值已在 socket.io@2.1.0
(2018 年 3 月)中從 60000 更新為 5000。
當時的理由
有些使用者在伺服器端和客戶端斷線之間遇到長時間延遲。「斷線」事件會花很長時間才能在瀏覽器中觸發,這可能是因為計時器延遲。因此進行了變更。
話雖如此,當透過速度較慢的網路傳送大型酬載時,目前的數值(5 秒)會導致意外斷線,因為它會阻止在客戶端和伺服器之間交換 ping-pong 封包。當同步任務封鎖伺服器超過 5 秒時,也可能會發生這種情況。
因此,新值 (20 秒) 似乎在快速中斷偵測和容忍各種延遲之間取得良好的平衡。
新功能
廣播時允許排除特定房間
感謝 Sebastiaan Marynissen 的傑出貢獻,現在您可以在廣播時排除特定房間
io.except("room1").emit(/* ... */); // to all clients except the ones in "room1"
io.to("room2").except("room3").emit(/* ... */); // to all clients in "room2" except the ones in "room3"
socket.broadcast.except("room1").emit(/* ... */); // to all clients except the ones in "room1" and the sender
socket.except("room1").emit(/* ... */); // same as above
socket.to("room4").except("room5").emit(/* ... */); // to all clients in "room4" except the ones in "room5" and the sender
允許將陣列傳遞給 io.to()
to()
方法現在接受房間陣列。
先前
const rooms = ["room1", "room2", "room3"];
for (const room of rooms) {
io.to(room);
}
// broadcast to clients in "room1", "room2" or "room3"
// WARNING !!! this does not work anymore in v4, see the breaking change above
io.emit(/* ... */);
之後
io.to(["room1", "room2", "room3"]).emit(/* ... */);
socket.to(["room1", "room2", "room3"]).emit(/* ... */);
其他實用方法
新增一些(期待已久)的方法
socketsJoin
:讓符合條件的 Socket 實例加入指定的房間
// make all Socket instances join the "room1" room
io.socketsJoin("room1");
// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
socketsLeave
:讓符合條件的 Socket 實例離開指定的房間
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");
// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
disconnectSockets
:讓符合條件的 Socket 實例中斷連線
// make all Socket instances disconnect
io.disconnectSockets();
// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
// this also works with a single socket ID
io.of("/admin").in(theSocketId).disconnectSockets();
fetchSockets
:傳回符合條件的 Socket 實例
// return all Socket instances of the main namespace
const sockets = await io.fetchSockets();
// return all Socket instances of the "admin" namespace in the "room1" room
const sockets = await io.of("/admin").in("room1").fetchSockets();
// this also works with a single socket ID
const sockets = await io.in(theSocketId).fetchSockets();
上述範例中的 sockets
變數是物件陣列,會顯示 Socket 類別的子集
for (const socket of sockets) {
console.log(socket.id);
console.log(socket.handshake);
console.log(socket.rooms);
socket.emit(/* ... */);
socket.join(/* ... */);
socket.leave(/* ... */);
socket.disconnect(/* ... */);
}
這些方法與廣播具有相同的語意,並套用相同的篩選器
io.of("/admin").in("room1").except("room2").local.disconnectSockets();
這會讓「admin」命名空間的所有 Socket 實例
- 在「room1」房間(
in("room1")
或to("room1")
)中 - 除了「room2」中的實例(
except("room2")
) - 而且僅在目前的 Socket.IO 伺服器(
local
)上
中斷連線。
類型化事件
感謝 Maxime Kjaer 的傑出貢獻,現在 TypeScript 使用者可以設定在用戶端和伺服器之間傳送的事件類型。
首先,宣告每個事件的簽章
interface ClientToServerEvents {
noArg: () => void;
basicEmit: (a: number, b: string, c: number[]) => void;
}
interface ServerToClientEvents {
withAck: (d: string, cb: (e: number) => void) => void;
}
現在您可以在用戶端使用它們
import { io, Socket } from "socket.io-client";
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();
socket.emit("noArg");
socket.emit("basicEmit", 1, "2", [3]);
socket.on("withAck", (d, cb) => {
cb(4);
});
您的 IDE 現在應該可以正確推斷每個參數的類型
在伺服器端也是如此(ServerToClientEvents
和 ClientToServerEvents
相反)
import { Server } from "socket.io";
const io = new Server<ClientToServerEvents, ServerToClientEvents>(3000);
io.on("connection", (socket) => {
socket.on("noArg", () => {
// ...
});
socket.on("basicEmit", (a, b, c) => {
// ...
});
socket.emit("withAck", "42", (e) => {
console.log(e);
});
});
預設情況下,事件沒有類型,參數會推斷為 any
。
autoUnref
選項
最後,感謝 KC Erb 的傑出貢獻,新增了 autoUnref
選項。
將 autoUnref
設為 true(預設值:false),如果事件系統中沒有其他 active timer/TCP socket(即使用戶端已連線),Socket.IO 用戶端將允許程式結束。
const socket = io({
autoUnref: true
});
注意:此選項僅適用於 Node.js 用戶端。
已知遷移問題
無法取得未定義的 emit
下列表達式
socket.to("room1").broadcast.emit(/* ... */);
在 Socket.IO v3 中運作正常,但現在被視為無效,因為 broadcast
旗標無用,因為 to("room1")
方法已將 Socket 實例置於廣播模式。
// VALID
socket.broadcast.emit(/* ... */); // to all clients but the sender
socket.to("room1").emit(/* ... */); // to clients in "room1" but the sender
// VALID (but useless 'broadcast' flag)
socket.broadcast.to("room1").emit(/* ... */);
// INVALID
socket.to("room1").broadcast.emit(/* ... */);