Страница Mercury — не дерево компонентов в коде. Это JSON-документ: данные, которые сервер может хранить, рендерер — читать, а редактор — менять, с одинаковыми проверками на всех этапах. Формат определяет @fiberfx/schema.
Документ
Документ содержит пять полей:
| Поле | Содержимое |
|---|---|
format |
версия переносимого формата, целое число от 1; FORMAT_VERSION равен 1 |
id |
идентификатор документа, непустая строка |
kind |
page (обычное содержимое), fragment (общее содержимое, на которое ссылаются другие документы) либо template |
roots |
идентификаторы узлов верхнего уровня в заданном порядке |
nodes |
все узлы в одном плоском объекте с ключами-идентификаторами |
Неописанные форматом поля сохраняются без изменений, чтобы окружение могло хранить собственные данные рядом с содержимым. Они тоже должны быть совместимы с JSON.
Узлы и слоты
Дерево не вложено в JSON. Каждый узел присутствует в nodes один раз, а родитель перечисляет идентификаторы потомков в именованных слотах:
id: идентификатор узла, равный его ключу вnodes;type: тип блока для рендеринга, напримерheading;version: версия схемы этого блока, целое число от 1;props: значения, только JSON;slots: отображение имён слотов в упорядоченные списки идентификаторов потомков.
Свойство может содержать ссылку вместо простого значения: { "$ref": "asset", "id": "…" }, где $ref — asset, route, entity либо fragment. Документ хранит только ссылку. Объект, на который она указывает, разрешается отдельно от документа, никогда не внутри него.
Реестр схем
Реестр схем описывает доступные блоки и допустимые данные каждого. createSchemaRegistry({ blocks, limits?, breakpoints?, tokens? }) строит его из определений блоков. Определение содержит type, version, props и, необязательно, defaults, slots и migrations.
- Схема свойства имеет
kind:string(сenum,maxLength),number(сinteger,min,max),boolean,reference(сto),token(сset),list(сitems,maxItems),record(сfields) либоjson. Любое свойство может бытьoptionalиresponsive. - Слот может перечислять допустимые типы блоков (
allow) и границы количества потомков (min,max). Без ограничений допускаются любой тип и любое количество. - Ограничения по умолчанию берутся из
DEFAULT_LIMITS:maxDepth— 32 уровня вложенности узлов,maxBytes— 1 048 576,maxNodes— 10 000,maxPropDepth— 16.
Ошибка определения — ошибка программы, поэтому createSchemaRegistry выбрасывает TypeError: при повторной регистрации типа, версии ниже 1, разрыве цепочки миграций или начальных значениях, не соответствующих собственной схеме.
Для каталога не нужно строить реестр самостоятельно. contentSchema() из @fiberfx/content возвращает реестр для проверки каждого документа Content: определения всех блоков каталога с точками перелома sm, md, lg и xl.
parseDocument
parseDocument(input, registry) читает документ из текста JSON либо уже разобранного значения. Ответ имеет одну из двух форм:
{ ok: true, document }, гдеdocument— сам входной документ, включая неизвестные поля. Значения не приводятся к другим типам и не дополняются значениями по умолчанию.- Отказ:
{ ok: false, code, path, reason, subject, node? }.
Некорректный ввод не приводит к исключению. Текст, не являющийся JSON, getter, выбрасывающий исключение, или отозванный proxy возвращают отказ malformed. Текст длиннее восьмикратного maxBytes отклоняется как too-large до разбора; сам maxBytes считается по компактной JSON-записи документа, поэтому пробелы не меняют результат проверки.
import { parseDocument } from "@fiberfx/schema";
import { contentSchema } from "@fiberfx/content";
const parsed = parseDocument(
{
format: 1,
id: "hello",
kind: "page",
roots: ["page"],
nodes: {
page: { id: "page", type: "page", version: 1, props: { title: "Hello" }, slots: { content: ["title", "intro"] } },
title: { id: "title", type: "heading", version: 1, props: { text: "Hello", level: 7 }, slots: {} },
intro: { id: "intro", type: "paragraph", version: 1, props: { text: "A first page." }, slots: {} },
},
},
contentSchema(),
);
if (!parsed.ok) console.log(parsed.code, parsed.path, parsed.node, parsed.reason);
// invalid-prop #/nodes/title/props/level title expected at most 6
Свойство level заголовка — число от 1 до 6, поэтому 7 отклоняется. При level: 1 тот же вызов возвращает { ok: true, document }.
Результаты проверки
Отказ описывает одну проблему: типизированный code, путь JSON Pointer path в форме фрагмента (# — весь документ, #/nodes/title/props/level — одно свойство), reason в виде предложения, объект subject, внутрь которого ведёт путь (при разборе всегда document), и node, если проблема находится внутри узла.
Проверки выполняются по порядку; ответом становится первый отказ:
- Форма данных:
malformedдля отсутствующего или неверного поля,newer-formatдля формата выше 1,too-many-nodesпри превышенииmaxNodes,not-json-safeдля несовместимого с JSON значения,too-deepпри вложенности значения большеmaxPropDepth. Ключ каждого узла должен совпадать с егоid. - Размер:
too-large, если компактный JSON превышаетmaxBytes. - Структура: каждый узел должен быть достижим от корней ровно один раз. Иначе —
unknown-child(идентификатора нет вnodes),multiple-parents,cycle,too-deepпри превышенииmaxDepthлибоorphan(узел не указан ни среди корней, ни в слоте). - Каждый узел известного блока:
unknown-slot,slot-cardinality(за пределамиminилиmax),slot-not-allowed(тип потомка отсутствует вallow), затем свойства:unknown-prop,missing-prop(кроме необязательных) иinvalid-prop(неверный вид, перечисление, длина, диапазон, токен либо переопределение с именем, не являющимся точкой перелома). - Фрагмент, ссылающийся на себя, даёт
cycle.
Приложение сайта Mercury показывает обработку отказа: записывает code, path и reason в журнал и оставляет начальную разметку страницы вместо частичного рендеринга.
Блоки, неизвестные реестру
Если реестр не знает type узла либо его version новее зарегистрированной, узел сохраняется без изменений и не проверяется по определению. Его положение в структуре по-прежнему проверяется. Поэтому документ для более позднего каталога успешно разбирается, а рендерер показывает такой узел как неподдерживаемый — подробнее в уроке о рендеринге.
Узел с более старой версией обрабатывается иначе: он отклоняется с outdated-block, и документ сначала нужно мигрировать.