Архитектура
Rector разделяет хранимый документ, среду редактирования, расширения и рендерер документа. Эти уровни используют общие контракты данных, но не общие изменяемые диспетчеры или состояние интерфейса.
Границы системы
Приложение
│
├─ createEditor(config) ──> Дескриптор редактора
│ ├─ API блоков
│ ├─ подписки на события
│ └─ save/render/destroy
│
├─ Блочные и внутристрочные расширения
│ └─ граница команды mutate(...)
│
└─ createEditorRenderer(config) ──> DOM документа
Версионированный документ JSON — контракт между редактированием, хранением и отображением.Граница приложения
Приложение создаёт и уничтожает редактор, передаёт конфигурацию, сохраняет документы и подписывается на публичные события. Оно получает узкий дескриптор IEditor, а не внутренние объекты сборки.
Дескриптор намеренно не предоставляет диспетчер блоков, диспетчер команд, диспетчер истории, диспетчер выделения, генератор событий, диспетчер всплывающих элементов или загрузчик стилей. Поэтому прикладной код не может обойти целостность документа.
Граница документа
EditorDocument — обычные сериализуемые данные. Rector копирует документ при передаче в редактор, на каждом шаге миграции и при выдаче результата сохранения. Живой DOM никогда не является контрактом хранения.
У каждого блока есть устойчивый id, зарегистрированный type, принадлежащие плагину data и необязательные поля revision, tunes и inline. Ядро владеет оболочкой и идентификаторами, а плагин — только формой собственных data. Принадлежащая источнику revision служит оптимизацией частичного отображения, а не ещё одной версией схемы.
Версия документа описывает оболочку и общие правила плагинов. Плагин обязан развивать свои данные совместимо или участвовать в явно заданной миграции документа.
Граница сборки редактора
createEditor() — корень сборки. Он проверяет конфигурацию и создаёт нормализацию документа, владение блоками, выделение, команды, историю, обработку клавиатуры, панели инструментов, маршрутизацию вставки из буфера, диагностику, локализацию и владение стилями.
Эти службы взаимодействуют через узкие внутренние интерфейсы. Они остаются деталями реализации, даже если в исходном дереве есть файл деклараций. Публичные импорты ограничены путями из карты exports пакета.
Граница команды
Каждое постоянное действие проходит через одну транзакцию команды. Транзакция фиксирует предыдущее состояние, выполняет синхронное изменение, помечает затронутые блоки и записывает один шаг истории. Неудачная команда откатывается целиком.
Прикладной код входит в эту границу через публичные методы редактора. Расширение получает только подходящую возможность mutate(...). Подробности приведены в разделе Команды и история.
Границы расширений
В Rector есть четыре разные роли расширений:
| Роль | Чем владеет | Чем не владеет |
|---|---|---|
| Блочный плагин | редактируемым DOM одного блока и его сериализованными data | порядком документа, идентификатором блока и общей историей |
| Внутристрочный инструмент | форматированием выделенного диапазона | постоянными данными виджета |
| Внутристрочный плагин | виджетом в тексте и его сериализованными данными | схемой окружающего блока |
| Рендерер блока | итоговым DOM одного типа блока | элементами управления и состоянием редактора |
Расширения получают возможности, а не внутренние диспетчеры. Плагин может изменить собственный элемент через переданный контекст, но не может переставлять произвольные блоки или создавать внутренние события.
Владение стилями
Приложение импортирует основную таблицу стилей Rector. Класс плагина может объявить статические URL таблиц стилей. Rector подсчитывает владельцев соответствующих элементов <link> между экземплярами редактора. После уничтожения последнего владельца подключённая таблица удаляется.
У рендерера документа отдельный жизненный цикл стилей. renderer.injectStyles() возвращает владельца, чей метод destroy() освобождает эти подключения. Симметричное освобождение предотвращает утечки глобальных стилей.
Владение ресурсами
У каждой подписки и каждого выделенного ресурса должен быть владелец:
- редактор владеет обработчиками корневого элемента, наблюдателями, внутренними диспетчерами, всплывающими элементами и зарегистрированными экземплярами плагинов;
- блочный плагин владеет обработчиками и сторонними объектами своего блока и освобождает их в
destroy(element); - группа внутристрочных элементов управления освобождает временные элементы в своём
destroy(); - рендерер владеет экземплярами рендереров, подключёнными к контейнерам результата, и освобождает их через
destroy(container?); - приложение владеет дескриптором редактора и каждым созданным владельцем стилей рендерера.
Расширение не должно откладывать освобождение ресурсов до выгрузки страницы.
Поток данных
Исходная загрузка
- Приложение передаёт документ в
createEditor(). - Схема документа проверяет оболочку и применяет детерминированную цепочку миграций.
- Зарегистрированные блочные плагины отображают собственные данные.
- Текстовые плагины превращают сохранённые заполнители внутристрочных виджетов обратно в DOM.
- После сборки Rector устанавливает исходную точку истории.
Редактирование и сохранение
- Публичный метод или контекст расширения открывает команду.
- Команда меняет принадлежащий ей DOM или структуру документа.
- Затронутый плагин сериализует элемент через
save(). - Rector переносит внутристрочные виджеты в карту
inlineблока. - Документ проверяется и выдаётся как отделённые данные.
Отображение документа
- Приложение передаёт сохранённый JSON в
EditorRenderer. - Рендерер выбирает
BlockRendererпо значениюtypeблока. - Внутристрочные заполнители очищаются и восстанавливаются через зарегистрированные плагины.
- Рендерер блока возвращает итоговый DOM.
destroy()освобождает ресурсы рендерера при замене или удалении контейнера.
Направление зависимостей
Ядро не импортирует код приложения. Блочные плагины зависят от публичных контрактов и небольших общих утилит. Рендерер зависит от контракта документа, но не от среды редактирования. Необязательные интеграции загружаются только возможностями, которым они нужны.
Сохраняйте это направление в собственных расширениях: опирайтесь на экспортируемые типы и возможности контекста, но не импортируйте файлы из core/, которых нет в exports файла package.json.