私人訊息 - 第 1 部分
在本指南中,我們將建立以下應用程式
我們將涵蓋下列主題
先備條件
本指南分為四個不同的部分
開始吧!
安裝
首先,讓我們取得聊天應用程式的初始實作
git clone https://github.com/socketio/socket.io.git
cd socket.io/examples/private-messaging
git checkout examples/private-messaging-part-1
以下是您應該在目前目錄中看到的內容
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── fonts
│ │ └── Lato-Regular.ttf
│ └── index.html
├── README.md
├── server
│ ├── index.js
│ ├── package.json
└── src
├── App.vue
├── components
│ ├── Chat.vue
│ ├── MessagePanel.vue
│ ├── SelectUsername.vue
│ ├── StatusIcon.vue
│ └── User.vue
├── main.js
└── socket.js
前端程式碼位於 src
目錄中,而伺服器程式碼位於 server
目錄中。
執行前端
此專案是一個基本的 Vue.js 應用程式,使用 @vue/cli
建立。
執行方式
npm install
npm run serve
然後,如果您在瀏覽器中開啟 http://localhost:8080,您應該會看到
執行伺服器
現在,讓我們啟動伺服器
cd server
npm install
npm start
您的主控台應列印
server listening at http://localhost:3000
到目前為止,一切順利!您應該可以開啟多個標籤頁,並在它們之間傳送一些訊息

