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

การทำงานร่วมกัน: ใครอยู่ที่นี่ เคอร์เซอร์สด และการล็อกการแก้ไข

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

เมื่อสองคนแก้ไขข้อมูลเดียวกันโดยไม่รู้ตัว งานของคนใดคนหนึ่งจะหายไป การแสดงว่าใครกำลังดูเอกสาร ทิกเก็ต หรืองานออกแบบเดียวกันอยู่ และดูอยู่ตรงไหน ช่วยป้องกันเรื่องนี้ และทำให้ผลิตภัณฑ์รู้สึกเหมือนได้ทำงานด้วยกัน เครื่องมือจัดการโปรเจกต์ CRM ระบบ help desk ไวต์บอร์ด และตัวสร้างฟอร์มได้ประโยชน์ทั้งหมด

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

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

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

  • ช่อง presence (presence-) หนึ่งช่องต่อเอกสาร พร้อมรายชื่อสมาชิกที่ถูกต้องบนทุกโหนด
  • อีเวนต์ฝั่งไคลเอนต์ (client-*) ส่งตำแหน่งเคอร์เซอร์และโฟกัสจากเบราว์เซอร์ถึงเบราว์เซอร์
  • อีเวนต์จากเซิร์ฟเวอร์ สำหรับการเปลี่ยนแปลงที่บันทึกแล้ว เผยแพร่หลังจากเซิร์ฟเวอร์ของคุณบันทึกเสร็จ
  • ช่อง presence ขนาดใหญ่ (presence-large-) เมื่อหน้าหนึ่งอาจมีผู้ชมพร้อมกันเกิน 100 คน

ขั้นที่ 1: แสดงว่าใครอยู่ที่นี่

ยืนยันสิทธิ์ presence-doc-<id> ด้วยเงื่อนไขเดียวกับการเข้าถึงเอกสาร และส่งชื่อกับอวาตาร์กลับเป็น user_info (ดูเส้นทางได้ที่แชตและสถานะกำลังพิมพ์) จากนั้น:

const doc = pusher.subscribe('presence-doc-' + docId);

doc.bind('pusher:subscription_succeeded', (members) => avatars.render(members));
doc.bind('pusher:member_added', () => avatars.render(doc.members));
doc.bind('pusher:member_removed', (m) => {
  avatars.render(doc.members);
  cursors.remove(m.id);
});

ผู้ใช้ที่เปิดหน้าไว้สองแท็บนับเป็นหนึ่งคน ถูกเพิ่มเมื่อซ็อกเก็ตแรกเข้า และถูกลบเมื่อซ็อกเก็ตสุดท้ายปิด

ขั้นที่ 2: แชร์เคอร์เซอร์และโฟกัส

เปิด อนุญาตอีเวนต์ฝั่งไคลเอนต์ ในการตั้งค่าแอป ส่งตำแหน่งไม่เกินไม่กี่ครั้งต่อวินาที และเฉพาะเมื่อมีการเปลี่ยนแปลง:

let pending = null;
document.addEventListener('pointermove', (e) => {
  pending = { x: e.pageX / document.body.scrollWidth, y: e.pageY };
});
setInterval(() => {
  if (!pending) return;
  doc.trigger('client-cursor', pending);
  pending = null;
}, 200);

doc.bind('client-cursor', (pos, meta) => cursors.move(meta.user_id, pos));

// Field locks: who is editing what
field.addEventListener('focus', () => doc.trigger('client-focus', { field: field.name }));
doc.bind('client-focus', ({ field }, meta) => locks.show(field, doc.members.get(meta.user_id)));

บนช่อง presence อาร์กิวเมนต์ที่สองของตัวจัดการอีเวนต์ฝั่งไคลเอนต์มี user_id ของผู้ส่ง

ขั้นที่ 3: กระจายการเปลี่ยนแปลงที่บันทึกแล้ว

เมื่อมีคนบันทึก ให้เก็บการเปลี่ยนแปลงก่อน แล้วจึงเผยแพร่ให้คนอื่น:

app.patch('/docs/:id', requireLogin, async (req, res) => {
  const change = await db.docs.apply(req.params.id, req.user.id, req.body.patch);
  await pusher.trigger('presence-doc-' + req.params.id, 'changed', { version: change.version, patch: req.body.patch, by: req.user.id }, { socket_id: req.body.socket_id });
  res.json(change);
});

ผู้ชมคนอื่นนำแพตช์ไปใช้ หรือโหลดข้อมูลใหม่หากเวอร์ชันของตนตามหลังอยู่

ผู้ชมมากกว่า 100 คน

ช่อง presence รับสมาชิกได้สูงสุด 100 คน เพราะทุกคนได้รับรายชื่อทั้งหมด สำหรับหน้าที่มีคนดูจำนวนมาก เช่น ประกาศทั้งบริษัทหรืออีเวนต์สด ให้ใช้ presence-large-doc-<id> กับ @hibira-io/large-presence ซึ่งรับได้ถึง 100,000 คน และส่งจำนวนคนกับตัวอย่างรายชื่อแทนอีเวนต์ทุกครั้งที่มีคนเข้า ที่นั่นใช้อีเวนต์ฝั่งไคลเอนต์ไม่ได้ จึงควรแชร์เคอร์เซอร์เฉพาะในห้องขนาดเล็ก

เคล็ดลับ

  • อีเวนต์ฝั่งไคลเอนต์ทุกอีเวนต์นับเป็นหนึ่งข้อความ ผู้แก้ไขสิบคนส่งเคอร์เซอร์วินาทีละห้าครั้งเท่ากับ 50 ข้อความต่อวินาที จึงควรส่งเมื่อมีการเปลี่ยนแปลง และลดความถี่เมื่อแท็บถูกซ่อน
  • เก็บข้อมูลหลักไว้บนเซิร์ฟเวอร์ อีเวนต์ฝั่งไคลเอนต์มีไว้สำหรับสัญญาณอย่างเคอร์เซอร์และโฟกัส ไม่ถูกบันทึก และไม่ถูกส่งกลับไปหาผู้ส่ง
  • สำหรับการแก้ไขข้อความร่วมกันเต็มรูปแบบ ให้ส่งการดำเนินการของไลบรารี CRDT เช่น Yjs หรือ Automerge ผ่านเซิร์ฟเวอร์ด้วยวิธีเดียวกัน