Еще один способ использовать SVG в React. На этот раз удобный +8


SVG-изображения можно вставлять непосредственно в html код, можно использовать символьные спрайты, теги <img>, <object> и даже <iframe>. Можно подключать SVG через data-url, css-backgrounds, css-filters и еще множеством способов. Но чтобы полноценно использовать всю суперсилу SVG, необходимо вставлять SVG-изображения непосредственно в html-разметку. Хотя на самом деле есть еще один способ. И он удобный.

Суперсила SVG

Одна из самых сильных сторон технологии SVG - возможность манипулировать стилями изображений через css. Это особенно важно для иконок, которые обязательно захочется подсветить на hover без использования дополнительных изображений.

Чтобы иметь возможность перекрашивать SVG через css, необходимо либо вставить SVG непосредственно в html, то есть «заинлайнить» их, либо использовать отдельный файл, символьный спрайт. Спрайт также должен быть вставлен в DOM. Ни одна другая техника не позволяет получить доступ к SVG из css. Рассмотрим эти техники подробнее.

Инлайнинг

Вставка SVG непосредственно в html это одновременно и самая простая и самая мощная с точки зрения кастомизации и манипуляции изображением техника.

<span>
  <svg viewBox="0 0 16 16" width="16" height="16">
    <!-- SVG -->
  </svg>
</span>

Такое изображение можно стилизовать через css, менять практически любое свойство и атрибут, даже анимировать его отдельные части. Но здесь есть ряд существенных ограничений:

  1. Если изображение большое, вся страница будет загружаться дольше

  2. Использование этой техники в js-фреймворках, даже с такими крайне мощными инструментами, как SVGR, сильно увеличивает размер js-бандла.

  3. Теряется контроль над загрузкой SVG, они перестают быть внешними изображениями. Вы не можете отдельно их хранить и кешировать, пренебречь их загрузкой на плохих соединениях и т.д.

Спрайт символов

Другой способ встраивания SVG, который позволяет стилизовать изображения через css, предполагает использование спрайта, состоящего из элементов <symbol>. Контент изображения хранится в отдельном скрытом файле и подключается в нужное место на странице через тег <use>.

<!-- Спрайт символов наверху страницы -->
<svg style="display:none">
  <symbol id="icon" viewBox="0 0 16 16" width="16" height="16">
    <!-- SVG -->
  </symbol>
</svg>

<!-- Использование -->
<span>
  <svg><use href="#icon" /></svg>
</span>

А если вы, как и Microsoft, не поддерживаете IE11, спрайт символов можно загружать по HTTP.

<svg>
  <use href="https://cdn.net/sprite.svg#example"/>
</svg>

К сожаление, использовать такой подход напрямую, то есть загрузить по HTTP содержимое отдельного SVG-изображения не получится, нужен именно спрайт. Вот так работать не будет:

<svg>
  <use href="https://cdn.net/icon.svg"/>
</svg>

К изображениям, подключенным через спрайт, можно получить доступ через css , правда, есть ограничения. Стилям не «пробиться» через shadow-boundary, границу, за которой начинается shadow-DOM и которая описывается тегом <use>.

Хорошие новости в том, что каскад продолжает работать, можно менять атрибуты fill и использовать currentColor в любом свойстве. Для большинства кейсов с иконками этого более чем достаточно:

<!-- Спрайт -->
<svg style="display:none">
  <symbol id="icon">
    <path fill="currentColor">
  </symbol>
</svg>

<!-- Добавляем иконке css-класс -->
<span>
  <svg class="icon"><use href="#icon" /></svg>
</span>

<!-- Красим SVG через css -->
<style>
  .icon {
    color: rebeccapurple;
  }
</style>

Спрайты удобны, но у них немало своих проблем, главная из которых - они плохо расширяемы. В конечном итоге спрайты разрастаются до огромных размеров и целиком подключаются на страницы ради использования одной-двух иконок.

Компромисс

Здесь не обойтись без компромисса. Нужно решение, которое загрузит на страницу только необходимые SVG-изображения, не будет рендерить их через javascript и позволит стилизовать их через css.

Таким решением может быть браузерный fetch! Нам нужен инструмент, который

  1. Загрузит SVG-изображения в браузере через fetch и закеширует их

  2. Добавит их в символьный спрайт

  3. Предоставит API для использования загруженных изображений в произвольном участке страницы через тег <use>.

Первым о таком решении задумался Chris Coyier еще в 2015 году, но оно так и не было оформлено и ждало своего дня. Сегодня этот день настал.

Handy SVG

Реализация называется Handy SVG, исходники и документация доступны на github, пакет публикуется в npm. Вам достаточно установить его:

npm i handy-svg

И использовать в React:

import {HandySvg} from 'handy-svg';
import iconSrc from './icon.svg';

export const Icon = () => (
    <HandySvg
        src={iconSrc}
        className="icon"
        width="32"
        height="32"
    />
);

Или в любом другом проекте с ванильным javascript:

import {injector} from 'handy-svg/lib/injector';

const src = "https://cdn-server.net/icon.svg";

// Загружает изображения и добавляет их в спрайт
injector.load(src);

// Доступ к id изображения
const id = injector.getId(src);

