Erstellung eines SVG-Icon-Systems mit React

Avatar of Sarah Drasner
Sarah Drasner am

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

Ich habe kürzlich an einem ReactJS Training von Michael Jackson und Ryan Florence teilgenommen. Ich war wirklich aufgeregt, teilzunehmen, teilweise weil ich so viele Fragen zu SVG und React hatte. Es gibt viele Aspekte der Arbeit mit React und SVG, und insbesondere deren Manipulation, die noch nicht vollständig unterstützt werden. Eine der größten Lücken für mich war das <use>-Element, da die meisten SVG-Icon-Systeme mit <use> erstellt werden.

Ich fragte Michael, ob er dachte, dass eine bessere Unterstützung für einige dieser Funktionen kommen würde, aber er zeigte mir einen viel besseren Weg, damit zu arbeiten, und umging diese Methode vollständig. Wir werden diese Technik durchgehen, damit Sie mit der Erstellung skalierbarer SVG-Icon-Systeme in React beginnen können, sowie einige Tricks, die meiner Meinung nach auch gut funktionieren könnten.

Hinweis: Es ist erwähnenswert, dass die Unterstützung für `use` kürzlich verbessert wurde, aber ich habe festgestellt, dass sie bestenfalls sporadisch ist und es andere Routing- und XML-Probleme gibt. Wir zeigen Ihnen hier einen anderen, saubereren Weg.

Was ist <use>?

Für diejenigen, die nicht vertraut sind, wie SVG-Icon-Systeme typischerweise aufgebaut sind, funktioniert es ungefähr so. Das <use>-Element klont eine Kopie eines beliebigen anderen SVG-Formelement mit der ID, auf die Sie im xlink:href-Attribut verweisen, und ermöglicht es Ihnen, es zu manipulieren, ohne alle Pfaddaten wiederholen zu müssen. Sie fragen sich vielleicht, warum man nicht einfach ein SVG als <img>-Tag verwenden würde. Das könnten Sie, aber dann wäre jedes Icon eine einzelne Anfrage und Sie hätten keinen Zugriff auf die Änderung von Teilen des SVG, wie z. B. die fill-Farbe.

Die Verwendung von <use> ermöglicht es uns, die Pfaddaten und das grundlegende Erscheinungsbild unserer Icons an einem Ort zu definieren, sodass sie einmal aktualisiert und überall geändert werden können, während wir immer noch den Vorteil haben, sie im laufenden Betrieb zu aktualisieren.

Joni Trythall hat einen großartigen Artikel über `use` und SVG-Icons, und Chris Coyier hat hier auf CSS-Tricks ebenfalls einen fantastischen Artikel geschrieben.

Hier ist ein kleines Beispiel, wenn Sie sehen möchten, wie der Markup aussieht

Siehe den Pen bc5441283414ae5085f3c19e2fd3f7f2 von Sarah Drasner (@sdras) auf CodePen.

Warum sich die Mühe mit SVG-Icons machen?

Manche von Ihnen fragen sich vielleicht an diesem Punkt, warum wir ein SVG-Icon-System anstelle eines Icon-Fonts verwenden würden. Wir haben unseren eigenen Vergleich zu diesem Thema. Außerdem schreiben und reden viele Leute gerade darüber.

Hier sind einige der überzeugendsten Gründe meiner Meinung nach:

  • Icon-Fonts sind schwer zugänglich zu machen. SVG hat die Fähigkeit, Titel- und ARIA-Tags hinzuzufügen, was einen enormen Vorteil für die Barrierefreiheit bietet, insbesondere in Fällen, in denen das Icon allein steht und die einzige Informationsquelle für die Navigation ist. Denken Sie an blinde Menschen, Menschen mit Legasthenie, ältere Menschen (Sie werden auch irgendwann alt sein, hoffentlich, also wenn Sie nicht der Typ Entwickler sind, der sich um diese Untergruppe kümmert, tun Sie es für das Karma! Aber im Ernst, kümmern Sie sich um ältere Menschen.)
  • Icon-Fonts sind auf einigen Displays nicht so scharf. Sie können dies vermeiden, indem Sie eine ausgefallene Font-Glättung in CSS durchführen, aber hier ist eine Einschränkung, die ich bemerkt habe: Es ist schwierig, sie zu überschreiben, ohne die Font-Glättung vollständig auszuschalten. SVGs sind generell schärfer, Zeichnen ist das, wofür sie gebaut wurden.
  • Icon-Fonts schlagen oft fehl. Die meisten Entwickler, die ich kenne, sind auf Szenarien gestoßen, in denen das Glyph X in einem Feld fehlt. Es gibt viele Möglichkeiten, wie Icon-Fonts fehlschlagen können, wo SVGs dies nicht tun. Sei es CORS-Probleme oder Opera Mini, es ist ein Kopfschmerz.
  • Icon-Fonts sind schwer zu positionieren. Sie sind ein Bild, das Sie mit Schriftartenstilen positionieren. Genug gesagt. Sie können keine Teile davon ohne hacky Stapelung animieren. SVGs bieten ein navigierbares DOM, um Teile eines Icons zu animieren oder Abschnitte zu kolorieren. Nicht jeder würde das wollen, aber es ist schön, die Option zu haben.

