Реалізація Real-Time спільного редагування на сайті (Yjs/Liveblocks)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Real-Time спільного редагування на сайті (Yjs/Liveblocks)
Складна
~2-4 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Real-Time спільного редактування (Yjs/Liveblocks)

Спільне редагування дозволяє кільком користувачам одночасно працювати з одним документом, видячи зміни друг друга у реальному часі. Google Docs-подібна функціональність для веб-додатків.

Вибір підходу

Yjs — open-source CRDT (Conflict-free Replicated Data Types) бібліотека. Керуйте сервером самостійно (y-websocket або Hocuspocus).

Liveblocks — managed платформа поверх Yjs. Без інфраструктури, але платно від $0.

Automerge — альтернатива Yjs, від Ink & Switch.

Yjs + Hocuspocus (self-hosted)

Сервер:

npm install @hocuspocus/server @hocuspocus/extension-database
import { Server } from '@hocuspocus/server';
import { Database } from '@hocuspocus/extension-database';
import { Logger } from '@hocuspocus/extension-logger';

const server = Server.configure({
  port: 1234,

  extensions: [
    new Logger(),
    new Database({
      fetch: async ({ documentName }) => {
        // Завантажити документ з БД при першому підключенні
        const doc = await documentRepo.findByName(documentName);
        return doc?.content ?? null;  // Yjs binary state
      },
      store: async ({ documentName, state }) => {
        // Зберігаємо стан документу в БД
        await documentRepo.upsert(documentName, state);
      }
    })
  ],

  async onAuthenticate({ token, documentName }) {
    // Перевірка доступу
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    const canAccess = await checkDocumentAccess(payload.sub, documentName);

    if (!canAccess) {
      throw new Error('Access denied');
    }

    return { userId: payload.sub };
  },

  async onConnect({ documentName, context }) {
    console.log(`User ${context.userId} connected to ${documentName}`);
  }
});

server.listen();

Клієнт — Tiptap редактор з Yjs:

import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import * as Y from 'yjs';
import { HocuspocusProvider } from '@hocuspocus/provider';

function CollaborativeEditor({ documentId }) {
  const doc = useMemo(() => new Y.Doc(), []);

  const provider = useMemo(() => new HocuspocusProvider({
    url: process.env.NEXT_PUBLIC_HOCUSPOCUS_URL,
    name: `doc:${documentId}`,
    document: doc,
    token: getAuthToken(),
    onStatus: ({ status }) => console.log('Provider status:', status)
  }), [documentId, doc]);

  const editor = useEditor({
    extensions: [
      StarterKit.configure({ history: false }),
      Collaboration.configure({ document: doc }),
      CollaborationCursor.configure({
        provider,
        user: {
          name: currentUser.name,
          color: generateUserColor(currentUser.id)
        }
      })
    ]
  });

  return (
    <div className="editor-container">
      <CollaboratorsAvatars provider={provider} />
      <EditorContent editor={editor} />
    </div>
  );
}

// Аватари активних учасників
function CollaboratorsAvatars({ provider }) {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const awareness = provider.awareness;

    const updateUsers = () => {
      const states = Array.from(awareness.getStates().values());
      setUsers(states.filter(s => s.user).map(s => s.user));
    };

    awareness.on('change', updateUsers);
    updateUsers();

    return () => awareness.off('change', updateUsers);
  }, [provider]);

  return (
    <div className="collaborators">
      {users.map(user => (
        <Avatar key={user.id} name={user.name}
          color={user.color} title={`${user.name} зейчас редагує`} />
      ))}
    </div>
  );
}

Liveblocks (managed)

import { createClient } from '@liveblocks/client';
import { createRoomContext } from '@liveblocks/react';
import * as Y from 'yjs';
import { LiveblocksYjsProvider } from '@liveblocks/yjs';

const client = createClient({
  publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_KEY
});

const { RoomProvider, useRoom } = createRoomContext(client);

function EditorPage({ documentId }) {
  return (
    <RoomProvider id={`document-${documentId}`} initialPresence={{}}>
      <CollaborativeEditorWithLiveblocks />
    </RoomProvider>
  );
}

function CollaborativeEditorWithLiveblocks() {
  const room = useRoom();
  const doc = useMemo(() => new Y.Doc(), []);

  useEffect(() => {
    const provider = new LiveblocksYjsProvider(room, doc);
    return () => provider.destroy();
  }, [room, doc]);

  const editor = useEditor({
    extensions: [
      StarterKit.configure({ history: false }),
      Collaboration.configure({ document: doc })
    ]
  });

  return <EditorContent editor={editor} />;
}

Розв'язання конфліктів через CRDT

Yjs використовує CRDT — математично доказаний алгоритм злиття конфліктуючих змін без координації. Два користувачі можуть редагувати офлайн, і при синхронізації конфлікти розв'язуються детерміністично.

Персистентність: y-leveldb / PostgreSQL

// Зберігання Yjs-документів в PostgreSQL
const documentTable = `
  CREATE TABLE IF NOT EXISTS documents (
    name VARCHAR(255) PRIMARY KEY,
    content BYTEA NOT NULL,   -- Yjs binary state
    updated_at TIMESTAMPTZ DEFAULT NOW()
  )
`;

// Інкрементальне збереження через y-protocols
import { encodeStateAsUpdate } from 'yjs';
const binaryState = encodeStateAsUpdate(doc);
await db.query(
  'INSERT INTO documents (name, content) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET content = $2',
  [docName, Buffer.from(binaryState)]
);

Часові рамки реалізації

  • Hocuspocus сервер + Tiptap клієнт + базове collaborative editing: 2–3 тижні
  • Курсори, аватари, персистентність в PostgreSQL: ще 1 тиждень
  • Liveblocks інтеграція (без self-hosted сервера): 1–1.5 тижні