Frameworks

Icons in Nuxt

glyphfox/vue rendert auf dem Server, daher kommen Nuxt-Seiten mit echtem SVG im HTML an. Diese Anleitung zeigt die Einrichtung, ein Plugin für gemeinsam genutzte Icons, einen automatisch importierten Wrapper und wie sich Registrierung und Morphs zwischen Server und Browser verhalten.

Installation

terminal
npm install glyphfox

Sie müssen kein Nuxt-Modul hinzufügen. Die Vue-Komponenten liegen in glyphfox/vue, das als reine ES-Module mit einem CommonJS-Build für require ausgeliefert wird. Nuxt importiert es daher auf dem Server und im Browser wie jede andere Abhängigkeit. Alles aus der Vue-Anleitung gilt auch hier; diese Seite behandelt, was sich ändert, wenn dieselben Komponenten zuerst auf dem Server rendern.

Icons in Seiten und Komponenten verwenden

pages/index.vue
<script setup lang="ts">
import { SearchIcon, ArrowRightIcon } from 'glyphfox/vue';
</script>

<template>
  <form class="search" action="/search">
    <SearchIcon :size="18" />
    <input name="q" type="search" placeholder="Search the docs" aria-label="Search the docs">
    <button type="submit">
      Search <ArrowRightIcon :size="16" mirror-rtl />
    </button>
  </form>
</template>

Das SVG ist Teil des Server-HTML, daher sind die Icons sichtbar, bevor überhaupt JavaScript läuft, und sie verschieben das Layout bei der Hydration nie. Nach der Hydration übernehmen dieselben Komponenten Auslöser, Morphs und Zustandswechsel. mirror-rtl spiegelt richtungsabhängige Icons wie Pfeile, wenn die Seite oder ein übergeordnetes Element dir="rtl" hat.

Die Komponenten in jeder Datei zu importieren, ist das expliziteste Setup und hält zugleich jeden Routen-Chunk klein: Eine Seite enthält nur die Icons, die sie verwendet.

Ein Plugin für gemeinsam genutzte Icons

Ein Plugin ist der richtige Ort für zwei Dinge: Komponenten, die in jedem Template verfügbar sein sollen, und die Daten von Namen, die nur als Strings in Icon, Fusion, MorphIcon oder morph-to vorkommen.

plugins/glyphfox.ts
import { HomeIcon, SettingsIcon, Icon, registerIcons } from 'glyphfox/vue';
import menu from 'glyphfox/icons/menu';
import x from 'glyphfox/icons/x';
import moon from 'glyphfox/icons/moon';
import cloud from 'glyphfox/icons/cloud';
import lock from 'glyphfox/icons/lock';

// Runs once per process on the server and once in the browser.
registerIcons([menu, x, moon, cloud, lock]);

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.component('HomeIcon', HomeIcon);
  nuxtApp.vueApp.component('SettingsIcon', SettingsIcon);
  nuxtApp.vueApp.component('GfIcon', Icon);
});

Halten Sie dieses Plugin universell, also ohne das Suffix .client oder .server: Der Server braucht die Daten, um das HTML zu schreiben, und der Browser braucht dieselben Daten für die Hydration. Auf dem Server ist die Registry ein Modulzustand, den alle Anfragen teilen. Das ist unbedenklich, weil sich Icon-Daten nie ändern.

Ein automatisch importierter Wrapper

Nuxt importiert Komponenten aus components/ automatisch. Ein kleiner Wrapper gibt der App eine einzige Icon-Komponente mit Ihren Standardwerten und einer festen Liste von Namen. So fällt ein Tippfehler schon bei der Typprüfung auf, statt ein leeres SVG zu rendern.

components/AppIcon.vue
<script setup lang="ts">
import { Icon } from 'glyphfox/vue';
import home from 'glyphfox/icons/home';
import search from 'glyphfox/icons/search';
import settings from 'glyphfox/icons/settings';
import bell from 'glyphfox/icons/bell';

const icons = { home, search, settings, bell };

const props = withDefaults(defineProps<{ name: keyof typeof icons; label?: string; size?: number }>(), { size: 20 });
</script>

<template>
  <Icon :icon="icons[props.name]" :size="props.size" :weight="1.75" :title="props.label" />
</template>
components/NotificationsButton.vue
<template>
  <button type="button" aria-label="Notifications">
    <AppIcon name="bell" />
  </button>
</template>

Wenn Sie Icon-Daten über die Prop icon übergeben, wird die Namens-Registry vollständig umgangen. Der Wrapper rendert daher auf dem Server und im Browser dasselbe Markup, ganz gleich, was wo registriert wurde. Ohne label bleibt das Icon dekorativ, und das ist in einem beschrifteten Button genau richtig.