Wenn Sie wie ich sind und eine riesige Codebasis aktualisieren, bei der Sie, um von einem Icon-Font zu SVG zu wechseln, buchstäblich Hunderte von Markup-Instanzen aktualisieren müssten, dann verstehe ich das. Ich verstehe es. Es ist in diesem Fall vielleicht nicht die Zeit wert. Aber wenn Sie Ihre Ansichten neu schreiben und sie mit React aktualisieren, lohnt es sich, diese Gelegenheit zu überdenken.

Tl;dr: Sie brauchen <use> in React nicht

Nachdem Michael geduldig zugehört hatte, wie ich erklärte, wie wir <use> verwenden, und ich ihm ein Beispiel für ein Icon-System gezeigt hatte, war seine Lösung einfach: Es ist nicht wirklich notwendig.

Betrachten Sie dies: Der einzige Grund, warum wir Icons definierten, um sie dann wiederzuverwenden (normalerweise als <symbol>s in <defs>), war, damit wir uns nicht wiederholen mussten und die SVG-Pfade einfach an einer Stelle aktualisieren konnten. Aber React ermöglicht das bereits. Wir erstellen einfach die Komponente

// Icon
const IconUmbrella = React.createClass({
 render() {
   return (
     <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
	<title id="title">Umbrella Icon</title>
        <path d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
      </svg>
   )
 }
});

// which makes this reusable component for other views
<IconUmbrella />

Siehe den Pen SVG-Icon in React von Sarah Drasner (@sdras) auf CodePen.

Und wir können sie immer wieder verwenden, aber im Gegensatz zur älteren <use>-Methode haben wir keine zusätzliche HTTP-Anfrage.

Zwei SVG-ähnliche Dinge, die Ihnen im obigen Beispiel auffallen könnten. Erstens, ich habe nicht diese Art von Ausgabe

<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

Oder sogar diese am SVG-Tag selbst

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" …

Das liegt daran, dass ich sichergestellt habe, meine SVGs mit SVGOMG oder SVGO zu optimieren, bevor ich das Markup überall hinzufüge. Ich empfehle Ihnen dringend, dies ebenfalls zu tun, da Sie die Größe Ihres SVG um einen beträchtlichen Betrag reduzieren können. Ich sehe normalerweise Prozentsätze um die 30 %, können aber bis zu 60 % oder mehr erreichen.

Eine weitere Sache, die Ihnen vielleicht auffällt, ist, dass ich einen Titel und ein ARIA-Tag hinzufüge. Dies wird Bildschirmsprachausgaben für Menschen, die assistierende Technologien verwenden, helfen, das Icon vorzulesen.

 <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
  <title id="title">Umbrella Icon</title>

Da diese ID eindeutig sein muss, können wir Instanzen des Icons mit Props übergeben und diese werden sowohl für den Titel als auch für das Aria-Tag übernommen, wie hier gezeigt

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice iconTitle="animatedOffice" />
        </div>
        <IconOffice iconTitle="orangeBook" bookfill="orange" bookside="#39B39B" bookfront="#76CEBD"/>
        <IconOffice iconTitle="biggerOffice" width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
 ...
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby={this.props.iconTitle}>
        <title id={this.props.iconTitle}>Office With a Lamp</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App/>, document.querySelector("#main"));

Der beste Teil, vielleicht