運作方式
伺服器初始化
Socket.IO 伺服器在 server/index.js
檔案中初始化
const httpServer = require("http").createServer();
const io = require("socket.io")(httpServer, {
cors: {
origin: "http://localhost:8080",
},
});
在此,我們建立一個 Socket.IO 伺服器,並將其附加到 Node.js HTTP 伺服器。
文件
需要 cors
設定,以便前端(在 http://localhost:8080
執行)傳送的 HTTP 要求可以到達伺服器(在 http://localhost:3000
執行,因此我們處於跨來源情況)。
文件
- 跨來源資源分享 (CORS)
- Socket.IO CORS 設定
用戶端初始化
Socket.IO 用戶端在 src/socket.js
檔案中初始化
import { io } from "socket.io-client";
const URL = "http://localhost:3000";
const socket = io(URL, { autoConnect: false });
export default socket;
autoConnect
設定為 false
,因此不會立即建立連線。我們稍後會手動呼叫 socket.connect()
,也就是在使用者選擇使用者名稱之後。
我們也註冊了一個 萬用監聽器,這在開發期間非常有用
socket.onAny((event, ...args) => {
console.log(event, args);
});
因此,用戶端收到的任何事件都會列印在主控台中。
選擇使用者名稱
現在,讓我們移到 src/App.vue
應用程式以 usernameAlreadySelected
設定為 false
啟動,因此會顯示選擇使用者名稱的表單
提交表單後,我們將到達 onUsernameSelection
方法
onUsernameSelection(username) {
this.usernameAlreadySelected = true;
socket.auth = { username };
socket.connect();
}
我們在 auth
物件中附加 username
,然後呼叫 socket.connect()
。
如果您在開發人員工具中開啟網路標籤頁,您應該會看到一些 HTTP 要求
- Engine.IO 交握(包含會話 ID — 在這裡是
zBjrh...AAAK
— 用於後續要求) - Socket.IO 交握要求(包含
auth
選項的值) - Socket.IO 握手回應(包含 Socket#id)
- WebSocket 連線
- 第一個 HTTP 長輪詢請求,在建立 WebSocket 連線後關閉
如果您看到這則訊息,表示連線已成功建立。
在伺服器端,我們註冊一個中介軟體,用來檢查使用者名稱並允許連線
io.use((socket, next) => {
const username = socket.handshake.auth.username;
if (!username) {
return next(new Error("invalid username"));
}
socket.username = username;
next();
});
username
會新增為 socket
物件的屬性,以便稍後重複使用。您可以附加任何屬性,只要不覆寫現有的屬性,例如 socket.id
或 socket.handshake
。
文件
在用戶端(src/App.vue
),我們為 connect_error
事件新增一個處理常式
socket.on("connect_error", (err) => {
if (err.message === "invalid username") {
this.usernameAlreadySelected = false;
}
});
connect_error
事件會在連線失敗時發出
- 由於低層級錯誤(例如伺服器當機)
- 由於中介軟體錯誤
請注意,在上述函式中,低層級錯誤並未處理(例如,可以通知使用者連線失敗)。
最後一個注意事項:connect_error
的處理常式會在 destroyed 掛勾中移除
destroyed() {
socket.off("connect_error");
}
因此,當元件被銷毀時,由我們的 App
元件註冊的監聽器會被清除。
列出所有使用者
連線後,我們會將所有現有使用者傳送給用戶端
io.on("connection", (socket) => {
const users = [];
for (let [id, socket] of io.of("/").sockets) {
users.push({
userID: id,
username: socket.username,
});
}
socket.emit("users", users);
// ...
});
我們正在迴圈處理 io.of("/").sockets
物件,它是所有目前已連線 Socket 實例的 Map,索引為 ID。
這裡有兩點說明
- 我們使用
socket.id
作為我們應用程式的使用者 ID - 我們只擷取目前 Socket.IO 伺服器的使用者(不適用於擴充時)
我們稍後會再回來討論這一點。
在用戶端(src/components/Chat.vue
),我們為 users
事件註冊一個處理常式
socket.on("users", (users) => {
users.forEach((user) => {
user.self = user.userID === socket.id;
initReactiveProperties(user);
});
// put the current user first, and then sort by username
this.users = users.sort((a, b) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
});
我們也會通知現有使用者
伺服器
io.on("connection", (socket) => {
// notify existing users
socket.broadcast.emit("user connected", {
userID: socket.id,
username: socket.username,
});
});
socket.broadcast.emit("user connected", ...)
會發送給所有已連線的用戶端,除了 socket
本身。
另一種廣播形式 io.emit("user connected", ...)
會將「使用者已連線」事件傳送給所有已連線的用戶端,包括新使用者。
文件:廣播事件
用戶端
socket.on("user connected", (user) => {
initReactiveProperties(user);
this.users.push(user);
});
使用者清單顯示於左側面板
私人訊息
當選擇特定使用者時,右側面板會顯示聊天視窗
以下是私人訊息的實作方式
用戶端(傳送者)
onMessage(content) {
if (this.selectedUser) {
socket.emit("private message", {
content,
to: this.selectedUser.userID,
});
this.selectedUser.messages.push({
content,
fromSelf: true,
});
}
}
伺服器
socket.on("private message", ({ content, to }) => {
socket.to(to).emit("private message", {
content,
from: socket.id,
});
});
在此,我們使用房間的概念。這些是 Socket 實例可以加入和離開的頻道,並且可以廣播給房間中的所有用戶端。
我們依賴 Socket 實例自動加入由其 ID 識別的房間(為您呼叫 socket.join(socket.id)
)。
因此,socket.to(to).emit("private message", ...)
會發射至指定的使用者 ID。
用戶端(接收者)
socket.on("private message", ({ content, from }) => {
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
if (user.userID === from) {
user.messages.push({
content,
fromSelf: false,
});
if (user !== this.selectedUser) {
user.hasNewMessages = true;
}
break;
}
}
});
連線狀態
在用戶端,Socket 實例會發出兩個特殊事件
connect
:連線或重新連線時disconnect
:斷線時
這些事件可用於追蹤連線狀態(在 src/components/Chat.vue
中)
socket.on("connect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = true;
}
});
});
socket.on("disconnect", () => {
this.users.forEach((user) => {
if (user.self) {
user.connected = false;
}
});
});
您可以透過停止伺服器來測試

回顧
好的,所以...我們目前為止所擁有的很棒,但有一個明顯的問題

說明:重新連線時會產生新的 Socket ID,因此每次使用者斷線並重新連線時,都會取得新的使用者 ID。
這就是我們需要持久使用者 ID 的原因,這是本指南第 2 部分的主題。
感謝您的閱讀!