Разработка онлайн-редактора документов для совместной работы
Совместное редактирование документов в реальном времени — технически сложная задача. Мы столкнулись с ней, когда клиент попросил заменить 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+ проектов для разных отраслей.







