blog.dopana

Back

Xây dựng các ứng dụng cộng tác thời gian thực như Google Docs, Figma hay Notion từng là một trong những thử thách kỹ thuật khó khăn nhất trong phát triển web. Việc xử lý độ trễ mạng, mất kết nối và giải quyết xung đột khi nhiều người dùng cùng gõ chữ tại một vị trí đòi hỏi các thuật toán vô cùng phức tạp.

Ngày nay, các thư viện như Yjs đã giúp nhiệm vụ này trở nên dễ dàng hơn rất nhiều. Hãy cùng tìm hiểu Yjs là gì, cách nó hoạt động dưới lớp vỏ bọc và tại sao nó lại là công cụ hàng đầu cho các ứng dụng cộng tác hiện đại.

Yjs là gì? (ELI5)#

Hãy tưởng tượng bạn và một người bạn đang cùng viết một câu chuyện trên một cuốn sổ tay cùng một lúc.

Trong các hệ thống cộng tác truyền thống (như Google Docs, sử dụng công nghệ Operational Transformation / OT), bạn cần một giáo viên (một máy chủ trung tâm) đứng ở giữa. Khi bạn viết một từ, bạn phải đưa giáo viên xem trước. Giáo viên quyết định ai viết trước, điều chỉnh vị trí chữ của các bạn và gửi lại kết quả cuối cùng cho cả hai. Nếu giáo viên đi vắng (máy chủ ngoại tuyến), các bạn không thể viết tiếp được nữa.

Yjs sử dụng CRDTs (Conflict-free Replicated Data Types), hoạt động hoàn toàn không cần giáo viên:

  • Mỗi chữ cái bạn viết ra đều được dán một nhãn dán tàng hình độc nhất (ID) và ghi lại hàng xóm của nó (“chữ này nằm ngay sau chữ X”).
  • Bạn và bạn của mình có thể viết bất kỳ thứ gì mình muốn, ngay cả khi đang ở các phòng khác nhau (ngoại tuyến).
  • Khi gặp lại nhau, các bạn chỉ cần so khớp các nhãn dán và hàng xóm để ghép các trang giấy lại. Vì các quy tắc gộp này mang tính toán học và xác định, cuốn sổ của cả hai sẽ tự động hiển thị kết quả giống hệt nhau mà không hề xảy ra xung đột!

Kiến trúc cốt lõi và Quy trình hoạt động#

Yjs hoạt động như một mô hình dữ liệu độc lập với mạng. Nó duy trì trạng thái tài liệu cục bộ (Y.Doc) và chuyển đổi các cập nhật thành các gói nhị phân nhỏ để gửi qua bất kỳ giao thức nào (WebSockets, WebRTC hay thậm chí là Email).

sequenceDiagram
    participant A as Client A
    participant M as Mạng WebSocket
    participant B as Client B
    Note over A: ydoc.getMap().set('x', 1)
    A->>M: encodeStateAsUpdate (nhị phân)
    Note over B: ydoc.getMap().set('y', 2)
    B->>M: encodeStateAsUpdate (nhị phân)
    M->>B: Gửi cập nhật của Client A
    Note over B: Y.applyUpdate() - Tu dong gop trang thai
    M->>A: Gửi cập nhật của Client B
    Note over A: Y.applyUpdate() - Tu dong gop trang thai

1. Các kiểu dữ liệu dùng chung (Shared Types)#

Yjs cung cấp các cấu trúc dữ liệu dùng chung hoạt động giống như các đối tượng JavaScript thông thường nhưng tự động đồng bộ hóa:

  • Y.Text: Dùng cho các trình soạn thảo văn bản cộng tác.
  • Y.Array: Danh sách dùng chung.
  • Y.Map: Bảng ánh xạ key-value dùng chung.
  • Y.XmlFragment: Dùng cho các cấu trúc dạng cây của văn bản đa định dạng (rich-text).

2. Provider kết nối (Connection Providers)#

Yjs độc lập với hạ tầng mạng. Bạn chỉ cần cắm các Provider để xử lý truyền thông tin:

  • y-websocket: Đồng bộ hóa máy chủ-máy khách tiêu chuẩn.
  • y-webrtc: Đồng bộ hóa ngang hàng (P2P) trực tiếp giữa các trình duyệt mà không cần máy chủ trung gian.
  • y-p2p / y-matrix: Các giao thức phi tập trung khác.

3. Provider cơ sở dữ liệu (Database Providers)#

Để đảm bảo người dùng không bị mất dữ liệu khi đóng tab trình duyệt, các persistence provider sẽ lưu trữ lịch sử cập nhật:

  • y-indexeddb: Lưu trữ trên trình duyệt của người dùng để hỗ trợ chỉnh sửa ngoại tuyến (offline).
  • y-leveldb / y-redis: Các adapter kết nối cơ sở dữ liệu trên máy chủ.

Nguyên lý hoạt động của Yjs (Thuật toán YATA)#

Yjs dựa trên thuật toán YATA (Yet Another Transformation Approach). Nó đảm bảo sự đồng thuận và giữ nguyên ý đồ của người viết thông qua các cơ chế:

  • ID Độc nhất Bất biến: Mỗi phần tử được chèn vào (như một ký tự) sẽ có một Client ID duy nhất kèm theo bộ đếm giao dịch cục bộ.
  • Mỏ neo nguồn gốc (Origin Anchors): Mỗi ký tự sẽ ghi nhớ ký tự bên trái (origin) và ký tự bên phải (originRight) tại thời điểm nó được chèn vào. Dù văn bản xung quanh có thay đổi, ký tự đó vẫn được neo đúng ngữ cảnh.
  • Mã hóa Run-Length (RLE): Để tránh việc phình to bộ nhớ do siêu dữ liệu (metadata), Yjs gộp các thao tác chèn liên tiếp từ cùng một client thành một khối cấu trúc duy nhất. Điều này giúp Yjs nhanh và tiết kiệm RAM vượt trội so với các thư viện CRDT khác.

Ví dụ mã nguồn nhanh#

Dưới đây là cách sử dụng Yjs để đồng bộ các cấu trúc dữ liệu cơ bản:

Tài liệu tham khảo#