Реалізація спільного редагування (Collaborative Editing) на сайті

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація спільного редагування (Collaborative Editing) на сайті
Складна
~1-2 тижні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • 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

Реалізація спільного редагування на сайті

Спільне редагування — синхронізація стану документа між кількома користувачами в реальному часі без конфліктів. Задача складніша, ніж «просто WebSocket»: два користувачі одночасно редагують один документ — чия зміна перемагає без втрати жодної?

Два підходи: OT проти CRDT

Operational Transformation (OT) — класичний підхід (Google Docs). Операції трансформуються відносно конкуруючих. Вимагає центрального сервера.

CRDT (Conflict-free Replicated Data Types) — структури, що математично гарантують eventual consistency без координатора. Yjs, Automerge — основні реалізації.

OT CRDT (Yjs)
Центральний сервер Обов'язковий Опціонально (P2P можливий)
Офлайн-редагування Складно Вбудовано
Продуктивність Висока Висока (Yjs дуже ефективен)
Складність реалізації Висока Низька (бібліотека займається)
Популярні бібліотеки ShareDB, ot.js Yjs, Automerge

Для більшості нових проектів — вибір Yjs.

Yjs: архітектура

Yjs надає shared types: Y.Text, Y.Map, Y.Array, Y.XmlFragment. Зміни автоматично синхронізуються через провайдер.

import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { QuillBinding } from 'y-quill';
import Quill from 'quill';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider(
  'wss://your-yjs-server.com',
  'document-room-id',
  ydoc,
  { connect: true }
);

const ytext = ydoc.getText('quill-content');
const editor = new Quill('#editor', { theme: 'snow' });
const binding = new QuillBinding(ytext, editor, provider.awareness);

provider.awareness.setLocalStateField('user', {
  name: 'Ivan',
  color: '#ff6b6b',
});

Sync Server

y-websocket — стандартний сервер для Yjs. Може персистувати до Redis або LevelDB:

const { setupWSConnection } = require('y-websocket/bin/utils');
const http = require('http');
const WebSocket = require('ws');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', (conn, req) => {
  setupWSConnection(conn, req, {
    docName: getDocNameFromUrl(req.url),
    gc: true,
  });
});

server.listen(1234);

Для production — Hocuspocus (офіційний TipTap/Yjs сервер з auth, persistence, hooks):

import { Server } from '@hocuspocus/server';
import { Database } from '@hocuspocus/extension-database';

const server = Server.configure({
  port: 1234,
  extensions: [
    new Database({
      fetch: async ({ documentName }) => {
        return await db.getDocument(documentName);
      },
      store: async ({ documentName, state }) => {
        await db.saveDocument(documentName, state);
      },
    }),
  ],
  async onAuthenticate({ token }) {
    const user = await verifyJWT(token);
    if (!user) throw new Error('Unauthorized');
    return { user };
  },
});

server.listen();

Інтеграція з TipTap

TipTap — найзрілійший rich-text редактор з нативною підтримкою Yjs через @tiptap/extension-collaboration:

import { Editor } from '@tiptap/core';
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';

const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({
  url: 'wss://your-server.com',
  name: `document-${docId}`,
  document: ydoc,
  token: authToken,
});

const editor = new Editor({
  extensions: [
    StarterKit.configure({ history: false }),
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({
      provider,
      user: { name: currentUser.name, color: generateColor(currentUser.id) },
    }),
  ],
});

Курсори та awareness

Awareness — це ефемерний стан (не зберігається): курсори, виділення, статус онлайн.

provider.awareness.on('change', ({ added, updated, removed }) => {
  const states = provider.awareness.getStates();
  renderRemoteCursors(states);
});

editor.on('selectionUpdate', ({ editor }) => {
  const { from, to } = editor.state.selection;
  provider.awareness.setLocalStateField('cursor', {
    anchor: Y.createRelativePositionFromTypeIndex(ytext, from),
    head: Y.createRelativePositionFromTypeIndex(ytext, to),
  });
});

Відносні позиції автоматично коригуються при вставці контенту.

Офлайн та персистентність

y-indexeddb зберігає у IndexedDB — користувачі працюють офлайн, синхронізуються при переподключенні:

import { IndexeddbPersistence } from 'y-indexeddb';

const persistence = new IndexeddbPersistence(`doc-${docId}`, ydoc);
persistence.on('synced', () => {
  console.log('Локальний контент завантажено з IndexedDB');
});
// Автоматично зберігається при кожній зміні, мержиться при переподключенні

Історія версій

Yjs зберігає історію операцій. Для UI "історія версій":

import { UndoManager } from 'yjs';

const undoManager = new UndoManager(ytext, {
  captureTimeout: 500,
  trackedOrigins: new Set([provider.awareness.clientID]),
});

document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.key === 'z') undoManager.undo();
  if (e.ctrlKey && e.key === 'y') undoManager.redo();
});

Снепшоти (іменовані версії):

const snapshot = Y.snapshot(ydoc);
const snapshotBinary = Y.encodeSnapshot(snapshot);
await saveSnapshot(docId, snapshotBinary);

const restoredDoc = Y.createDocFromSnapshot(ydoc, snapshot);

Контроль доступу

Hocuspocus розрізняє read/write:

async onAuthenticate({ token, documentName }) {
  const user = await verifyToken(token);
  const doc = await getDocumentMeta(documentName);

  if (doc.ownerId === user.id) return { user, role: 'owner' };
  if (doc.editors.includes(user.id)) return { user, role: 'editor' };
  if (doc.viewers.includes(user.id)) return { user, role: 'viewer' };

  throw new Error('Access denied');
},

async onChange({ context, update }) {
  if (context.role === 'viewer') {
    throw new Error('Read-only access');
  }
}

Структурований контент

Не лише текст. Для форм, kanban, презентацій:

const ytasks = ydoc.getArray('tasks');

const task = new Y.Map();
task.set('id', generateId());
task.set('title', 'New task');
task.set('status', 'todo');
ytasks.push([task]);

ytasks.observe(event => {
  renderBoard(ytasks.toArray());
});

Масштабування: Multi-Node

Hocuspocus підтримує Redis адаптер для розподіленого Yjs:

import { Redis } from '@hocuspocus/extension-redis';

const server = Server.configure({
  extensions: [
    new Redis({
      host: 'redis://your-redis:6379',
      // Всі екземпляри синхронізуються через Redis pub/sub
    }),
  ],
});

Терміни

  • Базовий спільний текстовий редактор (TipTap + Hocuspocus + PostgreSQL) — 5–7 днів
  • Курсори інших користувачів, presence-індикатори — плюс 2–3 дні
  • Офлайн-режим (IndexedDB persistence) — плюс 1–2 дні
  • Історія версій з UI — плюс 3–4 дні
  • Контроль доступу read/write — плюс 2–3 дні
  • Редагування структурованого контенту (дошка, форми) — окрема оцінка