Привет, разработчик!
При верстке макета из PSD часто иконки вставлены в формате SVG. А если нет — прошу их у дизайнера. Ранее я использовал иконочные шрифты, но недавно увидел преимущества спрайтов и решил попробовать с ними поиграться внедрить их в процесс разработки. Мне нравятся иконочные шрифты, но они имеют ряд недостатков(на эту тему почитайте CSSTricks). Эксперимент удался, и вот как я организовал систему.

Условия


Что я хочу получить от спрайтов:
  1. Гибкое управление размером, цветом и поведением(hover, focus etc) иконки
  2. Автоматизация, минимум ручной работы
  3. Кеширование иконок для хорошей скорости загрузки страниц
  4. Удобная вставка иконок в разметку страницы (для шаблонизации html я использую jade)


Моя структура папок:
+-- gulpfile.js                # gulpfile
L--assets                      # здесь редактируем файлы
    L-- jade/                  # шаблонизатор html
    L-- sass/                  # стили
    L-- js/                    # скрипты
    L-- i/                     # картинки, сюда мы и будем вставлять спрайт
L--dist                        # здесь получаем готовый проект


Подробнее о том как работает моя сборка — можете почитать в репозитории.
Для создания спрайта используем gulp, а именно:
  • gulp-svg-sprites — создание спрайта
  • gulp-svgmin — минификация SVG
  • gulp-cheerio — удаление лишних атрибутов из svg
  • gulp-replace — фиксинг некоторых багов, об этом ниже


Поехали!


Устанавливаем плагины(я это делаю глобально и потом линкую):
npm install gulp-svg-sprites gulp-svgmin gulp-cheerio gulp-replace -g
npm link gulp-svg-sprites gulp-svgmin gulp-cheerio gulp-replace

В gulpfile объявляем плагины:
var svgSprite = require('gulp-svg-sprites'),
	svgmin = require('gulp-svgmin'),
	cheerio = require('gulp-cheerio'),
	replace = require('gulp-replace');


Варим спрайт


Первый таск — создаем html-файл с тегами symbol.
gulp.task('svgSpriteBuild', function () {
	return gulp.src(assetsDir + 'i/icons/*.svg')
		// minify svg
		.pipe(svgmin({
			js2svg: {
				pretty: true
			}
		}))
		// remove all fill and style declarations in out shapes
		.pipe(cheerio({
			run: function ($) {
				$('[fill]').removeAttr('fill');
				$('[style]').removeAttr('style');
			},
			parserOptions: { xmlMode: true }
		}))
		// cheerio plugin create unnecessary string '>', so replace it.
		.pipe(replace('>', '>'))
		// build svg sprite
		.pipe(svgSprite({
				mode: "symbols",
				preview: false,
				selector: "icon-%f",
				svg: {
					symbols: 'symbol_sprite.html'
				}
			}
		))
		.pipe(gulp.dest(assetsDir + 'i/'));
});

Давайте разберемся, что тут происходит по частям.
Говорим откуда нам нужно взять иконки и минифицируем их. Переменная assetsDir — для удобства.
return gulp.src(assetsDir + 'i/icons/*.svg')
	// minify svg
	.pipe(svgmin({
		js2svg: {
			pretty: true
		}
	}))

Удаляем атрибуты style и fill из иконок, для того чтобы они не перебивали стили, заданные через css.
.pipe(cheerio({
	run: function ($) {
		$('[fill]').removeAttr('fill');
		$('[style]').removeAttr('style');
	},
	parserOptions: { xmlMode: true }
}))

Но я заметил у данного плагина один баг — иногда он преобразовывает символ '>' в кодировку '>'.
Эту проблему решает следующий кусок таска:
.pipe(replace('>', '>'))

Теперь сделаем из получившегося спрайт и положим в папку:
.pipe(svgSprite({
		mode: "symbols",
		preview: false,
		selector: "icon-%f",
		svg: {
			symbols: 'symbol_sprite.html'
		}
	}
))
.pipe(gulp.dest(assetsDir + 'i/'));

symbol_sprite.html — и есть наш спрайт. Внутри он будет содержать следующее(для простоты у меня пара иконок):
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0"
     style="position:absolute">

    <symbol id="icon-burger" viewBox="0 0 66 64">
        <path fill-rule="evenodd" clip-rule="evenodd" d="M0 0h66v9H0V0zm0 27h66v9H0v-9zm0 27h66v9H0v-9z"/>
    </symbol>

    <symbol id="icon-check_round" viewBox="-0.501 -0.752 18 18">
        <path d="M8.355 0C3.748 0 0 3.748 0 8.355s3.748 8.355 8.355 8.355 8.355-3.748 8.355-8.355S12.962 0 8.355 0zm0 15.363c-3.865 0-7.01-3.144-7.01-7.01 0-3.864 3.145-7.007 7.01-7.007s7.01 3.144 7.01 7.01-3.146 7.007-7.01 7.007z"/>
        <path d="M11.018 5.69l-3.9 3.9L5.69 8.165c-.262-.263-.688-.263-.95 0-.264.263-.264.69 0 .952l1.9 1.903c.132.13.304.196.476.196s.344-.066.476-.197l4.376-4.378c.263-.263.263-.69 0-.952s-.69-.262-.952 0z"/>
    </symbol>

