← 使用场景

带“正在输入”提示与在线状态的聊天

客服聊天、买卖双方消息、团队聊天室:消息立即送达、浏览器之间互传“正在输入…”提示,以及始终准确的在线名单。

聊天是用户最能感受到延迟的地方。晚一秒才出现的消息、永远不出现的“输入中…”提示,都会让产品看起来像没人在顾。客服聊天、买卖双方的消息、团队聊天室和游戏内聊天,需要的都是同样三件事:立即送达的消息、输入中提示,以及诚实的在线名单。

用户得到什么

  • 消息一送出,聊天室里每个人都立刻看到,不必刷新。
  • 有人在写回复时,显示“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 });

小提醒

  • Presence 频道最多 100 名成员。直播聊天这类更大的聊天室,请看实时拍卖与大量观众。
  • 在浏览器端节流客户端事件:每个连接都有速率限制,而且每个事件都算一条消息。
  • 要通知已经离开聊天室的人,搭配实时站内通知与推送使用。