Реализация совместного редактирования (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

Реализация совместного редактирования (Collaborative Editing) на сайте

Collaborative editing — это синхронизация состояния документа между несколькими пользователями в реальном времени без конфликтов. Задача сложнее, чем «просто WebSocket». Ключевая проблема: два пользователя одновременно редактируют один документ — чьё изменение победит и как не потерять ни одно из них?

Два подхода к синхронизации: OT vs 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';

// Документ Y — корень всего
const ydoc = new Y.Doc();

// Провайдер — транспорт синхронизации
const provider = new WebsocketProvider(
  'wss://your-yjs-server.com',
  'document-room-id',
  ydoc,
  { connect: true }
);

// Shared text тип
const ytext = ydoc.getText('quill-content');

// Связываем с редактором
const editor = new Quill('#editor', { theme: 'snow' });
const binding = new QuillBinding(ytext, editor, provider.awareness);

// Awareness — присутствие, курсоры
provider.awareness.setLocalStateField('user', {
  name: 'Иван',
  color: '#ff6b6b',
});

Сервер синхронизации

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

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

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end();
});

const wss = new WebSocket.Server({ server });
wss.on('connection', (conn, req) => {
  setupWSConnection(conn, req, {
    docName: getDocNameFromUrl(req.url),
    gc: true, // garbage collect deleted content
  });
});

server.listen(1234);

Для production — Hocuspocus (официальный сервер для TipTap/Yjs с авторизацией, персистентностью, хуками):

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 };
  },

  async onLoadDocument({ documentName, context }) {
    if (!canRead(context.user, documentName)) {
      throw new Error('Forbidden');
    }
  },
});

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 }), // history отключаем — Yjs управляет undo
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({
      provider,
      user: { name: currentUser.name, color: generateColor(currentUser.id) },
    }),
  ],
});

Курсоры и awareness

Awareness — это эфемерное состояние (не сохраняется в документе): курсоры, выделения, статус онлайн. Обновляется через CRDT-like механизм Yjs:

// Подписка на изменения 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),
  });
});

Относительные позиции (RelativePosition) важны: они указывают на символ в документе, а не на индекс. При вставке текста перед курсором — позиция автоматически корректируется.

Офлайн и локальное персистирование

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

import { IndexeddbPersistence } from 'y-indexeddb';

const persistence = new IndexeddbPersistence(`doc-${docId}`, ydoc);

persistence.on('synced', () => {
  console.log('Локальное содержимое загружено из IndexedDB');
});

// Состояние автоматически сохраняется при каждом изменении
// При переподключении WebsocketProvider — Yjs мержит локальные и серверные изменения

Контроль версий и история

Yjs хранит всю историю операций. Для UI «история версий»:

import { UndoManager } from 'yjs';

const undoManager = new UndoManager(ytext, {
  captureTimeout: 500, // объединять изменения в течение 500 мс
  trackedOrigins: new Set([provider.awareness.clientID]),
});

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

Для снепшотов (named versions):

import * as Y from 'yjs';

// Создание снепшота
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');
  }
}

Структурированный контент: Y.Map и Y.Array

Совместное редактирование не ограничивается текстом. Для форм, канбан-досок, презентаций:

// Доска задач
const ytasks = ydoc.getArray('tasks');

// Добавление задачи (синхронизируется всем)
const task = new Y.Map();
task.set('id', generateId());
task.set('title', 'Новая задача');
task.set('status', 'todo');
task.set('assignee', userId);
ytasks.push([task]);

// Перемещение задачи (автоматический merge если два пользователя перемещают одновременно)
ytasks.observe(event => {
  renderBoard(ytasks.toArray());
});

Масштабирование и кластеризация

При нескольких инстансах сервера нужен общий бэкенд для Yjs-документов. Hocuspocus поддерживает Redis адаптер:

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

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

Метрики производительности

Yjs оптимизирован для больших документов. Бенчмарки показывают обработку 1000+ одновременных операций без деградации. Для мониторинга:

  • Размер Y.Doc в памяти: Y.encodeStateAsUpdate(ydoc).byteLength
  • Количество операций GC: метрика из hocuspocus stats
  • Задержка синхронизации: разница между Date.now() при отправке и получении awareness-события

Сроки

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