Behebung von Höhenübergängen bei Vue für asynchron gerenderte Webkomponenten
Erfahren Sie, warum die Expand-Übergangseffekt in Vue von 0px auf 0px bei langsam aktualisierten Web Components animiert wird, und wie ResizeObserver die Animation bis zum Erreichen der tatsächlichen Höhe verzögert.
Ein wiederverwendbarer Erweitern/Kollabieren-Component, der auf Vue’s <Transition> basiert, misst in der Regel die Höhe eines Elements im enter-Hook und animiert von null auf diesen Wert. Das funktioniert gut für native Elemente sowie gewöhnliche Vue-Componenten, kann jedoch stillschweigend versagen, wenn das Kind ein dynamisch geladenes Web Component ist: Das Panel erscheint dann ohne jegliche Animation. Diese Anleitung erläutert das Zeitproblem hinter diesem Versagen, warum Polling nur eine teilweise Lösung ist, und wie man ResizeObserver verwendet, damit die Übergangsanimation genau dann beginnt, wenn der Inhalt eine tatsächliche Größe hat.
Warum die Animation von 0px auf 0px läuft
Nehmen wir an, Fehlermeldungen werden durch ein Benachrichtigungs-Element dargestellt, das als Custom Element implementiert ist und in die Erweiterung/Verkleinerungs-Übergangsanimation eingebettet ist:
<transition-expand>
<notification-wrapper v-if="showError" />
</transition-expand>
Wenn showError auf true gesetzt wird, fügt Vue <notification-wrapper> in den DOM ein und ruft sofort den enter-Hook der Übergangsanimation auf. Für eine Vue-Komponente ist das in Ordnung, da deren Markup zu diesem Zeitpunkt bereits vorhanden ist. Viele Web Components werden jedoch asynchron aktualisiert: Der Tag landet zunächst als leeres, unbekanntes Element im DOM und erhält erst später, nachdem seine Definition geladen wurde und sein Shadow DOM gerendert ist, Inhalt sowie Höhe.
Die tatsächliche Abfolge der Ereignisse sieht so aus:
v-if becomes true
↓
Vue inserts the element
↓
Transition enter() runs
↓
Height is still 0px
↓
Web Component finishes rendering
↓
Actual height becomes 160px
Die Übergangsanimation misst in Schritt drei, während das Element noch eine leere Struktur ist. Sie animiert pflichtgemäß von 0px auf den gemessenen Wert von 0px, und kurz darauf wird die Komponente ohne Übergang in ihrer vollen Größe von 160px gerendert. Für den Benutzer sieht es so aus, als wäre die Animation nie abgelaufen.
Die Workaround-Lösung für das Abfragen und deren Kosten
Die offensichtliche Lösung besteht darin, zu warten, bis das Element eine bestimmte Höhe angibt, bevor man misst. Eine Schleife vor der Höhenberechnung erreicht das, indem sie einmal pro Frame überprüft:
while (element.scrollHeight === 0) {
await new Promise(requestAnimationFrame)
}
Das funktioniert tatsächlich. Jede Iteration wartet auf den nächsten Animationen-Frame und fragt erneut nach, ob scrollHeight immer noch null ist. Der Nachteil ist, dass dadurch in jedem Frame wiederholt nach dem Layout abgefragt wird, bis sich etwas ändert. Wenn der Inhalt beispielsweise aufgrund eines fehlgeschlagenen Ladevorgangs keine Höhe erhält, läuft die Schleife solange weiter, wie das Element existiert. Zudem wird enter zu einer asynchronen Funktion, was die Verarbeitung des Hooks erschwert. Der Browser verfügt bereits über ein Benachrichtigungssystem für genau diese Situation, weshalb es nicht notwendig ist, ihm dieselbe Frage sechzig Mal pro Sekunde zu stellen.
Trennung der Messung von der Animation
Vor dem Umstieg auf einen Observer hilft es, den Übergangscode leicht umzustrukturieren. In einer typischen Implementierung misst enter() sowohl das Element als auch die Animation aus. Da die Messung nun warten müssen könnte, sind diese beiden Aufgaben besser getrennt zu halten.
Der Animationscode selbst bleibt genau so, wie er war; er wird einfach in einen Hilfsfunktion namens animateEnter() verschoben. Der neue enter() hat dann nur noch eine Aufgabe: zu entscheiden, wann mit der Ausführung begonnen werden soll:
- Falls das Element bereits gemessen werden kann, wird sofort
animateEnter()aufgerufen. - Andernfalls wird gewartet, bis dies möglich ist, und anschließend
animateEnter()aufgerufen.
Diese Refaktorierung ändert das Verhalten im üblichen Fall nicht und gibt der Wartelogik einen klaren, isolierten Platz.
Auf die tatsächliche Höhe mit ResizeObserver warten
ResizeObserver meldet, wenn sich die Größe eines Elements ändert – genau das ist hier das benötigte Signal. Nachdem die Animation extrahiert wurde, wird der neue enter()-Aufruf kürzer:
function enter(element: HTMLElement) {
if (element.scrollHeight > 0) {
animateEnter(element)
return
}
const observer = new ResizeObserver(() => {
if (element.scrollHeight === 0)
return
observer.disconnect()
requestAnimationFrame(() => {
animateEnter(element)
})
})
observer.observe(element)
}
Gehe die Schritte nacheinander durch. Der schnelle Weg kümmert sich um native Elemente sowie bereits gerenderte Komponenten: Wenn scrollHeight positiv ist, beginnt die Animation sofort, genauso wie zuvor. Andernfalls wird ein Observer an das Element angehängt. Sein Callback überprüft erneut die Höhe und gibt frühzeitig zurück, solange sie noch null ist; dieser Schutzmechanismus ist wichtig, weil der Observer auch sofort beim Beginn der Beobachtung ausgelöst wird, wenn das Element noch leer sein kann. Sobald eine echte Höhe vorhanden ist, trennt sich der Observer ab, sodass er bei späteren Größenänderungen nicht weiter ausgelöst wird. Die Animation beginnt dann im nächsten Animationsframe, wodurch dem Browser Zeit bleibt, das Layout vor Beginn der Übergangsanimation zu finalisieren.
Nur der Zeitpunkt des Startens der Animation hat sich geändert. Die Animation selbst ist unverändert.
Eckfälle, die in der Produktion berücksichtigt werden müssen
Je nach Verwendung des Komponenten gibt es einige Situationen, die behandelt werden sollten:
- Falls das Element vor dem Erreichen einer Höhe entfernt oder versteckt wird, bleibt der Observer weiterhin angehängt. Seine Trennung im Handling von Übergangsende oder -stopp verhindert einen weiterhin aktiven Observer.
- Der Observer reagiert auf die Größe des Elements, das er überwacht. Wenn Ihr
beforeEnter-Schritt das Element aufheight: 0setzt, überprüfen Sie, ob das beobachtete Element in diesem Zustand tatsächlich seine Größe ändern kann, oder überwachen Sie stattdessen den Inhalt darin.
done-Callback übermitteln, der an enter weitergegeben wird. Stellen Sie sicher, dass diese Meldung auch nach einem verzögerten Start weiterhin gesendet wird.Warum der Observer-Ansatz robuster ist
Verglichen mit dem Polling-Verfahren hat die ResizeObserver-Version mehrere Vorteile:
- Es gibt keinen Frame-für-Frame-Zyklus zur Überprüfung der Layouts.
- Er kann Web Components verarbeiten, die asynchron aktualisiert werden.
- Ebenso können träge geladene Vue-Components behandelt werden.
- Auch Bilder und anderer Inhalt, deren Größe nach der ersten Darstellung ändert sich, werden korrekt gehandhabt.
- Die Übergangsanimation bleibt generisch und kennt nicht, was sie umschließt.
Dieser letzte Punkt ist der wertvollste. Das Expand-Komponente muss nicht wissen, ob sein Kind ein natives div, eine Vue-Komponente oder ein benutzerdefiniertes Element ist; es wartet, bis der umschlossene Inhalt eine nicht-null-Größe meldet, und führt anschließend die Animation aus.
Zusammenfassung
Der ursprüngliche Ansatz „ermessen und dann animieren“ bleibt für die meisten Vue-Anwendungen vollkommen ausreichend, bei denen der Inhalt bereits zum Zeitpunkt des Ausführens von enter gestaltet ist. Die Anpassung ist wichtig, wenn die Darstellung asynchron abläuft und das Layout zum Zeitpunkt des Einfügens des Elements von Vue noch nicht bereit ist. Die allgemeine Lektion für wiederverwendbare UI-Primitiven besteht darin, nicht vorauszusetzen, wann das Layout verfügbar ist – stattdessen auf das eigene Signal des Browsers zu reagieren. Es handelt sich um eine kleine Codeänderung, die die Zuverlässigkeit deutlich verbessert, und jeder solche Randfall in einer echten Integration macht die Komponente für jedes Projekt, das sie anschließend verwendet, robuster.