Пару дней назад вышла полностью обновлённая версия svg-sprite-loader — webpack лоадера для создания SVG спрайтов. Внутри я подробно рассскажу о том как он работает и чем облегчает жизнь разработчику.


Изображение как модуль


Для webpack любой файл — модуль. Следуя этой концепции svg-sprite-loader, настроенный на обработку SVG, при импорте вида:


import twitterLogo from './logos/twitter.svg';

сделает следующее: содержимое изображения будет трансформировано в <symbol> и передано в модуль, который сгенерирует клиентский код для работы с символом и спрайтом. Этот код будет вызван во время исполнения программы как обычный модуль. Полученный код выглядит примерно так:


// Импорт класса, в который будет обёрнуто содержимое изображения после преобразования в <symbol>
import SpriteSymbol from 'svg-sprite-loader/runtime/symbol';

// Импорт объекта-синглтона, который представляет собой SVG спрайт и играет роль глобального хранилища символов
import globalSprite from 'svg-sprite-loader/runtime/browser-sprite';

// Создание экземпляра класса символа
const symbol = new SpriteSymbol({ /* symbol data */ });

// Добавление символа в спрайт
globalSprite.add(symbol);

// Модуль возвращает созданный экземпляр класса SpriteSymbol
export default symbol;

Таким образом SVG файл, будучи изначально простым текстом, превращается в объект с полями id, viewBox и content, на который в дальнейшем можно сослаться разметкой <svg><use xlink:href="#id"></svg>. Однако, чтобы такая ссылка работала, спрайт со всеми символами должен быть частью страницы. Помните объект-синглтон globalSprite, который хранит в себе все символы? Его код выглядит примерно так:


import BrowserSprite from '…';

const sprite = new BrowserSprite();
document.addEventListener('DOMContentLoaded', () => {
  sprite.mount(document.body);
});

export default sprite;

То есть спрайт автоматически вставится в страницу как только произойдёт событие DOMContentLoaded. Одной строкой импорта изображения мы преобразовали его в объект и создали спрайт, который сам отрисуется когда и куда надо. Удобно. Однако ссылаться на символ всё ещё приходится руками.


Решить эту проблему можно используя шаблонизатор. На примере React создадим компонент, который отрисовывает символ спрайта:


// icon.jsx
export default function Icon({glyph, viewBox = '0 0 16 16', className = 'icon', …props}){
  return (
    <svg className={className} viewBox={viewBox} {…props}>
      <use xlinkHref={`#${glyph}`} />
    </svg>
  );
}

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


import Icon from './icon.jsx';
import twitterLogo from './logos/twitter.svg';

<Icon glyph={twitterLogo.id} viewBox={twitterLogo.viewBox} />

Удобно. Однако всё ещё приходится указывать id символа и viewBox. А что если при импорте SVG возвращать не объект символа, а компонент использующий этот символ для отрисовки? Это возможно благодаря опции runtimeGenerator, которая указывает путь к Node.js модулю генерирующему обвязку над SVG изображением. Пример такого генератора можно посмотреть тут. Он произведёт следующий код:


import React from 'react';
import SpriteSymbol from 'runtime/symbol';
import globalSprite from 'runtime/global-browser-sprite';
import Icon from './icon.jsx';

const symbol = new SpriteSymbol({ /* symbol data */ });
globalSprite.add(symbol);

export default function TwitterIcon({…props}) {
  return <Icon glyph={symbol.id} viewBox={symbol.viewBox} {…props} />;
}

И тогда импорт изображения уже вернёт React-компонент с предустановленными свойствами:


import TwitterLogo from './logos/twitter.svg';

render(
  <div>
    <TwitterLogo width="100" />
    <TwitterLogo fill="red" />
    <TwitterLogo fill="blue" style={{width: 600}} />
  </div>,
  document.querySelector('.app')
);

Удобно. И всё это одной строкой импорта.


Server side rendering


Создавать спрайт на лету в браузере конечно удобно, но что делать если код отрисовывающий ui работает на сервере? Там нет DOM и браузерных событий, как тогда спрайт отрисует сам себя? Для этих целей svg-sprite-loader предлагает использовать изоморфный вариант спрайта, работающий в любой среде исполнения. Он ничего не знает про окружение, поэтому вызывать отрисовку нужно вручную. Пример отрисовки страницы на сервере:


import template from 'page-view.twig';
import globalSprite from 'svg-sprite-loader/runtime/sprite';

// Конвертируем массив символов спрайта в объект где ключами выступают id символов
const symbols = globalSprite.symbols.reduce((acc, s) => {
  acc[s.id] = s;
  return acc;
}, {});

// Конвертируем спрайт в строку вида '<svg><symbol id="…">…</symbol>…</svg>'
const sprite = sprite.stringify();
const content = template.render({ sprite, symbols });

В для отрисовки спрайта нужно вывести переменную sprite, а для вставки символа использовать объект symbols:


// layout.twig
<body>
{{ sprite }}

