有「輸入中」提示與上線狀態的聊天
客服聊天、買賣雙方訊息、團隊聊天室:訊息立即送達、瀏覽器之間互傳「輸入中…」提示,以及始終正確的上線名單。
聊天是使用者最能感受到延遲的地方。晚一秒才出現的訊息、永遠不出現的「輸入中…」提示,都會讓產品看起來像沒人在顧。客服聊天、買賣雙方的訊息、團隊聊天室和遊戲內聊天,需要的都是同樣三件事:立即送達的訊息、輸入中提示,以及誠實的上線名單。
使用者得到什麼
- 訊息一送出,聊天室裡每個人都立刻看到,不必重新整理。
- 有人在寫回覆時,顯示「Ann 正在輸入…」。
- 此刻在對話裡的人,名字旁邊亮綠燈。
用到的功能
- Presence 頻道(
presence-),在每個節點上都保持正確的聊天室成員名單。 - 用戶端事件(
client-*,Laravel Echo 的whisper),輸入中提示直接在瀏覽器之間傳遞,不經過你的伺服器。 - 排除發送者連線(
socket_id,Laravel 的toOthers()),發送者不會重複收到自己的訊息。 - Webhook(
member_added、member_removed),在伺服器需要對成員進出做出反應時使用。
步驟 1:授權聊天室
只有對話的成員才能加入它的頻道。由你的授權路由決定,並回傳成員名單要顯示的使用者資料:
app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
const { socket_id, channel_name } = req.body;
const room = channel_name.replace(/^presence-room-/, '');
if (!(await db.rooms.isMember(room, req.user.id))) return res.sendStatus(403);
res.json(pusher.authorizeChannel(socket_id, channel_name, {
user_id: String(req.user.id),
user_info: { name: req.user.name, avatar: req.user.avatarUrl },
}));
});步驟 2:加入、列出成員並顯示輸入中
在應用程式設定裡開啟 允許用戶端事件,然後:
const room = pusher.subscribe('presence-room-42');
room.bind('pusher:subscription_succeeded', (members) => {
members.each((m) => online.add(m.id, m.info));
});
room.bind('pusher:member_added', (m) => online.add(m.id, m.info));
room.bind('pusher:member_removed', (m) => online.remove(m.id));
// Typing: at most one event every 2 seconds while the user writes
let last = 0;
input.addEventListener('input', () => {
if (Date.now() - last < 2000) return;
last = Date.now();
room.trigger('client-typing', { name: me.name });
});
room.bind('client-typing', ({ name }) => typing.show(name, 3000));
room.bind('message', (m) => messages.append(m));步驟 3:透過伺服器發送訊息
訊息經過你的伺服器,先儲存、審核,再廣播。帶上發送者的 socket ID,發送者那個已經顯示訊息的分頁就會被略過:
app.post('/rooms/:id/messages', requireLogin, async (req, res) => {
const msg = await db.messages.create({ room: req.params.id, user: req.user.id, text: req.body.text });
await pusher.trigger('presence-room-' + req.params.id, 'message', msg, { socket_id: req.body.socket_id });
res.json(msg);
});瀏覽器在請求裡附上 pusher.connection.socket_id。
Laravel 的寫法
// routes/channels.php
Broadcast::channel('room.{id}', function ($user, $id) {
return $user->rooms()->whereKey($id)->exists() ? ['id' => $user->id, 'name' => $user->name] : false;
});
// In the controller
broadcast(new MessageSent($message))->toOthers();Echo.join(`room.${roomId}`)
.here((users) => online.set(users))
.joining((user) => online.add(user))
.leaving((user) => online.remove(user))
.listen('MessageSent', (e) => messages.append(e.message))
.listenForWhisper('typing', (e) => typing.show(e.name, 3000));
Echo.join(`room.${roomId}`).whisper('typing', { name: me.name });