Morphs, Animationen und Hydration

Der Server rendert ein Standbild. Animationen lebendiger Zustände sind CSS im SVG, daher dreht sich ein Spinner mit animate="spin" bereits im Server-HTML, es sei denn, Besucher bevorzugen reduzierte Bewegung. Morphs sowie die Auslöser für Hover und Klick setzen voraus, dass die Komponente zuerst hydriert wird.

morph-to mit dem Standardauslöser (mount) rendert auf dem Server das Ausgangs-Icon und spielt das Morphing direkt nach der Hydration ab, was bei jedem Seitenaufruf wie ein Flackern wirkt. Soll die Seite direkt im Zielzustand erscheinen, verwenden Sie trigger="state" und binden :active an einen gemeinsam genutzten Zustand: Der Server rendert dann das Ziel, und das Morphing läuft nur ab, wenn sich der Zustand ändert.

components/ThemeToggle.vue
<script setup lang="ts">
import { SunIcon } from 'glyphfox/vue';

const dark = useState('dark', () => false);
</script>

<template>
  <button type="button" aria-label="Dark theme" :aria-pressed="dark" @click="dark = !dark">
    <SunIcon morph-to="moon" trigger="state" :active="dark" />
  </button>
</template>

useState wird in den Payload der Seite serialisiert, daher starten Server und Browser mit demselben Wert, und das Icon hydriert ohne Mismatch. Das Morph-Ziel moon ist im Plugin oben registriert.

Bundle und Payload

In einer serverseitig gerenderten App kostet ein Icon doppelt: einmal das SVG-Markup im HTML und einmal der Komponentencode, der es hydriert. Das Markup ist klein, ein paar Pfade pro Icon. Gemessen in einem Produktions-Build mit Vite 8 ist die gemeinsame Vue-Runtime von glyphfox etwa 20 KB gzip groß, und jede Icon-Komponente fügt rund 0,1 KB gzip hinzu. Die Runtime überwiegt also, und zusätzliche Icons sind günstig.

Halten Sie Icon-Daten aus useState und useAsyncData heraus. Alles, was dort gespeichert ist, wird in das HTML jeder Seite serialisiert, die es verwendet. Importieren Sie stattdessen die Icon-Module; sie werden zusammen mit dem restlichen JavaScript gecacht.

Für Icons, die sich nie ändern, etwa in einem Footer oder einem statischen Marketing-Bereich, rendert eine Server-Komponente das Markup, ohne Icon-Code auszuliefern. Aktivieren Sie Component Islands und benennen Sie die Komponente mit dem Suffix .server.vue:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: { componentIslands: true },
});
components/FooterLinks.server.vue
<script setup lang="ts">
import { GitBranchIcon, RssIcon, MailIcon } from 'glyphfox/vue';
</script>

<template>
  <ul class="footer-links">
    <li><a href="https://github.com/kwy404/glyphfox"><GitBranchIcon :size="18" /> Source</a></li>
    <li><a href="/feed.xml"><RssIcon :size="18" /> Feed</a></li>
    <li><a href="mailto:hello@example.com"><MailIcon :size="18" /> Contact</a></li>
  </ul>
</template>

Nicht registrierte Namen

Ein Name, den niemand registriert hat, wird über einen dynamischen Import innerhalb des Pakets geladen. Nuxt baut mit Vite, und Vite überspringt dynamische Importe mit Variablen, wenn sie in node_modules liegen. In einem Produktions-Build werden diese Namen daher nie geladen. Wenn Sie zur Laufzeit wirklich beliebige Namen benötigen, etwa in einem Icon-Picker, lassen Sie Vite den Loader bündeln. Vite erzeugt dann pro Icon einen kleinen Chunk, der erst geladen wird, wenn der jeweilige Name verwendet wird:

nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      dynamicImportVarsOptions: { exclude: [/node_modules\/(?!glyphfox\/)/] },
    },
  },
});

Das hilft erst nach der Hydration. Das serverseitige Rendering wartet nicht auf einen dynamischen Import, daher muss ein Icon, das im HTML stehen soll, weiterhin registriert oder als Komponente importiert werden.

Props

Es handelt sich um die Komponenten aus glyphfox/vue mit denselben Props. In Templates können Sie die Attributnamen in kebab-case verwenden.