</svg>

Щепотка стилей


Теперь нам нужно сделать стили для нашего спрайта(в данном случае файл .scss). В плагине gulp-svg-sprites мы можем задать этот файл, но вот какая досада — его нельзя сделать при данной настройке:
mode: "symbols"

Я решил сделать для создания scss отдельный таск. Если вы нашли другое решение, напишите в комментариях.
// create sass file for our sprite
gulp.task('svgSpriteSass', function () {
	return gulp.src(assetsDir + 'i/icons/*.svg')
		.pipe(svgSprite({
				preview: false,
				selector: "icon-%f",
				svg: {
					sprite: 'svg_sprite.html'
				},
				cssFile: '../sass/_svg_sprite.scss',
				templates: {
					css: require("fs").readFileSync(assetsDir + 'sass/_sprite-template.scss', "utf-8")
				}
			}
		))
		.pipe(gulp.dest(assetsDir + 'i/'));
});

В свойстве cssFile объявляем, куда положить на scss файл(потом инклудим его).
В свойстве templates объявляем, где взять для него шаблон. Код моего шаблона:
.icon {
	display: inline-block;
	height: 1em;
	width: 1em;
	fill: inherit;
	stroke: inherit;
}
{#svg}
.{name} {
	font-size:{height}px;
	width:({width}/{height})+em;
}
{/svg}

Получаем _svg_sprite.scss следующего содержания:
.icon {
	display: inline-block;
	height: 1em;
	width: 1em;
	fill: inherit;
	stroke: inherit;
}

.icon-burger {
	font-size:64px;
	width:(66/64)+em;
}

.icon-check_round {
	font-size:18px;
	width:(18/18)+em;
}

Скомпилированный css будет таким:
.icon {
	display: inline-block;
	height: 1em;
	width: 1em;
	fill: inherit;
	stroke: inherit;
}

.icon-burger {
	font-size: 64px;
	width: 1.03125em;
}

.icon-check_round {
	font-size: 18px;
	width: 1em;
}

Обратите внимание, что размеры иконок выражены через em, что позволит нам в дальнейшем управлять ими через font-size.
Составляем итоговый таск, чтобы запускать одну команду:
gulp.task('svgSprite', ['svgSpriteBuild', 'svgSpriteSass']);

Подключаем на страницу


Итак мы получили html-файл с иконками и scss-файл с оформлением. Далее подключим файл на страницу, используя кеширование через localStorage. Этот процесс подробно описан в статье Caching SVG Sprite in localStorage.
Подключаем js-файл следующего содержания:
;( function( window, document )
{
	'use strict';

	var file     = 'i/symbol_sprite.html',
		revision = 1;

	if( !document.createElementNS || !document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect )
		return true;

	var isLocalStorage = 'localStorage' in window && window[ 'localStorage' ] !== null,
		request,
		data,
		insertIT = function()
		{
			document.body.insertAdjacentHTML( 'afterbegin', data );
		},
		insert = function()
		{
			if( document.body ) insertIT();
			else document.addEventListener( 'DOMContentLoaded', insertIT );
		};

	if( isLocalStorage && localStorage.getItem( 'inlineSVGrev' ) == revision )
	{
		data = localStorage.getItem( 'inlineSVGdata' );
		if( data )
		{
			insert();
			return true;
		}
	}

	try
	{
		request = new XMLHttpRequest();
		request.open( 'GET', file, true );
		request.onload = function()
		{
			if( request.status >= 200 && request.status < 400 )
			{
				data = request.responseText;
				insert();
				if( isLocalStorage )
				{
					localStorage.setItem( 'inlineSVGdata',  data );
					localStorage.setItem( 'inlineSVGrev',   revision );
				}
			}
		}
		request.send();
	}
	catch( e ){}

}( window, document ) );

Все, мы подключили наш файл на страницу, после первой загрузки он кешируется.
Иконки я встраиваю через миксин jade, т.к. это быстро и удобно:
mixin icon(name,mod)
	- mod = mod || ''
	svg(class="icon icon-" + name + ' ' + mod)
		use(xlink:href="#icon-" + name)

Теперь, чтобы встроить иконку вызываем миксин с её именем:
+icon('check_round','red_mod')
+icon('burger','green_mod')

Результирующий html:
<svg class="icon icon-check_round red_mod">
    <use xlink:href="#icon-check_round"></use>
</svg>
<svg class="icon icon-burger green_mod">
    <use xlink:href="#icon-burger"></use>
</svg>

Открываем страницу в браузере:



Пока размеры иконок в натуральную величину и имеют стандартный цвет. Изменим это(не в сгенерированном файле, а в главном):
.icon-burger {
	font-size:3rem;
	&.green_mod {
		fill:green;
	}

}
.icon-check_round {
	font-size:3rem;
	&.red_mod {
		fill: red;
	}
}

Результат:

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

Размытие


К сожалению, не все дизайнеры делают иконки по пиксельной сетке. В этом случае иконки будут «размываться». Если вы экспортируете иконки из иллюстратора вам нужно включить пиксельную сетку и подогнать размер и расположение иконки под пиксельную сетку. Если вы работаете в готовыми svg-файлами — воспользуйтесь сервисом iconmoon для их правильного выравнивания.

Особую благодарность выражаю @akella, который помог мне в написании данного решения.

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


  1. Grawl
    07.12.2015 11:18
    +2

    О как хитро – через localStorage. Я догадался только в data:URI кодировать и подключать CSS-файл с картинками (пусть и SVG) как отдельный ресурс, но в таком случае их перекрашивать не выйдет. А через inline SVG это куда опрятнее выходит. Здорово всё выглядит.

    Отдельная благодарность за Sass и Jade. Божественная связка.


    1. gatilin222
      07.12.2015 11:27

      Про localStorage подсказали на @cssunderhood)


      1. Eklykti
        07.12.2015 18:35
        +1

        А если на клиенте вдруг выключен js, то вместо иконки юзер увидит хрен?


        1. gatilin222
          07.12.2015 21:21

          osvaldas.info/caching-svg-sprite-in-localstorage Здесь написано, что делать в этом случае.


    1. dom1n1k
      07.12.2015 16:10
      +3

      Я как ни старался, никакой божественности в Jade не ощутил. Есть, конечно, свои плюсы и сильные стороны, но некоторое количество минусов перечеркивают их полностью. Проще по старинке сверстать.


      1. gatilin222
        07.12.2015 21:22

        Согласен, нет ничего божественного, просто подходит под мои задачи)


  1. Houston
    07.12.2015 11:24

    Для вебпака тоже такое есть, если кому нужно github.com/kisenka/svg-sprite-loader


  1. TNK
    07.12.2015 15:20

    Есть же gulp-svgstore. Вместо кэширования в localStorage предпочитаю использовать внешний файл-спрайт и svg4everybody. Ещё покорёбило, что результат склейки svg назвали .html.


    1. gatilin222
      07.12.2015 15:48

      Так я и показал, что это один из способов решения) Используйте те плагины, которые вам удобны)


  1. Iskin
    07.12.2015 18:22
    +1

    Я инлайню SVG прямо в CSS. С помощью postcss-svg можно сменить цвет. Правда нет анимаций, зато опрятнее выходит — меньше магии и нормальное кеширование.


    1. Large
      08.12.2015 17:33

      при подключении через символ тоже не работает цсс анимация.


      1. gatilin222
        08.12.2015 18:23

        Друг, вы не правы, все замечательно работает)


        1. Large
          08.12.2015 18:56

          jsfiddle.net/v5y027r1 варианты с use так же не работают. если у вас есть рабочий вариант — поделитесь.

          Обсуждали здесь — habrahabr.ru/company/devexpress/blog/269331


          1. Large
            08.12.2015 19:24

            налажал в коде выше. вот правильный вариант jsfiddle.net/v5y027r1/5 проверять в хроме.


          1. gatilin222
            08.12.2015 20:04

            Я имел ввиду transition) А то что animation не работает — не страшно, он и не нужен для иконок(для меня)


  1. Iskin
    07.12.2015 18:23

    Вообще, конечно, кеширование в localStorage — очень неправильный подход. localStorage создан для данных, а не кеша ресурсов. Например, когда пользователь нажмёт «Очистить кеш», то localStorage не будет очистен.


    1. JiLiZART
      08.12.2015 02:05

      Кешем могут теперь заниматься модные нынче service worker'ы dev.opera.com/service-worker.js


      1. gatilin222
        08.12.2015 07:14

        Спасибо за наводку, еще не изучал их


      1. mr47
        08.12.2015 21:19

        Модные — да. Но рано их использовать.


  1. jmaks13
    08.12.2015 09:52

    Если кому то интересно, то можно создать svg спрайт иначе, вот статья
    www.liquidlight.co.uk/blog/article/creating-svg-sprites-using-gulp-and-sass


    1. gatilin222
      08.12.2015 10:15

      Это тоже хороший способ, но мне он не подходит(


  1. Punk_UnDeaD
    09.12.2015 14:32

    > Результирующий html:

    Традиционно ужасен. Это не упрёк автору, просто констатация факта.


  1. Xu4
    14.12.2015 04:14
    +1

    Но я заметил у данного плагина один баг — иногда он преобразовывает символ '>' в кодировку '&g t;', только без пробела между символами 'g' и 't'(если убрать пробел — получится символ).

    Это замечание относится конкретно к статье на Хабре: вы не учли, что в кодах есть символ «&», который можно написать как «&amp;». Соответственно, если вы хотите написать «&gt;», то нужно писать это так: &amp;gt; — оно при публикации статьи превратится в &gt;

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


    1. gatilin222
      14.12.2015 07:36
      +1

      Спасибо вам! А я то мучался, а оно вон все как просто)