Startseite / Artikel / Behebung von Höhenübergängen bei Vue für asynchron gerenderte Webkomponenten

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.

1144 Wörter

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 auf height: 0 setzt, überprüfen Sie, ob das beobachtete Element in diesem Zustand tatsächlich seine Größe ändern kann, oder überwachen Sie stattdessen den Inhalt darin.
  • Wenn eine Übergangsanimation ausschließlich durch JavaScript-Hooks gesteuert wird, erwartet Vue, dass Sie die Abschlussmeldung über den 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.