// pages/about.twig
<svg viewBox="{{ symbols.twitter.viewBox }}">
  <use xlink:href="#{{ symbols.twitter.id }}" />
</svg>
</body>

В случае с React всё будет работать также как в браузере, с возможностью скомпилировать компонент при импорте.


Спрайт как отдельный файл


Но что если использование конструкции <svg><use … /></svg> в разметке не возможно или требует больших переделок? Все уже привыкли к использованию SVG в качестве background-image, ведь если вы не пишете SPA, то интерфейсные изображения логичнее импортировать из стилей, чем из разметки.


Для такого случая предусмотрен специальный режим, который работает совсем по-другому и требует наличия дополнительного плагина (идущего в поставке с лоадером). Предположим имеется такой импорт:


.logo { backgroung-image: url(./logos/twitter.svg); }

Лоадер в комбинации с плагином сделают следующее:


  1. Все изображения будут конвертированы в <symbol> и помещены в спрайт, который будет создан в виде отдельного файла (sprite.svg по умолчанию).


  2. Все импорты изображений будут заменены на путь к спрайту с id символа на конце:

.logo { backgroung-image: url(sprite.svg#twitter); }

Такой подход позволяет применить технику SVG стеков, которая поддерживается всеми браузерами кроме Safari (мобильным и десктопным) и Android browser вплоть до 4.4.4. Но как всегда есть полифилл.


Автоконфигурирование


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


  • Extract-режим — если SVG импортируется из CSS/SCSS/LESS/Stylus/HTML — спрайт будет создаваться в виде отдельного файла.
  • Окружение — если код собирается webpack'ом под браузер (target: browser), будет использован модуль спрайта для браузера. В противном случае — его изоморфный вариант.
  • Формат экспорта модуля — для webpack 1 лоадер генерирует такой экспорт module.exports = …, для webpack 2 и старше export default ….

Это значит что в большинстве случаев стандартной настройки лоадера должно хватить:


// для webpack 1
module: {
  loaders: [
    {
      test: /\.svg$/,
      loader: 'svg-sprite-loader'
    }
  ]
}

// для webpack 2
module: {
  rules: [
    {
      test: /\.svg$/,
      loader: 'svg-sprite-loader'
    }
  ]
}

Что ещё


  • Если настроить лоадер на растровые изображения (png или jpg), он закодирует их в base64 и обернёт в <svg><image xlink:href="base64data"></svg>. Это удобно при разработке, когда векторная версия логотипа/иконки ещё не отрисована, а показать результат уже надо.
  • Рантайм модуль для браузера решает многие проблемы работы с SVG спрайтами под капотом. Смотрите его конфиг и исходный код.
  • В extract режиме лоадера есть возможность создать сколько угодно спрайтов или по спрайту для каждого чанка.

Ссылки


Поделиться с друзьями
-->

Комментарии (10)


  1. justboris
    02.05.2017 09:30

    В статье не хватает объяснения, зачем вообще нужны svg-спрайты и почему не подходит стандартный подход вебпака с инлайном в data-uri


    1. AiZen_13
      02.05.2017 15:47

      По-моему svg-спрайты не нуждаются в представлении


      1. justboris
        02.05.2017 15:57

        А я вот с ходу не смог найти, почему вынесение иконок в отдельный файл лучше, чем инлайн их в css через data-uri.


        Если вы знаете, поделитесь информацией, пожалуйста.


        1. AiZen_13
          02.05.2017 16:00
          +1

          1) возможность раскраски а-ля иконочный шрифт
          2) от data-uri css-ник разбухнет


          1. justboris
            02.05.2017 16:05

            Так понятнее. Спасибо!


          1. qtuz
            02.05.2017 16:27

            возможность раскраски а-ля иконочный шрифт

            возможности раскраски ограничены всего 2 цветами, но для одноцветных иконок подходит, очень удобно.


            от data-uri css-ник разбухнет

            нет, если их не кодировать в base64, а вставлять как есть, экранировав специальные символы. Так делает svg-url-loader. Тогда объём будет меньше, браузер быстрее парсить такой контент, да и гзипится оно лучше.


        1. Aingis
          02.05.2017 16:13

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


          А без JS это не только выключенные скрипты у примерно 1% пользователей: он может не загрузиться, сломаться, быть заблокирован расширением или даже оказаться на забаненном Роскомнадзором CDN — таких в какой-то момент может оказаться весьма много.


          1. qtuz
            02.05.2017 16:36

            Я сам больше за использование SVG из CSS, но поддержка в Safari и Android до 4.4 не позволяет полностью насладиться этой элегантной красотой :)


            1. Aingis
              02.05.2017 17:24

              SVG поддерживается в Андроиде с версии 4.0. И какие проблемы в Safari?


              1. qtuz
                02.05.2017 17:28

                Я имел ввиду использование SVG-стеков из CSS (ссылаться на символ в спрайте по его id — .logo {background-image: url('sprite.svg#logo')}), сейчас это невозможно из-за Safari и Android Browser. Но есть js-полифиллы.