CSS-live.ru

Создание системы SVG-иконок с помощью React

Перевод статьи Creating an SVG Icon System with React с сайта css-tricks.com для css-live.ru. Автор — Сара Дрэснер.

Недавно я прошла обучение ReactJS у Майкла Джексона и Райана Флоренса. И мне очень понравилось, частично потому, что я нашла ответы на множество вопросов про SVG и React. В том, что касается работы с Реактом и SVG, есть много мелочей, которые еще не совсем поддерживаются. Одной из главных загадок для меня был элемент <use>, поскольку большинство систем SVG-иконок строятся именно на нём.

Я спросила Майкла, станет ли в будущем поддержка этих фич лучше, на что он показал мне отличный способ работы с ними, полностью обходя этот метод. Рассмотрим эту технику, чтобы вы смогли начать писать масштарибуемые системы SVG-иконок, и некоторые хитрости, которые также пригодились бы в работе.

Примечание: стоит отметить, что поддержка use недавно улучшилась, но мне показалось, что она по меньшей мере не стабильна и у нее есть другие проблемы с маршрутизацией и XML. Здесь мы рассмотрим более аккуратный способ.

Что такое <use>?

Для тех, кто не знает, как обычно строятся системы SVG-иконок, это работает примерно так. Элемент <use> клонирует копию любого другого элемента SVG-фигуры с помощью ссылки на ID в атрибуте xlink:href, и по-прежнему управляет им, без повторения всех данных пути. Может возникнуть вопрос, почему бы не подключать SVG тегом <img>. В принципе, можно, но тогда каждая иконка запрашивалась бы отдельно и вы бы не смогли менять части SVG, такие, как цвет fill.

C <use> данные контуров и основное оформление иконок можно определить в одном месте, так что их можно будет один раз обновить — и эти изменения применятся везде, причем делать это можно на лету.

У Джони Трайтел есть отличная статья про use и SVG-иконки, а Крис Койер также написал ещё одну потрясающую статью здесь на CSS-Tricks.

Вот небольшой пример, как может выглядеть разметка:

Зачем заморачиваться с SVG-иконками?

Возможно, некоторым из вас интересно, почему вначале мы собираемся использовать систему SVG-иконок вместо иконочного шрифта. У нас есть собственное сравнение на этот счёт. К тому же есть куча людей, которые сейчас пишут и говорят об этом.

Вот некоторые из самых убедительных причин, на мой взгляд:

  • Иконочные шрифты плохо сочетаются с доступностью. В SVG можно добавлять заголовок и теги ARIA, что даёт огромное преимущество доступности, особенно в тех случаях, когда иконка — один-единственный источник информативной навигации. Подумайте о слепых, о людях с дислексией или пожилых (когда-нибудь вы сами станете пожилыми, хочется надеяться, так что если вам сейчас безразлична эта группа, то сделайте это хотя бы для кармы! Но серьезно, подумайте о пожилых.)
  • Иконочные шрифты недостаточно четкие на некотрых экранах. Этого можно избежать, применив какое-то хитрое сглаживание шрифтов в CSS, но есть нюанс: его трудно переопределить, не выключив сглаживание шрифтов полностью. SVG в целом четче, рисование — это его предназначение.
  • Иконочные шрифты не срабатывают в изрядной доле случаев. Большинство моих знакомых разработчиков сталкиваются со сценариями с крестиком отсутствующего символа в рамке, и есть множество ситуаций, где иконочные шрифты в отличие от SVG могут не сработать. Будь то CORS-ограничения или Opera mini, это головная боль.
  • Иконочные шрифты трудно позиционировать. Это изображение, которое позиционируется с помощью стилей шрифта. И этим всё сказано. Нельзя анимировать их части без кучи хаков. SVG предлагает управляемый DOM для анимации или окрашивания частей иконки. Это не всегда нужно, но такая возможность уж точно не помешает.

Если вы, как я, обновляете немереную прорву кода, где ради перехода с иконочных шрифтов на SVG пришлось бы перелопатить буквально сотни мест разметки, я вас понимаю. Еще как понимаю. В этом случае, пожалуй, оно не стоит такой траты времени. Но если вы всё равно переписываете слой представления и переводите его на Реакт, то здесь такую возможность стоит рассмотреть.

Краткая выжимка: в Реакте не нужен <use>

После того, как Майкл терпеливо выслушал мой рассказ про то, как мы используем <use>, и я показала ему пример системы иконок, его решение было простым: всё это не нужно.

Подумайте вот о чем: единственная причина, по которой мы определяли иконки, чтобы затем повторно использовать их (обычно в виде <symbol> в <defs> ) — чтобы не пришлось повторяться и можно было обновлять SVG-контуры в одном месте. Но Реакт уже позволяет это. Нужно просто создать компонент:

// Иконка
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">Иконка зонта</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>
   )
 }
});

// что делает это многоразовым компонентом для других представлений
<IconUmbrella />

И можно использовать его снова и снова, но в отличие от старого способа с <use>, нет дополнительного HTTP-запроса.

Возможно, в примере выше вы заметили две SVG-шные штуки. Первое, у меня нет этого вывода:

<?xml version="1.0" encoding="utf-8"?>
<!-- Сгенерировано IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

Или даже этого у самого тега SVG:

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

