Ein SVG-Icon-System im Stil von Icon-Fonts für Vue

Avatar of Kevin Lee Drum
Kevin Lee Drum am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Die Verwaltung einer benutzerdefinierten Icon-Sammlung in einer Vue-App kann manchmal eine Herausforderung sein. Eine Icon-Schriftart ist einfach zu verwenden, aber für die Anpassung müssen Sie sich auf Drittanbieter-Font-Generatoren verlassen, und Merge-Konflikte können schmerzhaft zu lösen sein, da Schriftarten Binärdateien sind.

Die Verwendung von SVG-Dateien anstelle dessen kann diese Schmerzpunkte beseitigen, aber wie können wir sicherstellen, dass sie genauso einfach zu verwenden sind und es gleichzeitig einfach ist, Icons hinzuzufügen oder zu entfernen?

Hier ist, wie mein ideales Icon-System aussieht

  • Um Icons hinzuzufügen, legen Sie sie einfach in einen dafür vorgesehenen icons-Ordner. Wenn Sie ein Icon nicht mehr benötigen, löschen Sie es einfach.
  • Um das rocket.svg Icon in einer Vorlage zu verwenden, ist die Syntax so einfach wie <svg-icon icon="rocket" />.
  • Die Icons können mit den CSS-Eigenschaften font-size und color skaliert und eingefärbt werden (genau wie bei einer Icon-Schriftart).
  • Wenn mehrere Instanzen desselben Icons auf der Seite erscheinen, wird der SVG-Code nicht jedes Mal dupliziert.
  • Keine Bearbeitung der Webpack-Konfiguration erforderlich.

Dies werden wir aufbauen, indem wir zwei kleine, einzelne Komponenten schreiben. Es gibt ein paar spezifische Anforderungen für diese Implementierung, obwohl ich sicher bin, dass viele von Ihnen Magiern da draußen dieses System für andere Frameworks und Build-Tools umarbeiten könnten

  • webpack: Wenn Sie Vue CLI verwendet haben, um Ihre App zu erstellen, dann verwenden Sie bereits webpack.
  • svg-inline-loader: Dies ermöglicht es uns, den gesamten SVG-Code zu laden und Teile zu bereinigen, die wir nicht wollen. Führen Sie npm install svg-inline-loader --save-dev im Terminal aus, um zu beginnen.

Die SVG-Sprite-Komponente

Um unsere Anforderung zu erfüllen, SVG-Code nicht für jede Instanz eines Icons auf der Seite zu wiederholen, müssen wir ein SVG "Sprite" erstellen. Wenn Sie noch nie von einem SVG-Sprite gehört haben, stellen Sie es sich als ein verstecktes SVG vor, das andere SVGs beherbergt. Überall, wo wir ein Icon anzeigen müssen, können wir es aus dem Sprite kopieren, indem wir die ID des Icons in einem <use>-Tag referenzieren, wie hier

<svg><use xlink:href="#rocket" /></svg>

Dieser kleine Codeausschnitt ist im Wesentlichen, wie unsere <SvgIcon>-Komponente funktionieren wird, aber lassen Sie uns zuerst die <SvgSprite>-Komponente erstellen. Hier ist die gesamte SvgSprite.vue-Datei; ein Teil davon mag auf den ersten Blick einschüchternd wirken, aber ich werde alles aufschlüsseln.

<!-- SvgSprite.vue -->

<template>
  <svg width="0" height="0" style="display: none;" v-html="$options.svgSprite" />
</template>

<script>
const svgContext = require.context(
  '!svg-inline-loader?' + 
  'removeTags=true' + // remove title tags, etc.
  '&removeSVGTagAttrs=true' + // enable removing attributes
  '&removingTagAttrs=fill' + // remove fill attributes
  '!@/assets/icons', // search this directory
  true, // search subdirectories
  /\w+\.svg$/i // only include SVG files
)
const symbols = svgContext.keys().map(path => {
  // get SVG file content
  const content = svgContext(path)
   // extract icon id from filename
  const id = path.replace(/^\.\/(.*)\.\w+$/, '$1')
  // replace svg tags with symbol tags and id attribute
  return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>')
})
export default {
  name: 'SvgSprite',
  svgSprite: symbols.join('\n'), // concatenate all symbols into $options.svgSprite
}
</script>