Hier ist ein wirklich cooler Teil dieses Ganzen: Abgesehen von der Notwendigkeit zusätzlicher HTTP-Anfragen kann ich die Form des SVG in Zukunft vollständig aktualisieren, ohne Änderungen am Markup vornehmen zu müssen, da die Komponente in sich abgeschlossen ist. Noch besser ist, dass ich nicht die gesamte Icon-Schriftart (oder SVG-Sprite) auf jeder Seite laden muss. Da alle Icons als Komponenten vorliegen, kann ich etwas wie webpack verwenden, um für die jeweilige Ansicht zu "opt-in", welche Icons ich benötige. Angesichts des Gewichts von Schriften und insbesondere von schwergewichtigen Icon-Font-Glyphen ist dies eine enorme Möglichkeit für eine Leistungssteigerung.

All das, plus: Wir können Teile des Icons im laufenden Betrieb mit Farbe oder Animation auf sehr einfache Weise mit SVG und Props verändern.

Dynamische Veränderung im laufenden Betrieb

Eine Sache hier, die Ihnen vielleicht aufgefallen ist, ist, dass wir sie noch nicht im laufenden Betrieb anpassen, was ein Grund dafür ist, dass wir SVG überhaupt verwenden, oder? Wir können einige Standard-Props für das Icon deklarieren und sie dann ändern, wie hier

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <IconOffice />
        <IconOffice width="200" height="200"/>
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200'
    };
  },
 render() {
   return (
     <svg className="office" width={this.props.width} height={this.props.height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));

Siehe den Pen SVG-Icon in React mit Standard-Props von Sarah Drasner (@sdras) auf CodePen.

Nehmen wir es noch einen Schritt weiter und ändern Sie einige der Erscheinungsbilder basierend auf der Instanz. Wir können Props dafür verwenden und einige Standard-Props deklarieren.

Ich liebe SVG, weil wir jetzt ein navigierbares DOM haben. Im Folgenden ändern wir die Farbe mehrerer Formen im laufenden Betrieb mit fill. Beachten Sie, dass Sie, wenn Sie es gewohnt sind, mit Icon-Fonts zu arbeiten, die Farbe nicht mehr mit color, sondern mit fill ändern. Sie können das zweite Beispiel unten überprüfen, um dies in Aktion zu sehen, die Bücher haben ihre Farbe geändert. Ich liebe auch die Möglichkeit, diese Teile im laufenden Betrieb zu animieren. Unten haben wir es in einen Div eingebettet, um es sehr einfach mit CSS zu animieren (möglicherweise müssen Sie auf "Wiederholen" klicken, um die Animation zu sehen).

Siehe den Pen SVG-Icon in React mit Standard-Props und Animation von Sarah Drasner (@sdras) auf CodePen.

// App
const App = React.createClass({
  render() {
    return (
      <div>
        <div className="switcher">
          <IconOffice />
        </div>
        <IconOffice bookfill="orange" bookside="#39B39B" bookfront="#76CEBD" />
        <IconOffice width="200" height="200" />
      </div>
    )
  }
});

// Icon
const IconOffice = React.createClass({
  getDefaultProps() {
    return {
      width: '100',
      height: '200',
      bookfill: '#f77b55',
      bookside: '#353f49',
      bookfront: '#474f59'
    };
  },
 render() {
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={this.props.width} height={this.props.height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={this.props.bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={this.props.bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>
          <path className="cls-7" d="M60.7 69.8h38.9v7.66H60.7z"/>
          <path className="cls-5" d="M60.7 134.7h38.9v7.66H60.7z"/>
          ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App />, document.querySelector("#main"));
.switcher .office {
  #bulb { animation: switch 3s 4 ease both; }
  #background { animation: fillChange 3s 4 ease both; }
}

@keyframes switch {
  50% {
    opacity: 1;
  }
}

@keyframes fillChange {
  50% {
    fill: #FFDB79;
  }
}

Einer meiner fantastischen Kollegen bei Trulia, Mattia Toso, hat auch eine wirklich schöne, viel schlankere Methode zur Deklaration all dieser Props empfohlen. Wir können die Wiederholung von this.props reduzieren, indem wir Konstanten für all unsere Verwendungen deklarieren und dann einfach die Variable anwenden.

render() {
   const { height, width, bookfill, bookside, bookfront } = this.props;
   return (
     <svg className="office" xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 188.5 188.5" aria-labelledby="title">
        <title id="title">Office Icon</title>
        <g className="cls-2">
          <circle id="background" className="cls-3" cx="94.2" cy="94.2" r="94.2"/>
          <path className="cls-4" d="M50.3 69.8h10.4v72.51H50.3z"/>
          <path fill={bookside} d="M50.3 77.5h10.4v57.18H50.3z"/>
          <path fill={bookfront} d="M60.7 77.5h38.9v57.19H60.7z"/>

Wir können dies auch *noch* besser machen, indem wir propTypes für die verwendeten Props deklarieren. PropTypes sind äußerst hilfreich, da sie wie lebende Dokumente für die wiederverwendeten Props sind.

propTypes: {
  width: string,
  height: string,
  bookfill: string,
  bookside: string,
  bookfront: string
},

Auf diese Weise erhalten wir, wenn wir sie falsch verwenden, wie im folgenden Beispiel, einen Konsolenfehler, der unseren Code nicht stoppt, sondern andere Personen, mit denen wir zusammenarbeiten könnten (oder uns selbst), darauf aufmerksam macht, dass wir Props falsch verwenden. Hier verwende ich eine Zahl anstelle einer Zeichenkette für meine Props.

<IconOffice bookfill={200} bookside="#39B39B" bookfront="#76CEBD" />

Und ich erhalte folgenden Fehler

Siehe den Pen SVG-Icon in React mit Spread mit Fehler von Sarah Drasner (@sdras) auf CodePen.

Noch schlanker mit React 0.14+

In neueren Versionen von React können wir einen Teil dieses unnötigen Codes reduzieren und unseren Code noch weiter vereinfachen, aber nur, wenn es sich um eine sehr "dumme" Komponente handelt, d.h. sie nimmt keine Lifecycle-Methoden an. Icons sind dafür ein ziemlich guter Anwendungsfall, da wir hauptsächlich rendern. Versuchen wir es also. Wir können uns von React.createClass verabschieden und unsere Komponenten als einfache Funktionen schreiben. Das ist ziemlich genial, wenn Sie schon lange JavaScript verwenden, aber mit React weniger vertraut sind – es liest sich wie die Funktionen, an die wir alle gewöhnt sind. Lassen Sie uns unsere Props noch weiter aufräumen und das Regenschirmsymbol wie auf einer Website wiederverwenden.

// App
function App() {
  return (
    <div>
      <Header />
      <IconUmbrella />
      <IconUmbrella umbrellafill="#333" />
      <IconUmbrella umbrellafill="#ccc" />
    </div>
  )
}
 
// Header
function Header() {
 return (
   <h3>Hello, world!</h3>
 )
}

// Icon
function IconUmbrella(props) {
  const umbrellafill = props.umbrellafill || 'orangered'
  
  return (
    <svg className="umbrella" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
      <title id="title">Umbrella</title>
      <path fill={umbrellafill} d="M27 14h5c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0zM27 14c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2v0 14c0 1.112-0.895 2-2 2-1.112 0-2-0.896-2-2.001v-1.494c0-0.291 0.224-0.505 0.5-0.505 0.268 0 0.5 0.226 0.5 0.505v1.505c0 0.547 0.444 0.991 1 0.991 0.552 0 1-0.451 1-0.991v-14.009c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-1.105-1.119-2-2.5-2s-2.5 0.895-2.5 2c0-5.415 6.671-9.825 15-9.995v-1.506c0-0.283 0.224-0.499 0.5-0.499 0.268 0 0.5 0.224 0.5 0.499v1.506c8.329 0.17 15 4.58 15 9.995h-5z"/>
    </svg>
  )
}
 
ReactDOM.render(<App />, document.querySelector("#main"));

Siehe den Pen SVG-Icon in React von Sarah Drasner (@sdras) auf CodePen.

SVG-Icon-Systeme sind in React wunderschön einfach und leicht erweiterbar, haben weniger HTTP-Anfragen und sind zukünftig leicht zu warten, da wir den Output vollständig aktualisieren können, ohne wiederholte Markup-Änderungen. Wir können die Leistung steigern, indem wir genau das auswählen, was wir brauchen. Wir können sie im laufenden Betrieb mit Props für Farbe ändern und sogar CSS-Animationen hinzufügen. All das und wir können sie auch für Bildschirmsprachausgaben zugänglich machen, was React und SVG-Icon-Systeme zu einer wirklich schönen Möglichkeit macht, Icons zu Ansichten im Web hinzuzufügen.