Portabilidade de Surface
Um micro frontend app recebe uma surface — a área retangular que o Web Host aloca para ele. Essa área geralmente não é a janela do navegador: o app pode ser um painel entre vários em um layout multi-painel, e o mesmo app pode ser renderizado por qualquer um dos motores de renderização em tamanhos diferentes na mesma tela.
Dimensionar um layout pela janela é, portanto, errado nos dois motores. O contrato de surface oferece uma alternativa portável em CSS e em JavaScript.
Status: contrato 1, entregue. As variantes
surface-*do Tailwind, a rolagem mediada pelo host e o hit testing profundo ainda não foram entregues; esta página documenta apenas o que existe hoje.
O contrato em CSS
Container queries
O host nomeia a caixa do app como wippy-surface, de modo que ela pode ser consultada como qualquer container CSS:
@container wippy-surface (min-width: 640px) {
.sidebar { display: block; }
}
Use isso em vez de @media (min-width: 640px) para qualquer coisa que responda ao espaço que o app ocupa. Unidades nativas de container resolvem contra a mesma caixa:
.hero { inline-size: 50cqw; }
Variáveis de surface
Quatro custom properties carregam a geometria como comprimentos simples em pixels:
| Propriedade | Significado |
|---|---|
--wippy-surface-width |
largura completa da surface |
--wippy-surface-width-unit |
1% da largura da surface |
--wippy-surface-height |
altura completa da surface (apenas dimensionamento por container) |
--wippy-surface-height-unit |
1% da altura da surface (apenas dimensionamento por container) |
Elas são a substituição portável para vw / vh:
/* era: inline-size: 50vw */
.panel { inline-size: calc(var(--wippy-surface-width-unit) * 50); }
Os valores são herdados, então qualquer elemento do app pode lê-los. Eles reportam o content box da caixa de consulta, que é a mesma caixa contra a qual 100cqw resolve.
As aplicações não devem declarar nem atribuir esses quatro nomes. Uma declaração descendente sombreia o valor herdado e silenciosamente desprende o app da surface.
Elas também precisam permanecer não registradas. Não as descreva com @property nem com CSS.registerProperty(). O host marca o eixo de bloco como indisponível atribuindo um valor garantidamente inválido, que computa para a string vazia apenas enquanto a propriedade estiver não registrada. Dê a uma delas um initial-value e ela computa para esse valor, de modo que um app dimensionado por conteúdo se reporta como dimensionado por container e supports('block-size') passa a retornar true — sem erro em lugar nenhum.
Duas ressalvas antes de comparar esses valores com 100cqw pixel a pixel. O primeiro frame pode ser mais largo: o valor de boot é semeado a partir do elemento <iframe> do lado do host antes de o documento do app existir, então ele não tem como saber se o conteúdo levantará uma barra de rolagem. Esse valor é embutido no CSS do documento, então o primeiro layout o usa e é corrigido um frame depois. E os valores são quantizados em 1/64 px, então compare com uma tolerância.
Dimensionamento por container e dimensionamento por conteúdo
| Eixo inline | Eixo de bloco | |
|---|---|---|
| Dimensionamento por container — o host impõe as duas dimensões | disponível | disponível |
| Dimensionamento por conteúdo — o conteúdo do app decide a altura | disponível | indisponível |
No dimensionamento por conteúdo, as propriedades de altura são deliberadamente inválidas, então var(--wippy-surface-height, 400px) recorre ao fallback em vez de reportar um número, e @container wippy-surface (min-height: …) nunca casa.
Qual dos dois um app recebe não é escolha do autor, e nada no package.json muda isso. O dimensionamento é definido por onde o Web Host renderiza o app:
| Renderizado como | Dimensionamento |
|---|---|
| uma página roteada, um painel de layout, o painel direito, uma aba de registry | container |
| um artefato embutido, um bloco de artefato inline, um widget da navbar | conteúdo |
Então o mesmo pacote é dimensionado por container em sua própria rota e dimensionado por conteúdo quando alguém o embute. Um app que precisa do eixo de bloco precisa, portanto, tolerar não tê-lo, ou declarar o requisito (abaixo) para que seja recusado em vez de renderizado quebrado. Leia o modo atual com host.surface.snapshot.sizing e condicione o comportamento a host.surface.supports('block-size') — nunca presuma.
cqh se comporta pior do que "indisponível": unidades de container recorrem ao small viewport quando nenhum container fornece o eixo de que precisam, então cqh produz silenciosamente um número plausível sem relação com a surface. Prefira var(--wippy-surface-height, <fallback>), que é fixado na raiz e cai visivelmente para o fallback. A mesma armadilha aparece dentro de um app que declara container-type: inline-size em um elemento intermediário e depois usa cqh abaixo dele.
Declarando requisitos
Opcional, no package.json do app:
{
"wippy": {
"path": "index.html",
"surface": {
"contract": 1,
"requirements": ["block-size"]
}
}
}
Os tokens aceitos são block-size e surface-scroll, ambos exigindo dimensionamento por container e sendo rejeitados quando a instância é dimensionada por conteúdo. registered-hit-testing, native-document-hit-testing e owner-visibility são vocabulário reservado e são rejeitados como não implementados, em vez de ignorados silenciosamente.
A validação roda antes da inicialização, então uma declaração insatisfazível falha visivelmente em vez de renderizar um app cujas queries de eixo de bloco nunca casam. Um app sem um bloco surface ainda renderiza e ainda recebe a caixa de consulta e as variáveis; ele simplesmente não anuncia portabilidade.
surface-scroll é aceito e reportado por supports(), mas esta release não entrega nenhuma API de rolagem mediada pelo host — declará-lo afirma uma intenção, não desbloqueia um método.
Lendo a surface a partir do JavaScript
Veja API do Proxy → Surface para a assinatura completa.
const { width, widthUnit, height, sizing } = host.surface.snapshot
if (host.surface.supports('block-size')) {
// seguro depender do eixo de bloco
}
const off = host.surface.onChange((s) => reposition(s.width, s.height))
// chame off() no teardown
O snapshot é lido de volta a partir das mesmas custom properties computadas que o CSS resolve, então ele não pode divergir do que @container e cqw enxergam.
Prefira CSS para layout. Recorra à API JavaScript onde o CSS não alcança: dimensionamento de canvas, matemática de virtualização, seleção de recursos e estilos gerados em tempo de execução.
engine: 'host'
host.surface.engine reporta iframe, fragment ou host. O último não é um motor de página — significa que o código está rodando onde nenhuma surface foi alocada:
- um web component montado diretamente no documento do host em vez de em uma página;
- o dev proxy standalone, sem nenhum Web Host.
Ali, o snapshot reporta width: 0, height: null, sizing: 'content', e supports() é false para tudo. Isso é deliberado: substituir pela janela do navegador seria a falsa equivalência que o contrato existe para evitar. Um componente montado diretamente deve medir sua própria raiz em vez disso.
O que o contrato não cobre
Container queries substituem media queries em CSS. Estes mecanismos vivem fora do CSS e continuam seguindo a janela do navegador:
| Mecanismo | Por quê | O que fazer |
|---|---|---|
<picture> / <source media> |
seleção de recursos em HTML; sem forma em container query | Controle a partir de host.surface.onChange, ou mova a direção de arte para um background-image em CSS sob @container |
srcset + sizes |
resolvem contra o viewport | Derive sizes a partir da surface, ou defina a fonte pelo JS |
matchMedia() |
pergunta à janela por definição | Use host.surface.onChange para geometria; mantenha matchMedia para preferências |
Overlays
O contrato de surface não captura position: fixed. container-type estabelece um contexto de formatação independente sem containment de layout, então um container de consulta computa contain: none e não ancora nada. Overlays do PrimeVue e overlays fixos feitos à mão continuam funcionando, sem mudanças.
O comportamento do motor é uma questão separada: no motor Web Fragment, position: fixed resolve contra a janela do host em vez do painel do app. Veja Motores de Renderização e fixe o app com wippy.renderEngine: "iframe" se a ancoragem exata ao viewport importar.
Dimensionar um overlay é uma questão diferente de ancorá-lo. Para um backdrop ou drawer que deve cobrir exatamente a surface, abandone as unidades de viewport e use inset: 0 — mas combine isso com o esquema de posicionamento que corresponde ao quão portável o app precisa ser:
/* Portável nos DOIS motores: resolve contra a própria raiz do app em vez de
contra o que quer que `fixed` acabe usando como referência.
`min-block-size: 100%` é estrutural — veja abaixo. */
.app-root { position: relative; min-block-size: 100%; }
.backdrop { position: absolute; inset: 0; }
O bloco contêiner é a raiz do app, não a surface, então o overlay cobre a surface apenas se essa raiz o fizer. No dimensionamento por conteúdo, isso acontece automaticamente (o conteúdo é a altura). No dimensionamento por container, o host impõe uma altura à caixa de consulta que a raiz do app não herda, então, sem min-block-size: 100%, o backdrop para discretamente antes do fim — falhando exatamente no modo em que a versão com fixed teria parecido correta. Os dois também diferem no comportamento: absolute rola com o conteúdo, fixed fica preso.
Coloque min-block-size: 100% no elemento mais externo dentro da surface. Uma altura percentual precisa de uma cadeia ininterrupta de alturas definidas acima dela, então aplicá-la a uma raiz de componente aninhada dentro de um #app de altura automática resolve para zero e reintroduz a mesma lacuna. Verificado em Chromium, Firefox e WebKit, com o caso sem min como controle.
/* Apenas no motor de iframe. `fixed` resolve contra o viewport do filho, que
ALI É a surface — mas contra a JANELA DO HOST no motor de fragment, onde
isso cobre a aplicação inteira em vez do painel. */
.backdrop { position: fixed; inset: 0; }
Evite var(--wippy-surface-height) para isso: ele é indisponível no dimensionamento por conteúdo, então um backdrop escrito dessa forma colapsa exatamente nas páginas em que é mais difícil de notar.
O elemento raiz do app (#app)
O motor Web Fragment exige que seu elemento raiz seja id="app". Não
#root, não #main, não <main> — o id é comparado literalmente.
O motor vincula a cadeia de altura da página a esse seletor e mede a altura do
seu conteúdo através dele. O documento refletido expõe wf-html/wf-body em vez
de html/body, então você não pode construir a cadeia a partir da raiz do
documento como faria dentro de um iframe.
Sintoma quando está errado: uma página fragment dimensionada por conteúdo cuja
raiz é #root (ou qualquer outra coisa) renderiza com altura zero — painel em
branco, sem erro no seu próprio código. O host registra um erro nomeando o
requisito. O motor de iframe não é afetado, porque ele tira a altura de
CmdBodySize, então o mesmo pacote pode parecer correto ali e ficar em branco
como fragment.
<!-- correto -->
<body><div id="app"></div></body>
createApp(App).mount('#app')
Não tente corrigir um fragment de altura zero dando uma altura ao #root.
Adicionar height: 100%, min-height: 100dvh ou 100vh a uma raiz com outro
nome não faz o motor medi-la, e unidades de viewport são erradas aqui pela razão
pela qual esta página inteira existe — elas descrevem a janela do navegador, não a
sua surface. Renomeie o elemento para app.
Limitações
- Caixa do body. No motor de iframe, o host zera
margin,paddingebordernobodydo app para que a surface alocada seja bem definida. Coloque o padding da página no seu próprio elemento raiz. O motor de fragment não faz isso, então um app que depende de padding no body renderiza um pouco diferente entre os motores. Ainda não há diagnóstico em tempo de build para isso. - Seletores
body > *e regras que miramhtml/body. No motor de iframe, o host envolve o conteúdo do body na caixa de surface, então seletores de filho direto enraizados embodynão casam mais com elementos do app, ebody/htmlse tornam ancestrais da caixa de consulta — uma regra@containerque os mire nunca se aplica. O motor de fragment tem a topologia oposta (a caixa de consulta fica acima da árvore refletida), mas um seletor literalbodyainda falha ali, porque o documento refletido é renomeado parawf-html/wf-body. Coloque essas regras no seu próprio elemento raiz dentro da surface; isso é correto nos dois motores. - Qualquer coisa renderizada através de
<w-iframe>/<w-artifact>não recebe surface — incluindo um painel gerenciado de nível superior. Esses elementos sempre constroem seu documento filho com o bootstrap de surface desabilitado e nada os mede, entãohost.surfacereportawidth: 0esizing: 'content'— mas comengine: 'iframe', nãoengine: 'host'. Verifiquesnapshot.widthem vez deenginese seu componente puder ser embutido dessa forma. Isso é esperado para um embed aninhado; é fácil deixar passar no caso de um painel de layout gerenciado declarado como{ kind: 'component', tagName: 'w-artifact' }, que é um slot de nível superior em tamanho completo e ainda assim não recebe contrato. Usekind: 'page'para conteúdo que precise de um. - Sem eixo de bloco no dimensionamento por conteúdo.
- O motor de fragment exige que o elemento raiz do app seja
#app. Ele vincula a cadeia de altura da página a esse seletor e mede a altura do conteúdo através dele, porque o documento refletido expõewf-html/wf-bodyem vez dehtml/body, então um app não consegue construir sua própria cadeia a partir da raiz como faria dentro de um iframe. Um app fragment dimensionado por conteúdo com outra raiz (#root,<main>) não pode ser medido: o host registra um erro nomeando o requisito e o painel renderiza com altura zero. O motor de iframe não é afetado — ele tira a altura deCmdBodySize. - A rota deprecada
/page/:idnão recebe surface. Ela renderiza em um iframe puro que nunca mede nada, então opta por sair completamente — sem caixa de consulta, sem wrapper, sem mudança no DOM do app. Um app se comporta ali exatamente como se comportava antes deste contrato existir. Use/c/:idpara obter uma surface. Como nos embeds aninhados, ela ainda reportaengine: 'iframe', então testesnapshot.widthem vez do nome do motor. - Os dois motores podem diferir por uma barra de rolagem. O motor de iframe mede o eixo inline a partir da caixa de consulta dentro do documento do app, então uma barra de rolagem de documento o estreita. O motor de fragment mede um wrapper no documento do host, que a rolagem do conteúdo refletido não estreita. Mesmo painel alocado e mesmo conteúdo rolável: o motor de fragment reporta o número ligeiramente maior.
- Não é um limite de isolamento. O contrato governa layout. Ele não dá a um fragment documento, viewport, seleção, top layer ou origem independentes.
Migração
Migração de Surface traz conversões receita por receita para apps existentes, cada uma rotulada como automática, condicional, manual ou não convertível.