Im Template hat unser einziges <svg>-Element seinen Inhalt an $options.svgSprite gebunden. Falls Sie mit $options nicht vertraut sind, enthält es Eigenschaften, die direkt an unsere Vue-Komponente angehängt sind. Wir hätten svgSprite an die data unserer Komponente anhängen können, aber wir brauchen Vue nicht wirklich, um Reaktivität dafür einzurichten, da unser SVG-Loader nur beim Erstellen unserer App ausgeführt wird.

In unserem Skript verwenden wir require.context, um alle unsere SVG-Dateien abzurufen und sie dabei zu bereinigen. Wir rufen svg-inline-loader auf und übergeben ihm mehrere Parameter mit einer Syntax, die den Query-String-Parametern sehr ähnlich ist. Ich habe diese zur besseren Verständlichkeit in mehrere Zeilen aufgeteilt.

const svgContext = require.context(
  '!svg-inline-loader?' + 
  'removeTags=true' + // remove title tags, etc.
  '&removeSVGTagAttrs=true' + // enable removing attributes
  '&removingTagAttrs=fill' + // remove fill attributes
  '!@/assets/icons', // search this directory
  true, // search subdirectories
  /\w+\.svg$/i // only include SVG files
)

Was wir hier im Grunde tun, ist, die SVG-Dateien zu bereinigen, die sich in einem bestimmten Verzeichnis (/assets/icons) befinden, damit sie in einem guten Zustand sind, um sie überall dort zu verwenden, wo wir sie brauchen.

Der Parameter removeTags entfernt Tags, die wir für unsere Icons nicht benötigen, wie z.B. title und style. Insbesondere möchten wir title-Tags entfernen, da diese unerwünschte Tooltips verursachen können. Wenn Sie hartkodierte Stile in Ihren Icons beibehalten möchten, fügen Sie removingTags=title als zusätzlichen Parameter hinzu, damit nur title-Tags entfernt werden.

Wir weisen unseren Loader auch an, fill-Attribute zu entfernen, damit wir unsere eigenen fill-Farben später mit CSS festlegen können. Es ist möglich, dass Sie Ihre fill-Farben beibehalten möchten. In diesem Fall entfernen Sie einfach die Parameter removeSVGTagAttrs und removingTagAttrs.

Der letzte Loader-Parameter ist der Pfad zu unserem SVG-Icon-Ordner. Wir geben require.context dann zwei weitere Parameter mit, damit es Unterverzeichnisse durchsucht und nur SVG-Dateien lädt.

Um alle unsere SVG-Elemente in unser SVG-Sprite zu verschachteln, müssen wir sie von <svg>-Elementen in SVG <symbol>-Elemente umwandeln. Dies ist so einfach wie das Ändern des Tags und das Geben jedem eine eindeutige id, die wir aus dem Dateinamen extrahieren.

const symbols = svgContext.keys().map(path => {
  // extract icon id from filename
  const id = path.replace(/^\.\/(.*)\.\w+$/, '$1')
  // get SVG file content
  const content = svgContext(path)
  // replace svg tags with symbol tags and id attribute
  return content.replace('<svg', `<symbol id="${id}"`).replace('svg>', 'symbol>')
})

Was machen wir mit dieser <SvgSprite>-Komponente? Wir platzieren sie auf unserer Seite vor allen Icons, die von ihr abhängen. Ich empfehle, sie ganz oben in der App.vue-Datei hinzuzufügen.

<!-- App.vue -->
<template>
  <div id="app">
    <svg-sprite />
