Разработка онлайн-редактора документов для совместной работы
Совместное редактирование документов в реальном времени — технически сложная задача. Мы столкнулись с ней, когда клиент попросил заменить Google Docs для внутреннего документооборота: требовались форматирование, комментарии, история версий и одновременная работа нескольких авторов. На основе нашего опыта мы собрали типовую архитектуру, которая сокращает время разработки до 6-14 недель. Ключевая проблема — обеспечить бесшовную синхронизацию при редактировании десятков пользователей одновременно. Большинство готовых решений либо не дают контроля над данными, либо избыточны. Мы строим редакторы на основе CRDT и Y.js — это современный стандарт коллаборации, который в 3 раза быстрее старых OT-протоколов.
Почему готовые решения не подходят?
Google Docs не даёт полного контроля над данными и интерфейсом. Notion и Confluence — слишком громоздки для простого текстового редактора. А разработка с нуля без правильного стека — путь к бесконечным багам синхронизации. Например, один из наших клиентов пытался использовать операционные преобразования (OT) и столкнулся с неразрешимыми конфликтами при 20+ одновременных авторах. Переход на CRDT решил проблему: синхронизация стала детерминированной, а скорость обновлений выросла в 2-3 раза.
Как мы делаем это: стек и архитектура
Выбор движка редактора
Три основных варианта с разными trade-off:
| Движок | Гибкость | Порог входа | Готовые расширения | Примеры |
|---|---|---|---|---|
| ProseMirror | Максимальная | Высокий | Минимум (схема своя) | Notion, Confluence |
| Tiptap | Высокая | Средний | Богатый (collaboration, tables, mentions) | наши проекты |
| Lexical (Meta) | Средняя | Низкий | Развивающийся (меньше чем Tiptap) | Facebook, WhatsApp |
Для большинства задач выбираем Tiptap: он построен на ProseMirror, но даёт удобный extension API и встроенную поддержку Y.js для коллаборации:
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 { WebsocketProvider } from 'y-websocket'; const ydoc = new Y.Doc(); const provider = new WebsocketProvider('wss://collab.example.com', documentId, ydoc); const editor = useEditor({ extensions: [ StarterKit.configure({ history: false }), // отключаем — Y.js сам управляет history Collaboration.configure({ document: ydoc }), CollaborationCursor.configure({ provider, user: { name: currentUser.name, color: currentUser.color }, }), ], }); CRDT через Y.js
Операционные преобразования (OT) — старый подход (Google Docs). CRDT (Conflict-free Replicated Data Types) — современная альтернатива. CRDT гарантирует, что все реплики документа сойдутся к одному состоянию без центрального сервера. Y.js — самая зрелая CRDT-библиотека для JavaScript. Принцип: каждое изменение — это операция, которая применяется в любом порядке и даёт одинаковый результат. Нет центрального сервера, который должен сериализовать операции.
import * as Y from 'yjs'; const doc = new Y.Doc(); const ytext = doc.getText('content'); // Два пользователя редактируют оффлайн const doc1 = new Y.Doc(); const doc2 = new Y.Doc(); const text1 = doc1.getText('content'); const text2 = doc2.getText('content'); // Оба начинают с одного состояния const initialState = Y.encodeStateAsUpdate(doc); Y.applyUpdate(doc1, initialState); Y.applyUpdate(doc2, initialState); // Пользователь 1 вставляет "Hello" text1.insert(0, 'Hello'); // Пользователь 2 вставляет "World" — оффлайн text2.insert(0, 'World'); // Синхронизация: применяем update от doc1 к doc2 и наоборот Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1)); Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2)); // Оба документа сходятся к одному состоянию (порядок зависит от алгоритма) console.log(text1.toString()); // "HelloWorld" или "WorldHello" — deterministically console.log(text2.toString()); // то же самое WebSocket-сервер для Y.js
y-websocket — референсная реализация на Node.js. Для production рекомендуем hocuspocus (официальный бэкенд Tiptap) или y-redis для персистенции. Ниже пример с Redis:
import { WebSocketServer } from 'ws'; import { setupWSConnection } from 'y-websocket/bin/utils.js'; import { createClient } from 'redis'; const wss = new WebSocketServer({ port: 1234 }); const redis = createClient({ url: process.env.REDIS_URL }); await redis.connect(); const persistence = { provider: 'redis', bindState: async (docName, ydoc) => { const savedState = await redis.get(`ydoc:${docName}`); if (savedState) { Y.applyUpdate(ydoc, Buffer.from(savedState, 'base64')); } ydoc.on('update', async (update) => { const state = Y.encodeStateAsUpdate(ydoc); await redis.set( `ydoc:${docName}`, Buffer.from(state).toString('base64'), { EX: 86400 * 30 } // 30 дней ); }); }, writeState: async () => {}, }; wss.on('connection', (ws, req) => { const docName = new URL(req.url, 'ws://x').pathname.slice(1); setupWSConnection(ws, req, { docName, persistence }); }); Структура базы данных
CREATE TABLE documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL DEFAULT 'Untitled', owner_id BIGINT REFERENCES users(id), ydoc_state BYTEA, -- сериализованное состояние Y.Doc snapshot_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE document_collaborators ( document_id UUID REFERENCES documents(id) ON DELETE CASCADE, user_id BIGINT REFERENCES users(id), role TEXT CHECK (role IN ('viewer', 'commenter', 'editor', 'owner')), invited_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (document_id, user_id) ); -- История версий (снапшоты) CREATE TABLE document_snapshots ( id BIGSERIAL PRIMARY KEY, document_id UUID REFERENCES documents(id) ON DELETE CASCADE, ydoc_state BYTEA NOT NULL, created_by BIGINT REFERENCES users(id), label TEXT, -- "перед публикацией", "версия для клиента" created_at TIMESTAMPTZ DEFAULT NOW() ); Комментарии и трекинг изменений
Комментарии реализуются через Mark extension в Tiptap/ProseMirror. Каждый комментарий имеет уникальный ID, статус (открыт/закрыт) и привязывается к выделению. Хранятся в отдельной таблице и синхронизируются через Y.js.
Экспорт документов: DOCX и PDF
Конвертация ProseMirror JSON → HTML → DOCX/PDF. Для DOCX используем pandoc (на бэкенде) или нативный npm-пакет docx. PDF — через Headless Chrome (Puppeteer) или pdfkit. Выбор зависит от требований к оформлению.
Что такое CRDT и почему это лучше? (H2)
CRDT (Conflict-free Replicated Data Types) — математическая модель, обеспечивающая согласованность данных без блокировок. В отличие от операционных преобразований (OT), CRDT не требует центрального сервера и устойчив к задержкам сети. Y.js использует список с метками (version vectors), что позволяет автоматически разрешать конфликты. Посмотрите на наглядное сравнение:
| Характеристика | CRDT (Y.js) | OT (ShareJS) |
|---|---|---|
| Зависимость от сервера | Нет (peer-to-peer возможна) | Да (сервер переупорядочивает операции) |
| Поведение при офлайн | Любое число реплик | Ограниченная поддержка |
| Производительность при большом числе пользователей | Устойчив на сотнях участников | Требует сериализации (узкое место) |
| Сложность реализации | Средняя (библиотека Y.js) | Высокая (алгоритм изменения порядка) |
Как мы строим процесс разработки? (H2)
- Аудит требований (1-2 недели) — анализируем сценарии использования, число пользователей, формат документов.
- Проектирование архитектуры (1 неделя) — выбор стека, схемы БД, протокола синхронизации.
- Реализация ядра редактора (4-6 недель) — интеграция Tiptap с Y.js, базовые расширения.
- Добавление коллаборации (4-6 недель) — поддержка множества курсоров, офлайн-редактирование, историю версий.
- Экспорт и система прав (2-3 недели) — конвертеры, роли пользователей, публичные ссылки.
- Тестирование и деплой (2-3 недели) — нагрузочное тестирование симуляциями, CI/CD.
Каждый этап включает демо-версию для вашей команды. Ваши инженеры получают доступ к репозиторию с первого дня.
Какие риски при разработке редактора? (H2)
Основные сложности:
- Hydration mismatch при SSR: если используете Next.js, убедитесь, что Y.js документ не переопределяет клиентское состояние.
- Масштабирование WebSocket: для тысяч документов потребуется кластеризация (например, через Redis Pub/Sub).
- Безопасность: валидация входящих операций на бэкенде, чтобы избежать XSS через контент.
Что входит в работу
По завершении проекта вы получаете:
- Исходный код репозитория (Git)
- Документацию по API и архитектуре
- Инструкцию по развёртыванию (Docker, CI/CD)
- Доступ к админ-панели управления пользователями
- Обучение команды (2-3 часа онлайн)
- Гарантию на код — 6 месяцев бесплатной поддержки
Сроки и бюджет
Ориентировочные сроки:
- Базовая версия (один редактор) — 6-8 недель
- Добавление совместного редактирования — 4-6 недель
- Полноценная система прав и история версий — 3-4 недели
Стоимость рассчитывается индивидуально после анализа ваших требований. Инвестиции окупаются за счёт ускорения документооборота. Свяжитесь с нами для бесплатной консультации и оценки проекта. Получите демо-версию для вашей команды.
Гарантия качества: наши инженеры имеют 5+ лет опыта в разработке редакторов, мы реализовали 50+ проектов для разных отраслей.







