带“正在输入”提示与在线状态的聊天
客服聊天、买卖双方消息、团队聊天室:消息立即送达、浏览器之间互传“正在输入…”提示,以及始终准确的在线名单。
聊天是用户最能感受到延迟的地方。晚一秒才出现的消息、永远不出现的“输入中…”提示,都会让产品看起来像没人在顾。客服聊天、买卖双方的消息、团队聊天室和游戏内聊天,需要的都是同样三件事:立即送达的消息、输入中提示,以及诚实的在线名单。
用户得到什么
- 消息一送出,聊天室里每个人都立刻看到,不必刷新。
- 有人在写回复时,显示“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 });