Разработка онлайн-редактора документов

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка онлайн-редактора документов
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка онлайн-редактора документов

Онлайн-редактор с совместным редактированием — одна из технически сложных задач в веб-разработке. Google Docs строили годами. Но если не нужен полный паритет с Docs, а нужна конкретная функциональность (форматирование текста, комментарии, несколько авторов одновременно) — это реализуемо за разумный срок с правильными инструментами.

Выбор движка редактора

Три варианта со своими trade-off.

ProseMirror — низкоуровневый, максимальная гибкость, сложный порог входа. На нём построены Notion, Atlassian Confluence, GitLab. Подходит когда нужна нестандартная схема документа.

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

Lexical (Meta) — новее, лучше производительность на больших документах, активная разработка. Меньше готовых расширений.

CRDT через Y.js

Operational Transformation (OT) — старый подход, который использует Google Docs. CRDT (Conflict-free Replicated Data Types) — современный, проще в реализации распределённой системы. 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:

import { WebSocketServer } from 'ws';
import { setupWSConnection } from 'y-websocket/bin/utils.js';
import { createClient } from 'redis';

const wss = new WebSocketServer({ port: 1234 });

// Persistence через Redis (вместо дефолтного in-memory)
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 });
});

Для production: hocuspocus (официальный бэкенд-сервер для Tiptap) или y-redis для персистенции.

Структура документа: схема базы данных

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()
);

Комментарии и треки изменений

Комментарии в ProseMirror/Tiptap реализуются через marks с ID:

// Extension для комментариев
const Comment = Mark.create({
  name: 'comment',
  inclusive: false,

  addAttributes() {
    return {
      commentId: { default: null },
      resolved: { default: false },
    };
  },

  parseHTML() {
    return [{ tag: 'span[data-comment-id]' }];
  },

  renderHTML({ HTMLAttributes }) {
    return ['span', {
      ...HTMLAttributes,
      'data-comment-id': HTMLAttributes.commentId,
      class: HTMLAttributes.resolved ? 'comment resolved' : 'comment',
    }, 0];
  },
});

// Добавление комментария к выделению
function addComment(editor: Editor, text: string) {
  const commentId = crypto.randomUUID();
  editor.chain().focus().setMark('comment', { commentId }).run();

  // Сохраняем текст комментария в БД
  saveComment({ commentId, text, documentId });
}

Экспорт документов

Экспорт в DOCX через docx (npm) или через pandoc на бэкенде:

// Конвертация ProseMirror JSON → HTML → DOCX через pandoc
async function exportToDocx(documentId: string): Promise<Buffer> {
  const doc = await getDocument(documentId);
  const html = prosemirrorToHtml(doc.content); // через prosemirror-to-html

  // pandoc на бэкенде
  const { stdout } = await exec(
    `echo '${html.replace(/'/g, "'\\''")}' | pandoc -f html -t docx -o -`,
    { encoding: 'buffer' }
  );

  return stdout;
}

// Или нативно через docx npm package
import { Document, Paragraph, TextRun, Packer } from 'docx';

function generateDocx(nodes: ProseMirrorNode[]): Promise<Buffer> {
  const paragraphs = nodes.map(node => {
    const runs = node.content?.map(inline =>
      new TextRun({
        text: inline.text || '',
        bold: inline.marks?.some(m => m.type === 'bold'),
        italics: inline.marks?.some(m => m.type === 'italic'),
      })
    ) ?? [];
    return new Paragraph({ children: runs });
  });

  const doc = new Document({ sections: [{ children: paragraphs }] });
  return Packer.toBuffer(doc);
}

Права доступа и sharing

Три уровня: просмотр, комментирование, редактирование. Публичные ссылки с опциональным паролем:

class DocumentShareController extends Controller
{
    public function createShareLink(Request $request, string $docId): JsonResponse
    {
        $doc = Document::where('id', $docId)
            ->where('owner_id', $request->user()->id)
            ->firstOrFail();

        $share = DocumentShare::create([
            'document_id' => $docId,
            'token'       => Str::random(32),
            'permission'  => $request->input('permission', 'viewer'),
            'password'    => $request->filled('password')
                ? bcrypt($request->input('password'))
                : null,
            'expires_at'  => $request->input('expires_at'),
        ]);

        return response()->json([
            'url' => route('doc.shared', $share->token),
        ]);
    }
}

Сроки

Одиночный редактор с форматированием, экспортом в PDF/DOCX, комментариями — 6–8 недель. Добавление реального времени (Y.js + WebSocket), cursors присутствия, истории версий — ещё 4–6 недель. Полноценная система прав, audit log, интеграция с OAuth-провайдерами — ещё 3–4 недели.