Ein Skeleton-Layout-Loader für Discourse erstellen

Hallo :waving_hand:

Die Grundidee

Das Ziel war es, einen Skeleton-Loader zu erstellen, der aus der tatsächlichen Discourse-UI generiert wird, anstatt auf eine hartkodiert Skeleton-Vorlage zu verlassen.

Der Builder ermöglicht es einem Administrator, echte Elemente auf der Seite auszuwählen und in Skeleton-Bereiche umzuwandeln.

Zum Beispiel:

.title
.avatar
.topic-excerpt
.btn
.category-breadcrumb

Die Komponente verwendet diese Selektoren dann zur Laufzeit, um das Skeleton zu generieren.

Skeleton-Vorschau

Der interessante Aspekt ist, dass der Administrator die Selektoren nicht manuell schreiben muss. Der Builder analysiert das ausgewählte Element und generiert mehrere Kandidaten-Selektoren.


Die Generierung nützlicher Selektoren erwies sich als schwieriger als erwartet

Eines der ersten Probleme, auf die ich stieß, war die Selektor-Generierung.

Eine naive Implementierung kann leicht etwas wie Folgendes erzeugen:

.container.list-container.--topic-list .row.full-width .contents ...

Technisch gültig, aber für eine wiederverwendbare Skeleton-Konfiguration viel zu spezifisch.

Bei Icons kann es sogar noch schlimmer werden, da der generierte Selektor Implementierungsdetails wie SVG-bezogene Klassen enthalten kann.

Ich wollte eigentlich etwas, das näher an Folgendem liegt:

.badge-category__name

oder:

.badge-category__wrapper .d-icon

anstatt eines Selektors, der den gesamten DOM-Pfad beschreibt.

Daher generiert der Builder nun mehrere Kandidaten und bewertet sie anhand von Kriterien wie:

  • Selektortiefe
  • Anzahl der Klassen
  • wiederkehrende Treffer
  • zustandsbezogene Klassen
  • technische SVG/Icon-Klassen
  • ob der Selektor immer noch das ausgewählte Element trifft

Das Ergebnis ist eine Liste empfohlener Selektoren, aus denen der Administrator auswählen oder die er manuell bearbeiten kann.


Versteckte Elemente

Es gibt auch einen separaten Picker für Elemente, die während der Anzeige des Skeletons einfach verschwinden sollen.

Zum Beispiel:

.alert.alert-info

Das erwies sich als nützlich für Dinge wie Ankündigungs-Banner oder temporäre Hinweise, die beim Erstellen/Testen vorhanden sind, aber das Skeleton-Layout nicht beeinflussen sollten.

Ein interessantes Problem hier war, dass das Ausblenden eines Elements keinen leeren Raum hinterlassen darf.

Daher werden die ausgeschlossenen Elemente nicht einfach als display: none-Liste behandelt – die Geometrie-Berechnung muss auch verstehen, dass das Element nicht Teil des endgültigen Layouts ist.

Skeleton-Vorschau


Navigation

Wahrscheinlich die größte Herausforderung war die Navigation.

Das gewünschte Verhalten war:

klick

  ↓

Skeleton sofort anzeigen

  ↓

Discourse ändert Route

  ↓

Ziel-DOM erscheint

  ↓

Skeleton ausblenden

Die verlockende Lösung war es, tief in den Navigationslebenszyklus einzugreifen und darauf zu warten, bis der DOM vollständig stabilisiert ist.

Das erwies sich als der falsche Ansatz.

An einem Punkt konnte das Skeleton noch mehrere Sekunden sichtbar bleiben, nachdem der eigentliche Inhalt bereits da war.

Die Lektion war einfach:

Das Skeleton sollte kein DOM-Bereitschafts-Gate werden.

Sobald das Ziel genug echten Inhalt hat, um zu übernehmen, sollte das Skeleton aus dem Weg gehen.

Das machte einen großen Unterschied für die wahrgenommene Geschwindigkeit der Navigation.


Viewports

Discourse hat bereits ein responsives Viewport-System, daher verwendet die Komponente nun dieselbe Breakpoint-Abstraktion:

xs
sm
md
lg
xl
2xl

Die Skeleton-Konfiguration kann zusätzlich gruppiert werden als:

mobile → xs / sm
tablet → md
desktop → lg / xl / 2xl
all → alles

Das bedeutet, dass die Komponente die tatsächlichen Pixelwerte gar nicht kennen muss.

Wenn Discourse die Breakpoint-Werte ändert, muss die Skeleton-Komponente nicht um neue hartkodierte Zahlen herumgeschrieben werden.


Geometrie-Caching

Selektoren sagen uns, was gerendert werden soll, aber sie sagen uns nicht genau, wo die Skeleton-Formen erscheinen sollen.

Dafür habe ich die Erfassung der Geometrie hinzugefügt.

Der Builder kann die tatsächlich gerenderten Bereiche messen und ihre Geometrie speichern, damit der Loader während der SPA-Navigation sofort ein Zielskeleton rendern kann.

Es gibt auch eine explizite Option zum Sperren der Geometrie für Fälle, in denen ich nicht möchte, dass spätere Besuche die Referenzgeometrie kontinuierlich ändern.

Das war eine weitere wichtige Unterscheidung:

Selektor-Definition und gerenderte Geometrie sind zwei verschiedene Dinge.


Entwürfe

Eine weitere notwendige Ergänzung war der Entwurfszustand.

Ich wollte diesen Workflow vermeiden:

Builder öffnen

→ 10 Minuten mit der Konfiguration verbringen

→ Builder schließen

→ alles ist weg

Der Builder behält daher einen laufenden Entwurf getrennt von der eigentlichen Theme-Einstellung auf.

Der Entwurf ist auf die Kombination aus Seite/Route/Viewport beschränkt, sodass zum Beispiel:

topic-list / lg
topic-list / md
topic-list / xs

einander nicht versehentlich überschreiben.

Das Schließen des Builders zerstört die Arbeit nicht.


Rückgängig

Sobald der Builder interaktiver wurde, wurde ein Rückgängig-System fast unvermeidlich.

Der Builder speichert Snapshots seines Konfigurationszustands:

{
  "regions": \[\],
  "excludes": \[\]
}

anstatt zu versuchen, eine Historie von DOM-Operationen zu pflegen.

Das macht das Rückgängig-System viel leichter nachvollziehbar und hält es auch unabhängig vom tatsächlichen Seiten-DOM.


Dieses Projekt ist in aktiver Entwicklung. Hoffentlich bald bereit für eine Theme-Komponente! :slightly_smiling_face:

3 „Gefällt mir“