最近、業務でSVG画像の利用を検討する機会があったため調査しました。
「複数の似たような画像」の問題
フロントエンド周りでは良くある話題かと思いますが、プロジェクトでpngやjpeg形式の画像を利用している場合に「微妙に色やサイズが異なる」や「Retina対応」といった問題から、複数の似たような画像を準備・利用する必要が出てきます。
複数の似たような画像が増えてくると、一覧・検索性が悪くなり、使われていない画像や重複して存在する画像が出てくるなど保守性を落とす結果につながります。
また、画像取得のリクエストも増えるため、パフォーマンスにも悪影響を与えることにつながります。
リクエスト数、色・サイズ問題の解消
リクエストに関しては、画像をスプライト画像にまとめてリクエスト数を軽減する方法が近年では一般的だと思います。
色やサイズに関しては、変更可能なベクター形式のSVG画像を利用する方法がメジャーになりつつあり、そのSVG画像をスプライト化したSVGスプライトなるものもgulp
などのタスクランナーを利用する事で、比較的簡単に作成出来きます。
また、モダンブラウザでの利用に限定されますが、svgスプライト#個別svg画像
(以下、フラグメント指定)という形式のURIを記述する事で、backgroud-position
プロパティの指定でスプライト画像内における個別svg画像の座標を指定する必要が無くなる、という手法も存在しているようです。
対応ブラウザ@caniuse.com
今回はgulpにて、上記のフラグメント指定を可能にするsvgスプライトを作成する方法を紹介します。
なお、デフォルトのオプションで、フラグメント指定が可能なsvgスプライトを作成出来るgulpプラグインが見当たりませんでした。
(どなたか知っていたら教えて下さい:(´ཀ`」 ∠):)
svgスプライトを作成するgulpタスク
ディレクトリ構成
まず、前提とするディレクトリ構成になります。
- gulpfile.babel.js
- package.json
- develop/
- build/
- img/
- svg/
- test/(*.svgを格納)
- svg/
- img/
- src/
- template/
- _svg.html(svg画像を一覧するhtmlの雛形)
- template/
- build/
プラグインのインストール
関連プラグインをインストールします。
(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ではなく、gulp-svg-spriteというプラグインを利用する手段もあります。
gulpタスク
やっている事流れは下記です。
1. './develop/build/img/svg'配下のディレクトリ名を取得して配列に格納
2. 上記1で得られたディレクトリ数だけ、下記3~6の処理を実行
3. svg画像を圧縮
4. スプライト化
5. フラグメント指定に対応するためsvg
タグ配下にuse
、view
タグを生成・追加
6. 各ディレクトリ直下に、ディレクトリに格納されるsvg画像を一覧出来るsample_list.html
を追加
タスクは下記です(svgスプライト関連のコードだけ抜き出しているので、ちょっと気持ち悪いですが..)
// プラグインの読み込み
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
を生成する雛形です。
<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に掲載されている、フラグメント指定が行われているSVGスプライトを参考に加工・組み立てる処理をタスクに追加しています。
ちなみに、svgstoreに対するissueは存在しているようですが、対応完了しているのかな。。?
また、詳しく調べていくと、SVG画像はCSSからbackground
プロパティを用いて読み込んだ場合、fill
属性による色指定が適用されない事が分かりました。
つまり、色の変更を行いたい場合は、htmlからuse
タグを利用してfill
属性を変更するしか方法が存在しないという事になります。
想像していたよりも不便ですね。。
色やサイズ・利用方法という観点からはむしろ、SVGスプライトよりSVG画像をwebfont化する「iconfont」の方が便利そうだという教訓を得る事が出来ました。
SVG画像のメリット(iconfontと比較)としては、アニメーションに利用するようなケースで真価を発揮するのかなと思いました。
(javascriptから動的に変化させられるため)
という事で次回は、iconfontを作成するgulpタスクを書いていきたいと考えています。