802: Пакет/UI
- Требуется:
Движок, управляемый схемой, для создания динамичных, адаптируемых пользовательских интерфейсов непосредственно из структур данных. Он рассматривает Схему как основу дизайна, позволяя стилизовать и перенастраивать UI с помощью декларативных таблиц стилей и рендерить их через сменные адаптеры.
Пакет UI реализует движок рендеринга, где Структура — это Дизайн. Вместо ручного создания шаблонов для каждого типа данных, он генерирует интерфейс автоматически на основе базовой JSON Схемы. Это гарантирует, что UI всегда синхронизирован с моделью данных — измените схему, и интерфейс обновится мгновенно.
Основная философия: Структура как источник истины
В традиционной разработке модель данных и UI часто расходятся. Этот пакет решает эту проблему, делая Схему единственным источником истины для интерфейса.
- Динамическая генерация: UI не закодирован жестко; он выводится. Изменение в структуре схемы (например, добавление поля, изменение типа) немедленно отражается в отображаемом результате.
- Всегда актуально: Поскольку UI является прямой проекцией схемы, это исключает класс ошибок, при которых интерфейс отстает от модели данных.
Декларативная стилизация и реконфигурация
В то время как Схема диктует, что отображается, Таблица стилей диктует, как это выглядит. Такое разделение позволяет кардинально менять дизайн, не затрагивая базовую структуру.
- Декларативный слой: Таблицы стилей действуют как конфигурационный слой, который сопоставляет узлы схемы с визуальными свойствами.
- Реконфигурируемость: Вы можете полностью изменить макет, отступы и визуальную иерархию, меняя таблицы стилей, фактически «натягивая обложки» на сырую структуру данных.
Адаптеры и совместимость
Движок не зависит от конечной цели рендеринга. Он использует паттерн Адаптер, чтобы преобразовывать абстрактное дерево компонентов в конкретные элементы UI.
- Поддержка систем дизайна: Адаптеры могут быть нацелены на конкретные системы дизайна (например, Material UI, Ant Design), сопоставляя абстрактные типы схем с многофункциональными, готовыми компонентами.
- Резервный HTML: Стандартный HTML-адаптер гарантирует, что любая схема может быть отображена как семантический, доступный веб-контент «из коробки».
Мультимодальные представления
Одна и та же схема и данные могут быть спроецированы в разные контексты с помощью режимов просмотра.
- Режим редактирования: Генерирует полностью интерактивные формы для ввода и валидации данных.
- Режим просмотра: Отображает оптимизированные представления только для чтения для потребления данных.
Это позволяет одному определению служить нескольким целям в приложении, сокращая дублирование кода и обеспечивая согласованность между созданием и просмотром данных.
Основные концепции
Динамическая система свойств с выводами
Система построена на основе саморасширяющегося контроллера, где модульные свойства регистрируют себя и свои взаимозависимости, создавая мощный и расширяемый граф вывода.
- Свойства саморегистрируются: Каждый модуль свойства (например, для
data,schema,vars) автоматически регистрируется при импорте. - Объявление зависимостей: Свойства могут объявлять зависимости от других свойств (например,
stylesзависит отvarsиsettings). - Цепочки выводов: Когда базовое свойство изменяется, контроллер автоматически пересчитывает все зависимые свойства в правильном порядке, обеспечивая постоянную согласованность состояния UI.
- Типобезопасность: TypeScript выводит типы контроллера и полей из объединенных зарегистрированных свойств.
- Модульная архитектура: Новые свойства вместе с их логикой вывода можно добавлять, не изменяя существующий код.
Пользовательские свойства
Динамическая система свойств является ключом к этой расширяемости. Вы можете создавать и регистрировать свои собственные свойства для добавления новых функций и управления любым аспектом поведения поля. Это позволяет встраивать мощную, специфичную для домена функциональность непосредственно в движок рендеринга. Например, вы можете реализовать:
- Свойство
slots, которое зависит отstylesдля определения, какие компоненты UI отображать. - Свойство
errors, которое зависит отdataиschemaдля выполнения валидации. - Пользовательские свойства стилизации, которые реагируют на определенные условия данных.
Темизируемая система компонентов
Второй ключ к расширяемости — это система Theme. Она позволяет полностью отделить движок рендеринга от любого конкретного UI-фреймворка. Эта система организует четкий поток информации от абстрактных данных к конкретному UI:
- Схема предоставляет структуру:
schemaопределяет форму данных и общую иерархию дерева UI. - Контроллер создает состояние: Контроллер обрабатывает схему и данные, создавая конкретное состояние для каждого поля в дереве.
- Переменные (
vars) объявляют атомы: CSS-переменные используются для декларативного назначения компонентов-атомовименованнымслотам(например,--slot-title: 'TitleAtom'). - Атомы — это строительные блоки:
Атомы— это компоненты конечного уровня (например,<Input />,<Button />), которые привязываются к состоянию поля для отображения данных. - Поля организуют рендеринг: Компонент
Fieldдействует как организатор, проверяя, какиеатомыназначены его именованным слотам через переменные. - Рендеринг управляется данными:
Fieldотображаетатомтолько в том случае, если в его:term[состоянии]{canonical="state message" href="/ru/acts/009_agent_state.md"}существуют соответствующие данные для этого слота, обеспечивая минималистичный UI. - Поля как листья: Представляет одну точку данных (например, строку), составляя несколько
атомовв полный элемент ввода (метка, виджет, описание). - Поля как ветви (Fieldsets):
Fieldтакже может представлять «ветвь» (объект/массив), действуя как «fieldset», который обеспечивает макет для своих дочернихполей.
Такое четкое разделение ответственности позволяет осуществлять глубокую настройку на каждом уровне, от логики обработки данных до конечного отрисованного пикселя.
Управление состоянием и структурное разделение
Контроллер централизованно управляет состоянием дерева, различая сырые свойства, обработанное состояние и последнее отрисованное состояние, чтобы обеспечить эффективное обнаружение изменений.
- Сырые свойства (
controller.props,controller.dataи т. д.): Исходные свойства, переданные корневому компоненту. Они служат источником истины и никогда не изменяются в процессе обработки. Это обеспечивает поддержку как управляемого, так и неуправляемого режимов.- Управляемый режим: Когда предоставляются свойства
dataилиvars, система использует эти внешние значения. Обновления вызывают коллбэкиonChange/onVarsChange. - Неуправляемый режим: Когда предоставляются только
initialDataилиinitialVars, контроллер управляет состоянием внутренне.
- Управляемый режим: Когда предоставляются свойства
- Текущее состояние (
controller.current): Обработанное состояние. После фазыstoreсвойства обрабатываются (например, схема сворачивается, данные валидируются), и результат сохраняется вcontroller.current. Это состояние, которое распределяется по полям. - Последнее состояние (
controller.last): Поверхностная копияcurrentсостояния из предыдущего цикла рендеринга. Используется для сравнения с новымcurrentсостоянием для точного определения того, какие свойства и пути изменились.
Структурное разделение:
Архитектура использует единое, разделяемое дерево состояния (controller.current) для минимизации использования памяти и обеспечения согласованности состояния.
- Нулевое дублирование: Поля не получают собственных копий данных. Вместо этого они хранят ссылки на срезы дерева состояния
controller.current. - Согласованность состояния: Поскольку поля напрямую ссылаются на срезы
controller.current, данные состояния дерева всегда согласованы. Обновления UI затем пакетируются и отрисовываются на следующем тике для повышения производительности. - Видимость потомков: Родительские поля (например, для объекта) имеют доступ ко всему своему поддереву, включая все вложенные данные и схему.
Конвейер обновления и вывода
Контроллер использует единый, унифицированный конвейер как для начального рендеринга, так и для всех последующих обновлений. Это обеспечивает последовательный и предсказуемый поток состояния. Вот пошаговое описание процесса:
-
Триггер: Происходит внешнее событие (например, ввод пользователя, вызов API). Вызывается метод
updateсоответствующего свойства, который проверяет наличие значимых изменений. Если их нет, процесс останавливается. -
Перерисовка корня: Если обнаружено изменение, запускается перерисовка корневого компонента
<Form>. Это инициирует основной цикл обработки контроллера. -
Сохранение сырых свойств: Во время рендеринга контроллер сначала сохраняет сырые свойства из компонента
<Form>. -
Обработка свойств: Затем сырые свойства обрабатываются в согласованное внутреннее состояние (
controller.current). Например,schemaсворачивается. -
Построение дерева полей: Контроллер обнаруживает все пути полей из обработанного состояния и обеспечивает существование объекта
fieldдля каждого из них. -
Распределение изменений: Контроллер сравнивает новое состояние
controller.currentс предыдущим состоянием (controller.last), чтобы определить, какие именно поля и свойства изменились. -
Инвалидация и вывод: Для каждого обнаруженного изменения вызывается
controller.invalidate(). Это точка входа для реактивной системы вывода, которая запускает:controller.rederive(): Вычисляет новые значения для всех зависимых свойств поля (например,settings,styles) в правильном топологическом порядке.controller.cascade(): Разумно распространяет изменения на дочерние поля, запуская их собственные циклы перерасчета.- Любое поле, чье состояние изменяется в ходе этого процесса, ставится в очередь на перерисовку.
-
Пакетные обновления DOM: После завершения цикла рендеринга React, хук
useLayoutEffectочищает очередь рендеринга. Все поля, поставленные в очередь на этапе вывода, обновляются в DOM одним эффективным пакетом.
Производительность и эффективность
Эффективный рендеринг
Архитектура спроектирована для высокой производительности за счет минимизации согласования React и накладных расходов на перерисовку.
- Точное обнаружение изменений: Сравнивая
controller.currentиcontroller.lastдля сырых свойств и используя глубокие проверки на равенство в цикле вывода, система точно знает, какие поля и свойства изменились, избегая ненужных обновлений. - Выборочная инвалидация и вывод: Инвалидируются только поля, затронутые изменением. Цепочка вывода гарантирует, что пересчитываются только зависимые свойства.
- Отложенный и пакетный рендеринг: Запросы на рендеринг полей ставятся в очередь во время цикла обработки. Затем контроллер очищает эту очередь одним пакетом внутри
useLayoutEffect, минимизируя вызовы рендеринга. - Дедупликация рендеринга: Если несколько изменений затрагивают одно и то же поле в одном цикле, оно все равно рендерится только один раз.
Умное каскадирование и вывод
Система эффективно распространяет изменения переменных вниз по дереву полей, подобно наследованию CSS-переменных, минимизируя при этом пересчет.
- Кэшированный граф зависимостей: Отношения зависимости между всеми свойствами вычисляются один раз и кэшируются. Процесс
rederiveиспользует этот кэш для запуска выводов в правильном порядке без его пересчета при каждом изменении. - Ленивое наследование: Переменные наследуются вверх по дереву по требованию, когда поле вычисляет свои стили, используя
controller.inherit(). - Выборочное каскадирование: Когда CSS-переменная (
var) изменяется в поле, методcascadeраспространяет изменение на его потомков. Каскадирование останавливается на любом потомке, который определяет собственное локальное переопределение для этой конкретной переменной. - Дифференциальные обновления: Чтобы избежать ненужных перерисовок, логика
rederiveвыполняет глубокую проверку на равенство результата каждой функцииderive. Поле помечается для перерисовки только в том случае, если каскадное изменениеvarдействительно привело к другому конечному состоянию (например, другому объектуstyle), предотвращая избыточные рендеры. - Рендеринг на основе последствий: Это означает, что внешнее изменение (например, обновление
var) вызовет перерисовку только в том случае, если оно действительно вызовет значимое изменение в производном свойстве, влияющем на UI. Если изменениеvarпереопределяется более специфичным правилом и приводит к тому же конечному результатуstyle, избыточного рендеринга не произойдет.
Пример потока обновления
Единый конвейер обрабатывает все обновления. Цикл вывода является неотъемлемой частью фазы «Распределение».
// Пользователь обновляет CSS-переменную в поле 'user.name'
await controller.update('user.name', 'vars', { '--field-color': 'red' });
Что происходит внутри:
controller.update()вызывает методupdateуVarsProperty.- Метод
updateобнаруживает изменение и вызываетcontroller.render(), запуская перерисовку корневого компонента. - Контроллер запускает свой цикл обработки: он сохраняет сырые свойства, обрабатывает их в новое состояние
controller.currentи строит дерево полей. - На шаге распределения контроллер обнаруживает, что
varsвuser.nameизменился, и вызываетcontroller.invalidate('user.name', 'vars'). controller.invalidate()— это точка входа для логики вывода:- Обновляет
field.varsв поле 'user.name'. - Вызывает
controller.rederive(field, ['vars']). - Вызывает
controller.cascade(field, ['vars']).
- Обновляет
- Вывод и каскадирование:
rederive()запускает цепочку зависимостей для поля 'user.name', обновляя его производныеstylesи ставя его в очередь на рендеринг.cascade()рекурсивно распространяет изменение дочерним элементам, запуская их процессrederive. useLayoutEffectзапускается, очищая очередь рендеринга и обновляя DOM.
Диаграмма архитектуры: Жизненный цикл обновления
Справочник API
Методы контроллера
// Обновить свойство поля
controller.update(path: string, property: string, value: any): Promise<boolean>
// Слить с существующим значением свойства
controller.merge(path: string, property: string, value: object): Promise<boolean>
// Получить значение свойства
controller.get(property: string, path?: string): any
// Унаследовать значение свойства вверх по дереву
controller.inherit(property: string, path: string, key?: string): any
// Зарегистрировать подписчика поля
controller.register(path: string, forceRender: () => void): () => void
Пример регистрации свойства
Свойства — это автономные объекты, определяющие методы жизненного цикла для управления определенным аспектом состояния дерева.
const StylesProperty = {
priority: 50,
fieldDefaults: { styles: {} },
// Объявляем, что это свойство зависит от 'vars' и 'settings'
dependencies: ['vars', 'settings'],
// --- Методы жизненного цикла ---
// Вычисляет объект 'styles' на основе текущего состояния поля.
// Запускается автоматически при изменении 'vars' или 'settings'.
derive: field => {
const newStyles = getComputedFieldStyles(
field.mode,
varName => field.controller.inherit('vars', field.path, varName),
field.type
);
return { styles: newStyles };
},
// Обрабатывает обновления от поля, например, controller.update('path', 'styles', ...)
// Это менее распространено для чисто производного свойства.
update: (field, controller, value) => {
return false; // Обычно производные свойства не обновляются вручную.
},
// Вызывается controller.update() для запуска процесса вывода.
invalidate: (field, controller, newValue, oldValue) => {
// Инвалидация проще для производных свойств. Основная логика находится в `derive`.
// Логика перерасчета контроллера обработает повторное вычисление.
// Для базового свойства, такого как 'vars', это запустит цепочку.
controller.rederive(field, ['styles']);
controller.cascade(field, ['styles']);
},
};
Property.register('styles', StylesProperty);
Эта архитектура обеспечивает надежную основу для рендеринга сложных UI, сохраняя при этом превосходную производительность и удобство для разработчиков.