// Использование
const svg = `<svg class="icon"><use href="#${id}" /></svg>`;

Никаких проблем со стилизацией не возникнет:

.icon {
  color: rebeccapurple;
}

На страницу будут загружаться только необходимые изображения, вы сможете их закешировать, а в SPA-приложениях запросы конечно же не будут отправляться дважды. Ваш спрайт не будет разрастаться и вы вообще не будете задумываться о его существовании.

Конечно это решение имеет ряд ограничений. Например, почти невозможно избавиться от FONI (Flash of No Icons), состояния загрузки, при котором на странице отсутствуют иконки. Тем не менее, из всех возможных способов работы с SVG-изображениями в вебе, этот представляется наиболее удобным.


Пул-реквесты приветствуются и я буду рад любому фидбеку.




Комментарии (14):

  1. knotri
    /#24464622 / +1

    >На этот раз удобный

    А что не удобного в стандартном способе
    import { ReactComponent as MyLogo } from './logo.svg';

    • javar
      /#24465080

      А что у тебя прилетает в ReactComponent?

      Если там Реакт-компонент (как я понимаю из контекста), подготовленный через SVGR, например, тогда все svg-изображения становятся частью js-бандла. Это по разным причинам не очень удобно, главная из которых - ты не можешь управлять их загрузкой. Плюс накладные расходы на рендеринг в Реакте.

      А если там url до файла, получаемый через file-loader, то у тебя нет возможности встроить это svg на страницу так, чтобы работала стилизация через css.

    • kellas
      /#24466890

      Ну вот представь что у тебя так вставляется иконка в элемент списка.

      В списке рендерится например 20 строк и вот у тебя в итоге в html коде 20 раз одно и то же содержимое SVG файла. Которое может быть достаточно объемным. Растет Dom сильно.

      Использование спрайтов и символов решает эту проблему.

      P.s. именно лого обычно лучше встраивать через тег img

      • noodles
        /#24470850 / +1

        Тоже когда-то заморачивался и думал, что если 20 пунктов в списке - это 20 одинаковых иконок.. это плохо и нужно обязательно через спрайт и use всё это дело оформлять.

        Однако потом пришло "прозрение" - что по сути иконки - это очень-очень простые фигуры.. как правило один короткий path.. ну может парочку. И поэтому смысла возится со спрайтом вроде как и нет.. ибо сами иконки всего чуть-чуть больше чем объвление тега use. И на 20-ти элементах списка выигрыша почти и нет.

        Если же это иконка сложная - то скорее всего это уже не иконка, а какой-то отдельный элемент\иллюстрация, который встречается один раз на странице (лого там и др.).

        По сути тег <use> - скорее изначально задумывался как хелпер для самих векторных редакторов, чтоб в сложной иллюстрации можно было бы переиспользовать повторяющиеся фигуры и комбинировать их.
        Но потом кто-то придумал использовать этот эффект как возможность организации свг-спрайтов в html..))

  2. torbasow
    /#24464890 / +3

    Ой, а мы ведь нечто подобное делали несколько лет назад. Правда, без Реакта, на ванильном Джаваскрипте. Реакт тут и не при чём, по существу.

    • javar
      /#24465084

      Да, все так, Реакт тут опционален, там в конце есть пример использования на ванильном :)

  3. ionicman
    /#24465156 / +1

    Гспди, зачем?!

    Во-первых при всем этом не получится менять цвета и тд — т.к. SVG становится встроенным объектом (особенно при использовании svg-symbols) и придется поплясать с бубном, чтобы он стал обычным SVG в разметке и к его внутренностям можно было применять CSS.

    Во-вторых все это делается крохотной функцией на vanila js, а не модулем ноды.

    Картинка в тему

  4. litle_fronender
    /#24466874

    Интересно. Ндо будет попробовать. Спасибо за статью.

  5. kellas
    /#24466912 / +1

    Действительно оно из лучших решений! А handy удобный инструмент для организации работы со спрайтами.
    На деле представленные подходы можно и нужно комбинировать.
    Картинки встречающиеся в приложении только один раз - инлайнить.
    Используемые в нескольких местах картинки/иконки - брать из спрайта.
    Статичные изображения не требующие модификации(ну типа лого в футере) - вставляем через обычный тег img
    Что-то можно и в css через data-uri прописать.

    Если по каким-то причинам не удается сменить цвет иконки через fill и store , можно заморочиться с фильтрами на css и подобрать любой цвет через hue-rotate, например

    svg { filter: sepia() saturate(1000%) hue-rotate(0deg) }

  6. jakobz
    /#24467158 / +1

    Вот тут сравнивают перформанс разных способов втыкать svg: https://cloudfour.com/thinks/svg-icon-stress-test/

    Не похоже чтобы спрайты как-то помогали с кучей повторяющихся иконок.

    А у инлайна куча плюсов - он в CRA из коробки, ничего не моргает, грузится сразу все и только нужное. И даже подгружать можно по необходимости - через async-загрузку модулей у вебпака.

    Хотя я тоже был когда-то адептом спрайтов.

    • kellas
      /#24467186

      Хм и правда.. Очень любопытно ... Спрайты в хроме даже дольше отрабатывают