PropIn TemplatesTypStandardBeschreibung
sizesizenumber, string24Breite und Höhe; Zahlen sind Pixel.
colorcolorstringcurrentColorHauptfarbe.
secondaryColorsecondary-colorstringcolorFarbe der Duotone-Ebene und der Solid-Füllung.
secondaryOpacitysecondary-opacitynumber0.3 / 1Deckkraft der Duotone-Ebene oder der Solid-Füllung.
weightweightnumber2Strichstärke von 1 bis 3 im 24er-Raster, unter 24 px optisch ausgeglichen.
absoluteWeightabsolute-weightbooleanfalseHält den Strich bei jeder Größe auf weight Pixeln.
cornerscornersround, sharproundAbgerundete oder spitze Verbindungen und Enden.
variantvariantoutline, duotone, solidoutlineStilachse; solid entsteht aus den geschlossenen Formen.
rotaterotatenumber0Drehung in Grad.
flipXflip-xbooleanfalseHorizontal spiegeln.
flipYflip-ybooleanfalseVertikal spiegeln.
mirrorRtlmirror-rtlbooleanfalseIn Layouts von rechts nach links spiegeln.
animateanimatedraw, pop, wiggle, spin, pulse, bounce, shakekeinerVoreinstellung für den lebendigen Zustand.
triggertriggermount, hover, click, statemountWann die Animation oder der Morph abläuft.
activeactivebooleanfalseDer Zustand für den Auslöser state.
durationdurationnumber (ms)je nach VoreinstellungDauer der Animation oder des Morphs.
delaydelaynumber (ms)0Verzögerung der Animation oder des Morphs.
looploopboolean, numberfalseEndlos oder eine bestimmte Anzahl von Malen wiederholen (spin und pulse laufen standardmäßig in Schleife).
modifiermodifierplus, minus, check, x, alert, lock, clock, sync, arrow-up, arrow-down, dotkeinerBadge mit automatischer Aussparung.
modifierCornermodifier-cornertop-left, top-right, bottom-left, bottom-rightbottom-rightEcke des Badges.
morphTomorph-toname, datakeinerZiel des Morphs; der Auslöser bestimmt, wann es erscheint.
titletitlestringkeinerZugänglicher Name: title, role img und aria-label.
decorativedecorativebooleantrue ohne titleVerbirgt das Icon vor assistiven Technologien.

Stolperfallen

Funktioniert in der Entwicklung, leer in Produktion
Der Dev-Server bündelt das gesamte Paket vorab, daher wird lokal jeder Name aufgelöst. Der Produktions-Build behält nur Ihre Importe und lässt den On-Demand-Loader weg, sodass Namen, die nur in Strings vorkommen, ein leeres SVG rendern. Registrieren Sie diese Namen im universellen Plugin.
Ein Icon wird auf dem Server gerendert und verschwindet dann
Lädt der Server glyphfox ohne Tree-Shaking, registriert der Import von glyphfox/vue dort jedes Icon, während das Browser-Bundle nur registriert, was Sie importieren. Ein Name, der nur als String vorkommt, kann dann im Server-HTML erscheinen und nach der Hydration leer werden, begleitet von einer Hydration-Warnung in der Konsole. Wenn Sie jeden solchen Namen explizit registrieren, verschwindet der Unterschied.
Registrierung in einem reinen Client-Plugin
Ein plugins/glyphfox.client.ts läuft erst, nachdem der Server die Seite bereits gerendert hat. Das HTML enthält dann leere Icons, die erst später auftauchen. Verwenden Sie ein Plugin ohne Suffix.
Ein Morph, der bei jedem Seitenaufruf abläuft
morph-to mit dem Standardauslöser mount animiert direkt nach der Hydration. Verwenden Sie trigger="state" mit einem Zustand aus useState, wie beim Theme-Umschalter oben.
Unterschiedliche Namen auf dem Server und im Browser
Wenn Sie ein Icon anhand von window, localStorage oder der aktuellen Uhrzeit auswählen, unterscheidet sich das erste Rendering im Browser von dem auf dem Server, und Vue meldet einen Hydration-Mismatch. Lesen Sie solche Werte in onMounted aus, oder speichern Sie sie in einem Cookie, das der Server lesen kann.

Häufige Fragen

Brauche ich ein Nuxt-Modul für glyphfox?

Nein. Importieren Sie die Komponenten aus glyphfox/vue dort, wo Sie sie verwenden, oder registrieren Sie einen gemeinsamen Satz in einem Plugin. Ein Modul würde lediglich die globale Registrierung ergänzen, und die bietet Ihnen bereits ein zehnzeiliges Plugin, ohne zu verschleiern, welche Icons im Bundle landen.

Funktioniert das mit nuxt generate und vorgerenderten Routen?

Ja. Beim Prerendering läuft dasselbe serverseitige Rendering zur Build-Zeit, daher enthalten die erzeugten HTML-Dateien das SVG-Markup. Für die Registrierung gilt dieselbe Regel: Ein Name, den der Build nicht auflösen kann, wird als leeres SVG geschrieben.

Icons in diesem Leitfaden