跳至主要內容
版本:4.x

從 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 現在應該可以正確推斷每個參數的類型

在伺服器端也是如此(ServerToClientEventsClientToServerEvents 相反)

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(/* ... */);