title: 'Архитектура Edit и Save, сериализация грамматики блоков и миграции deprecated' description: >- Инженерный разбор жизненного цикла статических блоков в Gutenberg (WordPress 7.0+, TypeScript 5.8): работа useBlockProps внутри изолированного iframe-холста, алгоритм верификации AST при открытии поста и бесшовная миграция верстки через массив deprecated без ошибок валидации. module: gutenberg-blocks order: 2 difficulty: intermediate estimatedTime: 38 мин topics:
Главное отличие архитектуры Gutenberg от традиционных React-фреймворков заключается в том, что статический блок живёт в двух совершенно разных временных измерениях.
Первое измерение — Interactive Authoring Runtime (Edit): полноценное React-приложение внутри редактора WordPress, подключённое к реактивному стору @wordpress/data (core/block-editor, core/data), где редактор меняет настройки в сайдбаре (InspectorControls), правит текст на лету (RichText) и перетаскивает элементы внутри изолированного <iframe>-холста.
Второе измерение — Deterministic AST Serializer (save): чистая синхронная функция без состояния и без React-хуков жизненного цикла (useState, useEffect, useSelect в ней категорически запрещены). В момент нажатия кнопки «Сохранить» в админке функция save({ attributes }) преобразует JSON-объект атрибутов в строку чистого HTML, обрамлённую комментариями грамматики WordPress (<!-- wp:agentic-lab/callout {...} -->...<!-- /wp:agentic-lab/callout -->), и записывает её прямо в колонку wp_posts.post_content. На публичном фронтенде этот HTML отдаётся браузеру мгновенно без запуска React и без создания Virtual DOM.
Цена такой скорости на фронтенде — строгий Контракт Детерминированной Валидации (Block Validation Contract). При каждом повторном открытии статьи в редакторе Gutenberg ядро берёт сохранённый в БД HTML, параллельно прогоняет атрибуты через текущую функцию save() и посимвольно сравнивает оба DOM-дерева. Если разработчик обновил плагин и поменял <h2> на <h3> или добавил один CSS-класс в save(), не описав правило обратной совместимости в массиве deprecated, все ранее созданные блоки на сотнях страниц сайта мгновенно выпадут в критическую ошибку «This block contains unexpected or invalid content».
Архитектурная парадигма статического блока Gutenberg разделяет жизненный цикл на интерактивную фазу редактирования (Runtime Authoring) и детерминированную сериализацию состояния в AST/HTML-снимок (Serialization Phase).
Компонент Edit управляет мутациями структуры и атрибутов через реактивный стор @wordpress/data, в то время как функция save() преобразует снимок атрибутов в чистый HTML-фрагмент, обрамляемый блок-комментариями грамматики WordPress и персистентно сохраняемый в wp_posts.post_content.
При повторной инициализации редактора ядро выполняет верификацию детерминированности: AST-парсер извлекает сохранённый HTML и атрибуты, повторно исполняет текущую функцию save() с извлечёнными атрибутами и сравнивает сгенерированное дерево с HTML из базы данных. Любое структурное расхождение без зарегистрированного адаптера в конвейере deprecated трактуется как нарушение целостности данных (Block Validation Error).
┌─────────────────────────────────────────────────────────────────────────┐
│ ОТКРЫТИЕ ПОСТА В РЕДАКТОРЕ GUTENBERG (ВАЛИДАЦИЯ) │
└────────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼
[HTML из базы данных] [Атрибуты из комментария]
<!-- wp:agentic/cta {"v":1} --> Прогон через текущий save(attrs)
<div class="cta-v1">...</div> │
│ ▼
│ <section class="cta-v2">...
└───────────────────────────┬───────────────────────────┘
▼
Совпадают ли оба DOM-дерева?
├── ДА ──► Блок монтируется в Edit()
└── НЕТ ─► Обход массива deprecated[0..N]:
Найдена совпавшая старая версия save()?
├── ДА ──► Вызов migrate(oldAttrs) ──► Авто-апгрейд в v2!
└── НЕТ ─► КРИТИЧЕСКАЯ ОШИБКА: Block Validation Error!Парсер @wordpress/block-serialization-default-parser разбивает post_content на дерево блоков, извлекая JSON-атрибуты и innerHTML.
Ядро вызывает актуальную функцию save({ attributes }) в памяти браузера и сериализует полученный React-элемент в эталонную HTML-строку.
Алгоритм isEquivalentHTML нормализует пробелы и порядок атрибутов и сравнивает эталонную строку с сохранённым в базе innerHTML.
При несовпадении ядро по очереди тестирует записи из массива deprecated[]. Первая совпавшая запись трансформирует атрибуты через migrate().
Валидный блок передаётся в компонент Edit(), который привязывает обработчики фокуса и выделения к корневому узлу через useBlockProps().
Перед внесением любых правок в HTML скопируйте текущие объекты attributes и функцию save() в новый элемент массива deprecated[].
Реализуйте метод migrate(), который принимает старый словарь атрибутов и возвращает полный объект атрибутов новой версии со всеми дефолтными полями.
Внесите новые семантические теги, классы и контролы в основной компонент блока, сохраняя старую версию в deprecated неприкосновенной.
Вставьте старый HTML-комментарий v1 в редактор кода Gutenberg и убедитесь, что при переключении в визуальный режим блок мигрирует в v2 тихо и без ошибок в консоли.
Почему в функции save() категорически запрещено использовать React-хуки (useState, useEffect, useSelect) и недетерминированные вызовы вроде Date.now() или Math.random()?
В проектах Fullstack Agentic Lab архитектура Edit / Save с массивом deprecated применяется для:
<div class="box">) на v2 (<aside class="agentic-callout">), массив deprecated автоматически мигрирует тысячи старых статей при их открытии в редакторе без единого ручного исправления.@wordpress/data): внутри Edit() блок может динамически подтягивать список авторов, таксономий или глобальных настроек сайта через useSelect(), сохраняя в итоговый статический save() только выверенный слепок или делегируя динамику в render.php.Сравним архитектурные правила и ограничения между средой редактирования Edit() и сериализатором save():
Ниже представлен боевой производственный артефакт на TypeScript 5.8 для блока инженерного уведомления (agentic-lab/architecture-alert). Он демонстрирует правильную работу с useBlockProps в apiVersion: 3, разделение панелей BlockControls и InspectorControls, а также полноценный конвейер миграции deprecated при изменении как HTML-структуры (div → section), так и схемы атрибутов (alertText → heading + severityLevel).
/**
* Fullstack Agentic Lab — Architecture Alert Block (Block API v3)
* Стек: WordPress 7.0+, TypeScript 5.8, @wordpress/block-editor
*/
import { registerBlockType, type BlockDeprecation } from '@wordpress/blocks';
import {
useBlockProps,
RichText,
InspectorControls,
BlockControls,
} from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
ToolbarGroup,
ToolbarButton,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
// 1. Актуальный контракт атрибутов версии v2 (2026)
export interface AlertBlockAttributesV2 {
heading: string;
bodyContent: string;
severityLevel: 'info' | 'warning' | 'critical';
isDismissible: boolean;
}
// Устаревший контракт атрибутов версии v1 (для строгой типизации мигратора)
interface AlertBlockAttributesV1 {
alertText: string;
isDanger: boolean;
}
/**
* 2. Массив обратной совместимости (deprecated).
* Располагается в порядке от самой свежей устаревшей версии к самой старой.
*/
const deprecatedMigrations: BlockDeprecation<AlertBlockAttributesV2>[] = [
{
// Схема атрибутов старой версии v1
attributes: {
alertText: {
type: 'string',
default: '',
},
isDanger: {
type: 'boolean',
default: false,
},
},
// Проверка, подходит ли этот мигратор для конкретного старого блока
isEligible(attributes: Record<string, unknown>): boolean {
return typeof attributes.alertText === 'string' || typeof attributes.isDanger === 'boolean';
},
// Чистая функция трансформации атрибутов v1 -> v2
migrate(oldAttributes: Record<string, unknown>): AlertBlockAttributesV2 {
const v1 = oldAttributes as unknown as AlertBlockAttributesV1;
return {
heading: v1.isDanger ? 'Критическое архитектурное ограничение' : 'Инженерное примечание',
bodyContent: v1.alertText || '',
severityLevel: v1.isDanger ? 'critical' : 'info',
isDismissible: false,
};
},
// Точная копия старой функции save() версии v1!
// Нужна только для того, чтобы парсер убедился, что HTML в БД был создан легально.
save({ attributes }: { attributes: Record<string, unknown> }) {
const v1 = attributes as unknown as AlertBlockAttributesV1;
const legacyProps = useBlockProps.save({
className: `agentic-alert-v1 ${v1.isDanger ? 'is-danger' : 'is-info'}`,
});
return (
<div {...legacyProps}>
<RichText.Content tagName="p" value={v1.alertText} />
</div>
);
},
},
];
/**
* 3. Регистрация блока версии v2 с поддержкой apiVersion: 3.
*/
registerBlockType<AlertBlockAttributesV2>('agentic-lab/architecture-alert', {
apiVersion: 3,
title: __('Architecture Decision Alert', 'agentic-lab'),
category: 'agentic-lab-architecture',
icon: 'shield-alt',
edit({ attributes, setAttributes }) {
const { heading, bodyContent, severityLevel, isDismissible } = attributes;
// useBlockProps автоматически инжектирует ref и классы внутри изолированного iframe
const blockProps = useBlockProps({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<>
{/* Быстрое переключение критичности прямо во всплывающем тулбаре над блоком */}
<BlockControls>
<ToolbarGroup>
{(['info', 'warning', 'critical'] as const).map((level) => (
<ToolbarButton
key={level}
label={`Уровень: ${level.toUpperCase()}`}
isPressed={severityLevel === level}
onClick={() => setAttributes({ severityLevel: level })}
>
{level.toUpperCase()}
</ToolbarButton>
))}
</ToolbarGroup>
</BlockControls>
{/* Детальные настройки в правом сайдбаре (Inspector) */}
<InspectorControls>
<PanelBody title={__('Параметры алерта', 'agentic-lab')} initialOpen={true}>
<SelectControl
label={__('Уровень критичности (Severity)', 'agentic-lab')}
value={severityLevel}
options={[
{ label: 'Info (Инженерная заметка)', value: 'info' },
{ label: 'Warning (Предупреждение)', value: 'warning' },
{ label: 'Critical (Блокер продакшена)', value: 'critical' },
]}
onChange={(value) =>
setAttributes({ severityLevel: value as AlertBlockAttributesV2['severityLevel'] })
}
/>
<ToggleControl
label={__('Разрешить скрытие пользователем', 'agentic-lab')}
checked={isDismissible}
onChange={(val) => setAttributes({ isDismissible: Boolean(val) })}
/>
</PanelBody>
</InspectorControls>
{/* Семантическая разметка v2 внутри iframe-холста */}
<section {...blockProps} role="note">
<header className="agentic-alert-v2__header">
<span className="agentic-alert-v2__badge">{severityLevel.toUpperCase()}</span>
<RichText
tagName="h4"
className="agentic-alert-v2__title"
value={heading}
onChange={(val) => setAttributes({ heading: val })}
placeholder={__('Заголовок архитектурного правила...', 'agentic-lab')}
allowedFormats={['core/code']}
/>
</header>
<RichText
tagName="div"
className="agentic-alert-v2__body"
value={bodyContent}
onChange={(val) => setAttributes({ bodyContent: val })}
placeholder={__('Опишите технический контекст и последствия...', 'agentic-lab')}
allowedFormats={['core/bold', 'core/italic', 'core/code', 'core/link']}
/>
</section>
</>
);
},
// 4. Детерминированный сериализатор v2 (100% чистая функция без сайд-эффектов!)
save({ attributes }) {
const { heading, bodyContent, severityLevel, isDismissible } = attributes;
const blockProps = useBlockProps.save({
className: `agentic-alert-v2 is-severity-${severityLevel}`,
'data-dismissible': isDismissible ? 'true' : 'false',
});
return (
<section {...blockProps} role="note">
<header className="agentic-alert-v2__header">
<span className="agentic-alert-v2__badge">{severityLevel.toUpperCase()}</span>
<RichText.Content tagName="h4" className="agentic-alert-v2__title" value={heading} />
</header>
<RichText.Content tagName="div" className="agentic-alert-v2__body" value={bodyContent} />
</section>
);
},
// Подключаем конвейер миграции со старой вёрстки v1
deprecated: deprecatedMigrations,
});
Разработчик изменил разметку блока в функции save(), обернув текстовое поле в дополнительный контейнер div с классом wrapper, но не обновил массив deprecated. Что произойдёт при открытии существующей статьи с этим блоком в редакторе Gutenberg?