Это потому, что я оптимизировала свой SVG с помощью SVGOMG или SVGO прежде чем добавить разметку. Настоятельно рекомендую делать так же, поскольку можно прилично уменьшить размер SVG. Обычно у меня выходит около 30% процентов, но бывает до 60% и выше.  

Ещё вы, возможно, заметили, что я добавила заголовок и тег ARIA. Это поможет скринридерам произнести иконки для тех, кто использует вспомогательные технологии.

<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">Иконка зонта</title>

Поскольку этот id должен быть уникальным, можно передать props в экземляры иконки и распространится на заголовок и тег aria:

// Приложение
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>
    )
  }
});

// Иконка
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}>Офис с лампой</title>
        ...
      </svg>
   )
 }
});
 
ReactDOM.render(<App/>, document.querySelector("#main"));

Возможно, самое интересное

А вот и правда клёвая часть всего этого: помимо того, что не требуются дополнительные HTTP-запросы, можно ещё и полностью обновлять SVG-фигуру в будущем, не изменяя разметку, поскольку компонент самодостаточен. Больше того, мне не требуется загружать весь иконочный шрифт (или SVG-спрайт) на каждой странице. Когда все иконки превращены в компонент, я могу использовать что-то типа Webpack, чтобы конструировать наборы иконок для каждого конкретного представления. Учитывая вес шрифтов, а особенно иконочных шрифтов со сложными формами, это просто спасение для производительности.

Вдобавок к этому: можно очень просто  на лету перекрашивать и анимировать части иконок с помощью SVG и props.

Меняем это на лету

Возможно, вы заметили, что мы ещё не настраиваем его на лету, а ведь отчасти ради этого мы и выбрали SVG в первую очередь, не так ли? Можно объявить в иконке некоторые props по умолчанию, а затем изменять их, например, так:

// Приложение
const App = React.createClass({
  render() {
    return (
      <div>
        <IconOffice />
        <IconOffice width="200" height="200"/>
      </div>
    )
  }
});

// Иконка
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"));

Разовъём эту идею и немного изменим внешний вид в зависимости от экземпляра. Для этого можно использовать props и объявить некоторые props по умолчанию.

Я люблю SVG, поскольку теперь у нас есть управляемый DOM, так что давайте изменим цвет некоторых фигур на лету с помощью fill. Не забывайте, если раньше вы привыкли работать с иконочными шрифтами, что теперь вы управляете цветом не с помощью color, а с помощью fill. Можно посмотреть это в действии во втором примере ниже, где у книг изменился цвет. Также мне нравится возможность анимировать эти части на лету, ниже мы обернули его в div, чтобы легко анимировать его с помощью CSS (чтобы увидеть анимацию, возможно придётся нажать кнопку «Rerun»)

// Приложение
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>
    )
  }
});

// Иконка
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">Иконка офиса</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;
  }
}

Один мой замечательный коллега из Trulia, Маттиа Тосо, также рекомендовал классный и гораздо более чистый способ объявления всех этих props. Здесь можно избавиться от повторения this.props, объявив всё, что мы будем использовать, через const, а в дальнейшем просто подставляя отдельные переменные:

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">Иконка офиса</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"/>

Можно также сделать это ещё круче, объявив propTypes в используемом props. PropTypes невероятно полезны, поскольку они как живая документация для повторно используемых props .

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

Если использовать их неправильно, как примере ниже, то в консоли появится ошибка, которая не помешает коду запуститься, но сообщит вашим коллегам (или вам самим) о неправильном применении props. Здесь я использую для props число вместо строки.

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

И всплывает такая ошибка:

props-error

Ещё компактнее с React 0.14+

В новейших версиях Реакта можно избавиться от части этого хлама и упростить код ещё больше, но только в случае очень «глупого» компонента, который, например, не принимает методы жизненного цикла. Иконки отлично подходят для этого, поскольку мы в основном просто отображаем их, так что давайте попробуем. Можно избавиться от React.createClass и написать компоненты в виде простых функций. Это довольно мило, если вы уже давно используете JavaScript, но не очень знакомы с самим Реактом — это напоминает функции, к котрым мы все привыкли. Давайте почистим props ещё лучше и заново используем иконку зонта, как мы бы сделали это на сайте.

// Приложение
function App() {
  return (
    <div>
      <Header />
      <IconUmbrella />
      <IconUmbrella umbrellafill="#333" />
      <IconUmbrella umbrellafill="#ccc" />
    </div>
  )
}
 
// Шапка
function Header() {
 return (
   <h3>Привет, мир!</h3>
 )
}

// Иконка
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"));

Системы иконок на базе SVG в Реакте просто замечательны и легко расширяемы, с ними нужно меньше HTTP-запросов, и их будет легче поддерживать в будущем, поскольку можно будет полностью обновить вывод, не меняя разметку в нескольких местах. Можно улучшить производительность, выбрав то, что нам нужно. Можно менять их на лету с помощью props для цвета и даже добавить CSS-анимацию. Кроме того,  можно также сделать их доступными для скринридеров, что делает Реакт и системы SVG-иконок по-настоящему классным способом добавления иконок в веб-приложения.

P.S. Это тоже может быть интересно:

2 комментария

  1. Мне кажется, что держать в компоненте svg это как минимум не красиво и не удобно. длинный svg код просто засоряет экран… более красивое решение держать отдельный svg с всеми наборами иконок и подключать через use, впрочем так вы и делали

Добавить комментарий для Кубанычбек Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.