Перейти к содержанию

Документ Yjs и общий текст

Совместное редактирование начинается с модели документа. В приложении «Черновик» два участника будут составлять план встречи: Анна уточнит повестку, Борис добавит вопросы. Если хранить целую строку и по каждому нажатию отправлять её на сервер, последнее сообщение способно затереть чужую правку. Нам нужна модель, которая представляет изменения отдельно от видимого текста. В этом уроке создадим локальный документ Yjs и рассмотрим его общий текстовый тип.

Предполагается, что вы знакомы с модулями JavaScript, обработчиками DOM и асинхронными операциями. Серия использует зафиксированную совместимую ветку yjs 13.6.27. Это сознательный выбор учебного контракта: примеры следующей предварительной ветки библиотеки нельзя механически смешивать с адаптерами для версии 13. Архив содержит самостоятельный каталог каждого урока, поэтому вам не придётся угадывать промежуточные изменения по памяти.

Что хранит Y.Doc

Объект Y.Doc представляет одну реплику общего документа. Реплика — локальная копия структуры, которая может независимо получать изменения. Документ существует в памяти текущего процесса; создание объекта само по себе не открывает сеть, не проверяет пользователя и не записывает файл. Такое разделение позволяет сначала разобраться с текстом, затем выбрать транспорт и лишь после этого добавить долговременное хранение.

Внутри документа размещаются именованные общие типы. В «Черновике» имя markdown закреплено за Y.Text, а позднее имя meta будет закреплено за картой метаданных. Вызов getText('markdown') возвращает один и тот же общий тип в пределах конкретного документа. Другая строка имени создаёт другое поле. Поэтому совпадение названий в независимых репликах является частью схемы приложения, а не декоративным соглашением.

Рассмотрим начальные данные: заголовок «План встречи», пустую строку и пункт «Обсудить расписание». Markdown здесь остаётся обычным текстом. Yjs не интерпретирует решётку как заголовок и не проверяет структуру списка. Общий тип отвечает за последовательность символов; правила разметки, предварительный просмотр и допустимое содержание определяет приложение. Это полезная граница: модель можно сохранить независимо от используемого редактора.

Ниже показан полный файл src/app.js первого состояния. Начальный текст задаётся только в этом локальном упражнении. Когда появится серверная комната, источник начального содержимого переместится на сервер, иначе каждый открывшийся клиент мог бы независимо вставить одинаковый шаблон. Не переносите без изменения эту инициализацию в сетевую версию.

import * as Y from 'yjs';
const $=id=>document.getElementById(id);
let dispose = () => {};
async function openEditor(user='local',role='editor',displayName=user) {
 const doc=new Y.Doc(), text=doc.getText('markdown');
 text.insert(0,'# План встречи\n\n- Обсудить расписание\n');
 const render=()=>{ $('editor').value=text.toString();$('preview').textContent=text.toString();};text.observe(render);render();$('editor').disabled=true;
 let disposed=false;
 dispose=()=>{
  if(disposed)return;disposed=true;
  doc.destroy();$('editor').disabled=true;};
}
$('login').hidden=true;openEditor();
$('leave').onclick=()=>{dispose();$('status').textContent='Редактор закрыт';};window.addEventListener('pagehide',()=>dispose(),{once:true});
$('heading').hidden=true;
$('exchange').hidden=true;
$('undo').hidden=true;
$('redo').hidden=true;
$('revision').hidden=true;

Вставка в начало выполняется через insert, а получение привычной строки — через toString. Последняя операция создаёт представление для интерфейса; она не превращает общий тип в обычную изменяемую строку. Если сохранить результат в переменную и изменить эту переменную, документ не узнает о правке. Источником состояния остаётся Y.Text, поэтому операции редактирования должны обращаться именно к нему.

После самостоятельной подготовки локального browser bundle и открытия страницы вы должны увидеть исходный план в поле и в текстовом предварительном просмотре. Поле пока запрещено редактировать: первый урок исследует модель, а не привязку ввода. Это ожидаемый эффект листинга; авторский запуск не проводился. Если страница сообщает о необходимости подготовить bundle, это ответ учебного сервера, а не ошибка алгоритма Yjs.

Изменение и наблюдение

Наблюдатель общего типа вызывается при изменениях текста. В минимальной странице он переносит строковое представление в DOM. Обратите внимание на textContent: содержимое документа показывается как текст и не становится исполняемой HTML-разметкой. Даже если участник позже напишет тег с обработчиком события, такой предварительный просмотр должен вывести символы тега. Для полноценного Markdown-renderer потребуется отдельная политика безопасного HTML.

Не следует назначать пользовательское имя внутреннему идентификатору клиента Yjs. Идентификатор помогает библиотеке различать операции реплик; он не подтверждает личность Анны или Бориса. Одна Анна может открыть несколько вкладок с разными репликами. Две вкладки одного пользователя также могут работать одновременно. Пользовательские роли и связь с аккаунтом мы введём на сервере, где можно проверять доверенные данные.

Пока единственный документ существует только в памяти страницы. Перезагрузка создаст новый объект и снова выполнит учебную вставку. Это не потеря, которую CRDT обещает предотвратить: механизм слияния описывает согласование доставленных операций, а долговечность зависит от хранилища. На этом этапе правильный интерфейс не должен показывать надпись «Сохранено на сервере», поскольку сервера документа ещё нет.

Схема и жизненный цикл

Именование общих типов стоит зафиксировать заранее. Если один клиент ожидает текст в markdown, а другой в body, они могут успешно обмениваться пакетами и всё же показывать разные поля. Внешне такая ошибка похожа на сбой синхронизации, хотя транспорт доставляет данные. При изменении схемы потребуется явная миграция и договорённость о поддерживаемых версиях клиентов; новое имя нельзя считать автоматическим переименованием старого.

Также отделим существование объекта от существования комнаты. Комната draft-42 пока является только будущим идентификатором общего пространства. Y.Doc не знает, каким участникам разрешён доступ к этой комнате. Браузер может создать сколько угодно документов, но сервер должен независимо решать, какой из них имеет право читать и изменять конкретный ресурс. Эта мысль пригодится при добавлении провайдера.

При закрытии редактора вызов destroy освобождает документ и его обработчики. Кнопка закрытия намеренно не удаляет какие-либо серверные данные: таких данных ещё нет. В более сложном состоянии сначала будут остановлены provider, awareness и хранилище, затем уничтожен сам документ. Порядок важен, поскольку зависимые объекты держат ссылки на реплику и могут подписываться на её события.

Попробуйте мысленно заменить имя markdown на notes только в строке чтения. Ожидаемым результатом станет другое, пустое общее поле. Верните единое имя и объясните, почему начальный текст опять доступен. Этот вариант помогает проверить модель без сетевых эффектов. В следующем уроке то же поле станет редактируемым: мы свяжем изменение DOM с операциями над Y.Text, сохраняя единый источник состояния.

Оглавление серии · Исходные состояния проекта.

API сверены с первичной документацией 11 октября 2026 года. Пример имеет статус not_run: фактический запуск и наблюдение результата читателю предстоит выполнить самостоятельно.