Переносимость surface
Приложению-микрофронтенду выделяется surface — прямоугольная область, которую отводит ему Web Host. Эта область обычно не является окном браузера: приложение может быть одной панелью среди нескольких в многопанельной раскладке, и одно и то же приложение может отрисовываться любым из движков отрисовки с разными размерами на одном экране.
Поэтому подгонка вёрстки под окно неверна в обоих движках. Контракт surface даёт переносимую альтернативу в CSS и в JavaScript.
Статус: контракт 1, выпущен. Варианты Tailwind
surface-*, прокрутка через посредничество хоста и глубокий hit testing ещё не выпущены; эта страница документирует только то, что существует сегодня.
Контракт CSS
Container queries
Хост называет блок приложения wippy-surface, поэтому к нему можно обращаться как к любому CSS-контейнеру:
@container wippy-surface (min-width: 640px) {
.sidebar { display: block; }
}
Используйте это вместо @media (min-width: 640px) для всего, что реагирует на занимаемое приложением пространство. Нативные единицы контейнера разрешаются относительно того же блока:
.hero { inline-size: 50cqw; }
Переменные surface
Четыре пользовательских свойства несут геометрию в виде обычных пиксельных длин:
| Свойство | Значение |
|---|---|
--wippy-surface-width |
полная ширина surface |
--wippy-surface-width-unit |
1% ширины surface |
--wippy-surface-height |
полная высота surface (только при container sizing) |
--wippy-surface-height-unit |
1% высоты surface (только при container sizing) |
Это переносимая замена vw / vh:
/* было: inline-size: 50vw */
.panel { inline-size: calc(var(--wippy-surface-width-unit) * 50); }
Значения наследуются, поэтому их может прочитать любой элемент приложения. Они сообщают content box блока запроса — тот же блок, относительно которого разрешается 100cqw.
Приложения не должны объявлять или присваивать эти четыре имени. Объявление у потомка затеняет унаследованное значение и молча отвязывает приложение от surface.
Они также должны оставаться незарегистрированными. Не описывайте их через @property или CSS.registerProperty(). Хост помечает блочную ось недоступной, присваивая гарантированно невалидное значение, которое вычисляется в пустую строку только пока свойство не зарегистрировано. Дайте ему initial-value — и оно вычислится в это значение, поэтому приложение с content sizing сообщит о себе как об использующем container sizing, а supports('block-size') начнёт возвращать true, причём без единой ошибки.
Две оговорки, прежде чем сравнивать эти значения с 100cqw попиксельно. Первый кадр может быть шире: значение при загрузке засевается из элемента <iframe> на стороне хоста ещё до того, как документ приложения существует, поэтому оно не может знать, вызовет ли содержимое появление полосы прокрутки. Это значение запекается в CSS документа, поэтому первая раскладка использует его и исправляется кадром позже. Кроме того, значения квантованы до 1/64 px, поэтому сравнивайте с допуском.
Container sizing и content sizing
| Строчная ось | Блочная ось | |
|---|---|---|
| Container sizing — хост задаёт оба измерения | доступна | доступна |
| Content sizing — высоту определяет содержимое приложения | доступна | недоступна |
При content sizing свойства высоты намеренно невалидны, поэтому var(--wippy-surface-height, 400px) откатывается к запасному значению вместо того, чтобы сообщать число, а @container wippy-surface (min-height: …) никогда не срабатывает.
Какой режим получит приложение — не выбор автора, и ничто в package.json этого не меняет. Способ определения размера задаётся тем, где Web Host отрисовывает приложение:
| Отрисовано как | Способ определения размера |
|---|---|
| страница по маршруту, панель раскладки, правая панель, вкладка реестра | container |
| встроенный артефакт, встроенный блок артефакта, виджет панели навигации | content |
Таким образом, один и тот же пакет получает container sizing на собственном маршруте и content sizing, когда его кто-то встраивает. Поэтому приложение, которому нужна блочная ось, должно уметь обходиться без неё либо объявить требование (см. ниже), чтобы получить отказ, а не сломанную отрисовку. Читайте текущий режим через host.surface.snapshot.sizing и ставьте поведение под условие host.surface.supports('block-size') — никогда не предполагайте.
cqh ведёт себя хуже, чем «недоступно»: единицы контейнера откатываются к small viewport, когда ни один контейнер не предоставляет нужную им ось, поэтому cqh молча выдаёт правдоподобное число, не связанное с surface. Предпочитайте var(--wippy-surface-height, <fallback>), привязанное к корню и заметно откатывающееся. Та же ловушка возникает внутри приложения, которое объявляет container-type: inline-size на промежуточном элементе и затем использует cqh ниже него.
Объявление требований
Необязательно, в package.json приложения:
{
"wippy": {
"path": "index.html",
"surface": {
"contract": 1,
"requirements": ["block-size"]
}
}
}
Принимаются токены block-size и surface-scroll, оба требуют container sizing и отклоняются, когда экземпляр использует content sizing. registered-hit-testing, native-document-hit-testing и owner-visibility — зарезервированный словарь, они отклоняются как нереализованные, а не игнорируются молча.
Валидация выполняется до запуска, поэтому невыполнимое объявление приводит к явному сбою вместо отрисовки приложения, чьи запросы по блочной оси никогда не срабатывают. Приложение без блока surface всё равно отрисовывается и всё равно получает блок запроса и переменные; оно просто не заявляет переносимость.
surface-scroll принимается и сообщается через supports(), но этот релиз не поставляет API прокрутки через посредничество хоста — объявление заявляет намерение, но не открывает метод.
Чтение surface из JavaScript
Полную сигнатуру см. в Proxy API → Surface.
const { width, widthUnit, height, sizing } = host.surface.snapshot
if (host.surface.supports('block-size')) {
// на блочную ось можно опираться
}
const off = host.surface.onChange((s) => reposition(s.width, s.height))
// вызовите off() при демонтировании
Снимок считывается из тех же вычисленных пользовательских свойств, которые разрешает CSS, поэтому он не может разойтись с тем, что видят @container и cqw.
Предпочитайте CSS для вёрстки. Обращайтесь к JavaScript API там, куда CSS не дотягивается: определение размеров canvas, математика виртуализации, выбор ресурсов и стили, генерируемые во время выполнения.
engine: 'host'
host.surface.engine сообщает iframe, fragment или host. Последнее — не движок страниц: оно означает, что код выполняется там, где surface не выделялся:
- веб-компонент, смонтированный напрямую в документ хоста, а не в страницу;
- автономный dev-прокси, вообще без Web Host.
Там снимок сообщает width: 0, height: null, sizing: 'content', а supports() возвращает false для всего. Это сделано намеренно: подстановка окна браузера была бы той самой ложной эквивалентностью, ради избежания которой контракт существует. Компонент, смонтированный напрямую, должен измерять собственный корень.
Что контракт не покрывает
Container queries заменяют медиазапросы в CSS. Перечисленные механизмы живут вне CSS и продолжают следовать за окном браузера:
| Механизм | Почему | Что делать |
|---|---|---|
<picture> / <source media> |
Выбор ресурсов на уровне HTML; формы container query нет | Управляйте через host.surface.onChange либо перенесите арт-дирекшн в CSS background-image внутри @container |
srcset + sizes |
разрешаются относительно viewport | Выводите sizes из surface либо задавайте источник из JS |
matchMedia() |
по определению спрашивает окно | Используйте host.surface.onChange для геометрии; сохраните matchMedia для предпочтений |
Оверлеи
Контракт surface не захватывает position: fixed. container-type создаёт независимый контекст форматирования без layout containment, поэтому контейнер запроса вычисляет contain: none и ничего не якорит. И оверлеи PrimeVue, и самодельные fixed-оверлеи продолжают работать без изменений.
Поведение движка — отдельный вопрос: в движке Web Fragment position: fixed разрешается относительно окна хоста, а не панели приложения. См. Движки отрисовки и закрепите приложение через wippy.renderEngine: "iframe", если точная привязка к viewport важна.
Определение размера оверлея — вопрос, отличный от его якорения. Для подложки или выдвижной панели, которая должна покрывать ровно surface, откажитесь от единиц viewport и используйте inset: 0, но сочетайте это со схемой позиционирования, соответствующей требуемой переносимости приложения:
/* Переносимо между ОБОИМИ движками: разрешается относительно собственного корня
приложения, а не относительно того, к чему привязан `fixed`.
`min-block-size: 100%` несущее — см. ниже. */
.app-root { position: relative; min-block-size: 100%; }
.backdrop { position: absolute; inset: 0; }
Содержащим блоком является корень приложения, а не surface, поэтому оверлей покрывает surface только если его покрывает этот корень. При content sizing это происходит автоматически (содержимое и есть высота). При container sizing хост задаёт блоку запроса высоту, которую корень приложения не наследует, поэтому без min-block-size: 100% подложка тихо не дотягивается — отказывая ровно в том режиме, где вариант с fixed выглядел бы корректно. Различается и поведение: absolute прокручивается вместе с содержимым, fixed остаётся закреплённым.
Ставьте min-block-size: 100% на самый внешний элемент внутри surface. Процентной высоте нужна непрерывная цепочка определённых высот выше неё, поэтому применение её к корню компонента, вложенному в #app с автоматической высотой, разрешается в ноль и снова создаёт тот же зазор. Проверено в Chromium, Firefox и WebKit, с вариантом без min в качестве контроля.
/* Только движок iframe. `fixed` разрешается относительно дочернего viewport, который
ТАМ и есть surface, — но относительно ОКНА ХОСТА в движке фрагментов,
где это покроет всё приложение вместо панели. */
.backdrop { position: fixed; inset: 0; }
Избегайте здесь var(--wippy-surface-height): он недоступен при content sizing, поэтому написанная так подложка схлопывается ровно на тех страницах, где это труднее всего заметить.
Корневой элемент приложения (#app)
Движок Web Fragment требует, чтобы корневой элемент имел id="app". Не
#root, не #main, не <main> — id сопоставляется буквально.
Движок привязывает цепочку высот страницы к этому селектору и через него измеряет
высоту вашего содержимого. Отражённый документ предоставляет wf-html/wf-body,
а не html/body, поэтому построить цепочку от корня документа так, как это
возможно внутри iframe, нельзя.
Симптом при ошибке: страница-фрагмент с content sizing, корень которой —
#root (или что угодно другое), отрисовывается с нулевой высотой: пустая панель, никаких ошибок в
вашем коде. Хост логирует ошибку с указанием требования. Движок iframe
не затронут, поскольку берёт высоту из CmdBodySize, поэтому один и тот же пакет
может выглядеть нормально там и быть пустым в виде фрагмента.
<!-- правильно -->
<body><div id="app"></div></body>
createApp(App).mount('#app')
Не пытайтесь исправить фрагмент нулевой высоты, задав высоту #root. Добавление
height: 100%, min-height: 100dvh или 100vh к иначе названному корню не
заставит движок его измерять, а единицы viewport здесь неверны по той самой причине,
ради которой существует вся эта страница: они описывают окно браузера, а не ваш surface.
Вместо этого переименуйте элемент в app.
Ограничения
- Блок body. В движке iframe хост обнуляет
margin,paddingиborderуbodyприложения, чтобы выделенный surface был чётко определён. Отступы страницы задавайте на собственном корневом элементе. Движок фрагментов этого не делает, поэтому приложение, полагающееся на padding у body, отрисовывается между движками немного по-разному. Диагностики на этапе сборки для этого пока нет. - Селекторы
body > *и правила, нацеленные наhtml/body. В движке iframe хост оборачивает содержимое body в блок surface, поэтому селекторы прямых потомков отbodyбольше не совпадают с элементами приложения, аbody/htmlстановятся предками блока запроса — правило@container, нацеленное на них, никогда не применяется. У движка фрагментов обратная топология (блок запроса находится над отражённым деревом), но буквальный селекторbodyтам всё равно не работает, потому что отражённый документ переименован вwf-html/wf-body. Помещайте такие правила на собственный корневой элемент внутри surface; это корректно в обоих движках. - Всё, что отрисовывается через
<w-iframe>/<w-artifact>, не получает surface — включая управляемую панель верхнего уровня. Эти элементы всегда строят свой дочерний документ с отключённым bootstrap surface, и ничто их не измеряет, поэтомуhost.surfaceсообщаетwidth: 0иsizing: 'content'— но сengine: 'iframe', а неengine: 'host'. Проверяйтеsnapshot.width, а неengine, если ваш компонент может быть встроен таким образом. Для вложенного встраивания это ожидаемо; легко упустить это для панели управляемой раскладки, объявленной как{ kind: 'component', tagName: 'w-artifact' }, которая является полноразмерным слотом верхнего уровня и всё же не получает контракта. Для содержимого, которому он нужен, используйтеkind: 'page'. - Нет блочной оси при content sizing.
- Движок фрагментов требует, чтобы корневой элемент приложения был
#app. Он привязывает цепочку высот страницы к этому селектору и через него измеряет высоту содержимого, поскольку отражённый документ предоставляетwf-html/wf-body, а неhtml/body, поэтому приложение не может построить собственную цепочку от корня так, как это возможно внутри iframe. Приложение-фрагмент с content sizing и другим корнем (#root,<main>) невозможно измерить: хост логирует ошибку с указанием требования, и панель отрисовывается с нулевой высотой. Движок iframe не затронут — он берёт высоту изCmdBodySize. - Устаревший маршрут
/page/:idне получает surface. Он отрисовывается в голый iframe, который ничего не измеряет, поэтому полностью выходит из контракта: нет блока запроса, нет обёртки, нет изменений в DOM приложения. Приложение ведёт себя там ровно так, как до появления этого контракта. Чтобы получить surface, используйте/c/:id. Как и вложенные встраивания, он всё равно сообщаетengine: 'iframe', поэтому проверяйтеsnapshot.width, а не имя движка. - Два движка могут различаться на полосу прокрутки. Движок iframe измеряет строчную ось от блока запроса внутри документа приложения, поэтому полоса прокрутки документа его сужает. Движок фрагментов измеряет обёртку в документе хоста, которую прокрутка отражённого содержимого не сужает. При той же выделенной панели и том же прокручиваемом содержимом движок фрагментов сообщает чуть большее число.
- Это не граница изоляции. Контракт управляет вёрсткой. Он не даёт фрагменту независимый документ, viewport, выделение, top layer или origin.
Миграция
В разделе Миграция surface собраны порецептные преобразования для существующих приложений, каждое помечено как автоматическое, условное, ручное или непреобразуемое.