Cộng tác: ai đang ở đây, con trỏ trực tiếp và khóa chỉnh sửa
Hiện ảnh đại diện của mọi người đang xem cùng tài liệu hoặc phiếu, chia sẻ con trỏ và vị trí đang sửa, và phát các thay đổi đã lưu để không ai ghi đè lên người khác.
Khi hai người cùng sửa một bản ghi mà không biết, công sức của một người sẽ mất. Hiển thị ai khác đang xem cùng tài liệu, phiếu hỗ trợ hay bản thiết kế, và họ đang ở đâu, giúp tránh điều đó và khiến sản phẩm có cảm giác làm việc cùng nhau. Công cụ quản lý dự án, CRM, hệ thống hỗ trợ, bảng trắng và trình tạo biểu mẫu đều hưởng lợi.
Người dùng nhận được gì
- Ảnh đại diện của mọi người đang xem cùng trang, cập nhật khi có người đến và đi.
- Con trỏ trực tiếp, hoặc đánh dấu ô mà ai đó đang sửa.
- Khóa rõ ràng “Ann đang sửa phiếu này” thay vì hai lần lưu xung đột.
- Thay đổi của người khác hiện ngay tại chỗ, không cần tải lại.
Dùng những gì
- Kênh presence (
presence-), mỗi tài liệu một kênh, danh sách thành viên chính xác trên mọi node. - Sự kiện phía client (
client-*) cho vị trí con trỏ và ô đang sửa, gửi thẳng giữa các trình duyệt. - Sự kiện từ máy chủ cho thay đổi đã lưu, phát sau khi máy chủ của bạn đã lưu.
- Kênh presence lớn (
presence-large-) khi một trang có thể có hơn 100 người xem cùng lúc.
Bước 1: hiện ai đang ở đây
Cấp quyền presence-doc-<id> giống như cách bạn kiểm tra quyền truy cập tài liệu, và trả về tên cùng ảnh đại diện trong user_info (route xem ở chat và chỉ báo đang nhập). Sau đó:
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);
});Người dùng mở trang ở hai tab chỉ tính một lần: được thêm với socket đầu tiên và bị xóa khi socket cuối cùng đóng.
Bước 2: chia sẻ con trỏ và ô đang sửa
Bật Cho phép sự kiện phía client trong cài đặt ứng dụng. Gửi vị trí tối đa vài lần mỗi giây, và chỉ khi có thay đổi:
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)));Trên kênh presence, tham số thứ hai của hàm xử lý sự kiện phía client chứa user_id của người gửi.
Bước 3: phát các thay đổi đã lưu
Khi ai đó lưu, hãy lưu thay đổi trước rồi mới phát cho những người khác:
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);
});Những người xem khác áp dụng bản vá, hoặc tải lại bản ghi khi phiên bản của họ bị tụt lại.
Hơn 100 người xem
Kênh presence chứa tối đa 100 thành viên, vì ai cũng nhận toàn bộ danh sách. Với trang có nhiều người xem, như thông báo toàn công ty hay sự kiện trực tiếp, hãy dùng presence-large-doc-<id> cùng @hibira-io/large-presence: chứa tới 100.000 thành viên và gửi số lượng cùng một mẫu danh sách thay vì từng lượt vào. Ở đó không cho phép sự kiện phía client, nên chỉ chia sẻ con trỏ trong các phòng nhỏ.
Mẹo
- Mỗi sự kiện phía client tính là một tin nhắn. Mười người sửa, mỗi người gửi con trỏ năm lần mỗi giây, là 50 tin nhắn mỗi giây, nên chỉ gửi khi thay đổi và giảm tần suất khi tab bị ẩn.
- Giữ dữ liệu gốc trên máy chủ. Sự kiện phía client dành cho các tín hiệu như con trỏ và ô đang sửa; chúng không được lưu và không bao giờ gửi lại cho người gửi.
- Để cộng tác soạn thảo văn bản đầy đủ, gửi các thao tác của thư viện CRDT như Yjs hoặc Automerge qua máy chủ theo cùng cách.