← กรณีการใช้งาน

แชตพร้อมสถานะกำลังพิมพ์และสถานะออนไลน์

แชตฝ่ายบริการลูกค้า ข้อความระหว่างผู้ซื้อผู้ขาย และห้องแชตทีม: ข้อความมาถึงทันที สถานะ “กำลังพิมพ์…” ระหว่างเบราว์เซอร์ และรายชื่อผู้ออนไลน์ที่ถูกต้องเสมอ

แชตคือที่ที่ผู้ใช้รู้สึกถึงความหน่วงมากที่สุด ข้อความที่ขึ้นช้าไปหนึ่งวินาที หรือสถานะ “กำลังพิมพ์…” ที่ไม่เคยขึ้น ทำให้ผลิตภัณฑ์ดูเหมือนไม่มีใครดูแล แชตฝ่ายบริการลูกค้า ข้อความระหว่างผู้ซื้อกับผู้ขาย ห้องแชตทีม และแชตในเกม ต้องการสามอย่างเหมือนกัน คือ ข้อความที่มาถึงทันที สถานะกำลังพิมพ์ และรายชื่อผู้ออนไลน์ที่ตรงความจริง

สิ่งที่ผู้ใช้ได้รับ

  • ข้อความขึ้นให้ทุกคนในห้องเห็นทันทีที่ส่ง ไม่ต้องรีโหลด
  • “Ann กำลังพิมพ์…” ระหว่างที่มีคนเขียนตอบ
  • จุดสีเขียวข้างชื่อคนที่อยู่ในบทสนทนาตอนนี้

ฟีเจอร์ที่ใช้

  • ช่อง presence (presence-) รักษารายชื่อสมาชิกในห้องให้ถูกต้องบนทุกโหนด
  • อีเวนต์ฝั่งไคลเอนต์ (client-*, whisper ของ Laravel Echo) ส่งสถานะกำลังพิมพ์จากเบราว์เซอร์ถึงเบราว์เซอร์โดยไม่ผ่านเซิร์ฟเวอร์ของคุณ
  • การยกเว้นซ็อกเก็ต (socket_id, toOthers() ของ Laravel) ผู้ส่งจะไม่ได้รับข้อความของตัวเองซ้ำ
  • 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 คน สำหรับห้องที่ใหญ่กว่า เช่น แชตในไลฟ์สตรีม ดูที่การประมูลสดและผู้ชมจำนวนมาก
  • จำกัดความถี่ของอีเวนต์ฝั่งไคลเอนต์ในเบราว์เซอร์ เพราะแต่ละการเชื่อมต่อมีขีดจำกัดอัตรา และแต่ละอีเวนต์นับเป็นหนึ่งข้อความ
  • หากต้องการแจ้งคนที่ออกจากห้องไปแล้ว ให้ใช้ร่วมกับการแจ้งเตือนในแอปและพุช