<!-- ... -->

Die Icon-Komponente

Lassen Sie uns nun die SvgIcon.vue-Komponente erstellen.

<!-- SvgIcon.vue -->

<template>
  <svg class="icon" :class="{ 'icon-spin': spin }">
    <use :xlink:href="`#${icon}`" />
  </svg>
</template>

<script>
export default {
  name: 'SvgIcon',
  props: {
    icon: {
      type: String,
      required: true,
    },
    spin: {
      type: Boolean,
      default: false,
    },
  },
}
</script>

<style>
svg.icon {
  fill: currentColor;
  height: 1em;
  margin-bottom: 0.125em;
  vertical-align: middle;
  width: 1em;
}
svg.icon-spin {
  animation: icon-spin 2s infinite linear;
}
@keyframes icon-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(359deg);
  }
}
</style>

Diese Komponente ist viel einfacher. Wie bereits erwähnt, nutzen wir das <use>-Tag, um eine ID in unserem Sprite zu referenzieren. Diese id stammt von der icon-Prop unserer Komponente.

Ich habe dort eine spin-Prop hinzugefügt, die eine .icon-spin-Klasse umschaltet, als optionales Animationselement, falls wir es jemals brauchen sollten. Dies könnte zum Beispiel für ein Lade-Spinner-Icon nützlich sein.

<svg-icon v-if="isLoading" icon="spinner" spin />

Je nach Ihren Bedürfnissen möchten Sie vielleicht zusätzliche Props hinzufügen, wie z.B. rotate oder flip. Sie könnten die Klassen auch einfach direkt an die Komponente anheften, ohne Props zu verwenden, wenn Sie möchten.

Der größte Teil des Inhalts unserer Komponente ist CSS. Abgesehen von der Dreh-Animation wird der meiste davon verwendet, um unser SVG-Icon wie eine Icon-Schriftart wirken zu lassen¹. Um die Icons an die Textgrundlinie auszurichten, habe ich festgestellt, dass die Anwendung von vertical-align: middle zusammen mit einem unteren Rand von 0.125em in den meisten Fällen funktioniert. Wir setzen auch den Wert des fill-Attributs auf currentColor, was es uns ermöglicht, das Icon wie Text einzufärben.

<p style="font-size: 2em; color: red;">
  <svg-icon icon="exclamation-circle" /><!-- This icon will be 2em and red. -->
  Error!
</p>

Das ist alles!  Wenn Sie die Icon-Komponente überall in Ihrer App verwenden möchten, ohne sie in jede Komponente importieren zu müssen, die sie benötigt, stellen Sie sicher, dass Sie die Komponente in Ihrer main.js-Datei registrieren.

// main.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
Vue.component('svg-icon', SvgIcon)
// ...

Abschließende Gedanken

Hier sind ein paar Ideen für Verbesserungen, die ich absichtlich weggelassen habe, um diese Lösung zugänglich zu halten

  • Skalieren Sie Icons mit nicht-quadratischen Abmessungen, um ihre Proportionen beizubehalten
  • Injizieren Sie das SVG-Sprite auf die Seite, ohne eine zusätzliche Komponente zu benötigen.
  • Lassen Sie es mit vite funktionieren, einem neuen, schnellen (und webpack-freien) Build-Tool von Vue-Erfinder Evan You.
  • Nutzen Sie die Vue 3 Composition API.

Wenn Sie diese Komponenten schnell ausprobieren möchten, habe ich eine Demo-App erstellt, die auf der Standard-Vue-CLI-Vorlage basiert. Ich hoffe, dies hilft Ihnen bei der Entwicklung einer Implementierung, die den Bedürfnissen Ihrer App entspricht!


¹ Wenn Sie sich fragen, warum wir SVG verwenden, obwohl es sich wie eine Icon-Schriftart verhalten soll, dann schauen Sie sich den klassischen Beitrag an, der die beiden gegeneinander ausspielt.