SVG画像の使い方、gulpでcssからも読み込めるSVGスプライトを作成


最近、業務でSVG画像の利用を検討する機会があったため調査しました。

## 「複数の似たような画像」の問題
フロントエンド周りでは良くある話題かと思いますが、プロジェクトでpngやjpeg形式の画像を利用している場合に「微妙に色やサイズが異なる」や「Retina対応」といった問題から、複数の似たような画像を準備・利用する必要が出てきます。

複数の似たような画像が増えてくると、一覧・検索性が悪くなり、使われていない画像や重複して存在する画像が出てくるなど保守性を落とす結果につながります。

また、画像取得のリクエストも増えるため、パフォーマンスにも悪影響を与えることにつながります。

## リクエスト数、色・サイズ問題の解消
リクエストに関しては、画像をスプライト画像にまとめてリクエスト数を軽減する方法が近年では一般的だと思います。

色やサイズに関しては、変更可能なベクター形式のSVG画像を利用する方法がメジャーになりつつあり、そのSVG画像をスプライト化したSVGスプライトなるものも`gulp`などのタスクランナーを利用する事で、比較的簡単に作成出来きます。

また、モダンブラウザでの利用に限定されますが、`svgスプライト#個別svg画像`(以下、フラグメント指定)という形式のURIを記述する事で、`backgroud-position`プロパティの指定でスプライト画像内における個別svg画像の座標を指定する必要が無くなる、という手法も存在しているようです。
[対応ブラウザ@caniuse.com](http://caniuse.com/#feat=svg-fragment)

今回はgulpにて、上記のフラグメント指定を可能にするsvgスプライトを作成する方法を紹介します。
なお、デフォルトのオプションで、フラグメント指定が可能なsvgスプライトを作成出来るgulpプラグインが見当たりませんでした。
(どなたか知っていたら教えて下さい_:(´ཀ`」 ∠):_)

## svgスプライトを作成するgulpタスク
### ディレクトリ構成
まず、前提とするディレクトリ構成になります。 

* gulpfile.babel.js
* package.json
* develop/
	* build/
		* img/
			* svg/
				* test/(*.svgを格納)
	* src/
		* template/
			* _svg.html(svg画像を一覧するhtmlの雛形)

### プラグインのインストール
関連プラグインをインストールします。
(package.jsonが準備されている前提)
`$ npm i -D gulp-svgmin gulp-svgstore gulp-cheerio fs path`

> gulp-svgmin   - svg画像の圧縮
> gulp-svgstore - svg画像をスプライト化
> gulp-cheerio  - svgスプライトを加工
> fs, path      - ディレクトリ取得・操作

なお、[gulp-svgstore](https://www.npmjs.com/package/gulp-svgstore)ではなく、[gulp-svg-sprite](https://www.npmjs.com/package/gulp-svg-sprites)というプラグインを利用する手段もあります。

### gulpタスク

やっている事流れは下記です。
1. './develop/build/img/svg'配下のディレクトリ名を取得して配列に格納
2. 上記1で得られたディレクトリ数だけ、下記3~6の処理を実行
3. svg画像を圧縮
4. スプライト化
5. フラグメント指定に対応するため`svg`タグ配下に`use`、`view`タグを生成・追加
6. 各ディレクトリ直下に、ディレクトリに格納されるsvg画像を一覧出来る`sample_list.html`を追加


タスクは下記です(svgスプライト関連のコードだけ抜き出しているので、ちょっと気持ち悪いですが..)

```gulpfile.babel.js
// プラグインの読み込み
import svgmin   from 'gulp-svgmin';
import svgstore from 'gulp-svgstore';
import cheerio  from 'gulp-cheerio';
import fs       from 'fs';
import path     from 'path';

// 設定
const conf = {
  // svg config
  svgBaseDir : './develop/build/img/svg',
  // template config
  tmpSrcDir  : './develop/src/template'
};

// 指定ディレクトリ内に存在するディレクトリ名を再帰的に取得
const getFolders = dir => {
    return fs.readdirSync(dir)
        .filter(file => {
            return fs.statSync(path.join(dir, file)).isDirectory();
        });
};

/**
 * Svg Sprite Tasks
 **/
gulp.task('svg_sprite', () => {
    const baseDir = conf.svgBaseDir;
    // baseDir配下のディレクトリ名を再帰的に取得
    const folders = getFolders(baseDir);

    folders.map(folder => {
        // svgスプライトの素材対象
        const srcGlob = conf.svgBaseDir + '/' + folder + '/*.svg',
        // サンプルガイドの格納先ディレクトリ
              templateSrcGlob  = conf.tmpSrcDir + '/_svg.html',
              templateDestGlob = baseDir + '/' + folder;

        gulp.src(srcGlob, { base: baseDir })
            .pipe(svgmin())
            .pipe(svgstore({ inlineSvg: true }))
            .pipe(cheerio({
                run: ($, file) => {
                    const $svgTag = $('svg');

                    // svg画像の属性を抽出($.mapは引数指定が逆)
                    const symbols = $svgTag.find('symbol').map((idx, item) => {
                        // viewBox内の値を抽出・配列に分割
                        const viewBoxArr = $(item).attr('viewBox').match(/\d+/g);
                        const symbolObj = {
                            'id'    : $(item).attr('id'),
                            'posX'  : viewBoxArr[0],
                            'posY'  : viewBoxArr[1],
                            'width' : viewBoxArr[2],
                            'height': viewBoxArr[3]
                        };
                        return symbolObj;
                    }).get();

                    // 指定したタグと属性オブジェクトを元にタグのグループ(配列)を生成
                    const tagGroupMaker = (tag, callback) => {
                        const tagArr = symbols.map((item, idx) => {
                            let heightArr = [];
                            let reduceHeight = 0;
                            if (idx > 0) {
                                let $i = 0;
                                for (; $i < idx; $i++) {
                                    heightArr.push(symbols[idx-1].height);
                                }
                                reduceHeight = heightArr.reduce((prev, current)=>{
                                    return parseInt(prev, 10) + parseInt(current, 10);
                                });
                            }

                            const buildTag = $(tag).attr(callback(item, reduceHeight));
                            return buildTag;
                        });

                        return tagArr;
                    };

                    // useタグの組み立て
                    const useTagGroup = tagGroupMaker(
                        '<use/>',
                        (item, posY) => {
                            return {
                                'xlink:href': `#${item.id}`,
                                'width'     : item.width,
                                'height'    : item.height,
                                'x'         : item.posX,
                                // y座標位置の調整(重ならないようにする、余白の設定)
                                'y'         : posY
                            };
                        }
                    );

                    // viewタグの組み立て
                    const viewTagGroup = tagGroupMaker(
                        '<view/>',
                        (item, posY) => {
                            return {
                                'id'     : `${item.id}_css`,
                                'viewBox': `0 ${posY} ${item.width} ${item.height}`
                            };
                        }
                    );

                    // svg配下に組み立てたタグを追加
                    $svgTag.append(useTagGroup).append(viewTagGroup);

                    $svgTag.attr({
                        // デフォルトは非表示
                        'display': 'none',
                        // cssからのハッシュリンク読み取りを有効にする設定
                        'xmlns:xlink': 'http://www.w3.org/1999/xlink'
                    });
                    // fill属性をリセット
                    $('[fill]').removeAttr('fill');

                    // _template.htmlを基に、_sample_list.htmlを生成
                    gulp.src(templateSrcGlob)
                        .pipe(template({
                            inlineSvg  : $svgTag,
                            symbols    : symbols,
                            spriteName : folder
                        }))
                        .pipe(rename('sample_list.html'))
                        .pipe(gulp.dest(templateDestGlob));
                },
                parserOptions: { xmlMode: true }
            }))
            .pipe(rename(path => {
                path.basename = folder;
            }))
            .pipe(gulp.dest(baseDir));
    });
});
```

### sample_list.htmlの雛形
SVG画像を一覧する`sample_list.html`を生成する雛形です。

```_svg.html
<html>
<head>
    <title><%= spriteName %>.svg</title>
    <style>
        html {
            background: #eee;
        }
        body {
            margin: 0;
        }
        svg {
            width: 70px;
            height: 70px;
        }

        .title {
            padding: 7px;
            margin: 0px auto 35px;
            text-align: center;
            background: #5a5a5a;
            color: #fff;
        }
        .title span {
            display: inline-block;
            font-size: 25px;
            font-family: monospace;
        }
        .svg-icon-list {
            padding: 0;
            text-align: center;
        }
        .svg-icon-list__item {
            display: inline-block;
            width: 100px;
            height: 100px;
            margin: 0 5px 10px;
            padding: 10px 5px 0;
            text-align: center;
            vertical-align: top;
            background: #fff;
        }
        .svg-icon-list__item__id {
            margin: 0;
            font-weight: bold;
            font-family: monospace;
            font-size: 17px;
            word-break: break-all;
        }
    </style>
</head>
<body>
<div class="container">
    <%= inlineSvg %>
    <h1 class="title"><span><%= spriteName %>.svg</span></h1>
    <ul class="svg-icon-list">
        <% _.each(symbols, function(symbol) { %>
        <li class="svg-icon-list__item">
            <svg><use xlink:href="#<%= symbol.id %>"></use></svg>
            <p class='svg-icon-list__item__id'><%= symbol.id %></p>
        </li>
        <% }); %>
    </ul>
</div>
</body>
</html>
```


## 考察

フラグメント指定可能なSVGスプライトについて、日本語の文言があまり無かったため、[css-tricks](https://css-tricks.com/svg-fragment-identifiers-work/)に掲載されている、フラグメント指定が行われているSVGスプライトを参考に加工・組み立てる処理をタスクに追加しています。
ちなみに、[svgstoreに対するissue](https://github.com/w0rm/gulp-svgstore/issues/27)は存在しているようですが、対応完了しているのかな。。？

また、詳しく調べていくと、SVG画像はCSSから`background`プロパティを用いて読み込んだ場合、`fill`属性による色指定が適用されない事が分かりました。

つまり、色の変更を行いたい場合は、htmlから`use`タグを利用して`fill`属性を変更するしか方法が存在しないという事になります。

想像していたよりも不便ですね。。
色やサイズ・利用方法という観点からはむしろ、SVGスプライトよりSVG画像をwebfont化する「iconfont」の方が便利そうだという教訓を得る事が出来ました。

SVG画像のメリット(iconfontと比較)としては、アニメーションに利用するようなケースで真価を発揮するのかなと思いました。
(javascriptから動的に変化させられるため)

という事で次回は、iconfontを作成するgulpタスクを書いていきたいと考えています。