Gov Design System v Nuxtu a hydratace, která potichu selhává
Postavil jsem ukázkovou aplikaci na Nuxtu 4 s komponentami Gov Design System CE. Vypadala v pořádku, konzole byla čistá. Přesto Vue při každém načtení zahodilo velkou část stránky, kterou poslal server, a postavilo ji znovu. Tady je, proč se to děje a co s tím.
Co se děje
Komponenty gov-* jsou webové komponenty napsané ve Stencilu a nepoužívají shadow DOM. Jakmile se komponenta zaregistruje, vezme své potomky a přesune je do vlastního HTML.
Nuxt mezitím dělá tohle:
- server pošle hotové HTML,
- prohlížeč ho zobrazí,
- Vue stránku „hydratuje“: projde DOM a porovná ho s tím, co by sám vykreslil.
Když se komponenty zaregistrují před krokem 3, Vue najde strom, který nepoznává. Potomci jsou jinde, přibyly obalové prvky. Vue to vyhodnotí jako nesoulad, komponenty zahodí a vykreslí je znovu. Uživatel vidí poskočení obsahu a formulářová pole na okamžik ztratí stav.
Proč to není vidět
V produkčním sestavení Vue podrobnosti o nesouladu nevypisuje. A pokud používáte nuxt-security, jeho volba removeLoggers odstraní z produkčního kódu volání console.* úplně. Konzole je pak čistá, i když se stránka při každém načtení staví dvakrát.
Chcete-li to změřit, zapněte si hlášení v produkčním sestavení:
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true' },
},
security: { removeLoggers: false },
})
Pak stačí otevřít stránku a spočítat varování Hydration ... mismatch. Hlídejte si, že měříte čerstvé sestavení: když build selže, starý výstup ve složce .output zůstane a měříte něco, co už neexistuje. Mně se to stalo třikrát.
Oprava: registrovat komponenty až po hydrataci
Stačí počkat, až Vue stránku převezme, a teprve potom komponenty zaregistrovat:
// app/plugins/design-system.client.ts
import { ComponentLibrary } from '@gov-design-system-ce/vue/plugin'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:suspense:resolve', async () => {
await ComponentLibrary.install?.(nuxtApp.vueApp)
})
})
Tím nesoulady zmizí. Objeví se ale tři vedlejší účinky.
1. Atributy zapsané pod špatným jménem
Na prvek, který ještě není zaregistrovaný, Vue neumí nastavit vlastnost, a tak zapíše atribut pod jejím jménem: z nativeType vznikne atribut nativetype. Komponenta ale čte native-type, takže odesílací tlačítko zůstane obyčejným tlačítkem.
Po registraci už komponenta ví, které atributy sleduje (observedAttributes), takže je lze dorovnat:
function passOnEarlyAttributes(): void {
for (const element of document.querySelectorAll('*')) {
if (!element.localName.startsWith('gov-')) continue
const component = customElements.get(element.localName) as { observedAttributes?: string[] } | undefined
for (const name of component?.observedAttributes ?? []) {
const written = name.replaceAll('-', '')
if (written !== name && element.hasAttribute(written) && !element.hasAttribute(name)) {
element.setAttribute(name, element.getAttribute(written) ?? '')
}
}
}
}
Volá se hned po ComponentLibrary.install.
2. Poskočení obsahu
Než se komponenty zaregistrují, mají nulovou výšku nebo ukazují jen svůj text. Po registraci vyrostou a stránka poskočí. Pomáhá vyhradit jim místo předem. Selektor :not(:defined) nestačí, protože prvek je „defined“ dřív, než se vykreslí. Stencil ale po vykreslení přidá třídu hydrated:
gov-form-input:not(.hydrated),
gov-form-select:not(.hydrated) {
display: block;
min-height: 2.5rem;
}
3. v-if uvnitř komponenty
v-if přímo uvnitř gov-* komponenty je past. Vue si pamatuje, kde má jeho zástupný uzel být, jenže komponenta ho mezitím přesunula. Při přepnutí podmínky pak spadne na removeChild. Uvnitř těchto komponent používejte v-show:
<GovFormControl>
<GovFormInput :invalid="!!error" />
<GovFormMessage v-show="error" variant="error">{{ error }}</GovFormMessage>
</GovFormControl>
Výsledek
Po úpravách hlásí produkční sestavení 0 nesouladů a kumulativní posun obsahu (CLS) je nejvýš 0,02. Celá ukázka včetně testů je na GitHubu.
Měřeno s Gov Design System CE 4.7, Nuxtem 4.6 a Vue 3.5.