Переносимость 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 собраны порецептные преобразования для существующих приложений, каждое помечено как автоматическое, условное, ручное или непреобразуемое.