この記事は一休.com アドベントカレンダー 2018の2日目の記事です。
宇都宮です。宿泊事業本部でフロントエンドの開発を行っています。
今回は、最近一休.comに導入した、SVGスプライトによるアイコンの作り方・使い方について紹介します。
アイコンの一般的な使い方
アイコンは、一般的に、以下のような方式で使用されると思います。
- ビットマップ画像(gif, png等)
- アイコンフォント(Font Awosome等)
- SVG
このうち、ビットマップ画像によるアイコンは拡大・縮小に弱いため、様々な解像度の画面に対応する必要のある現代には不向きです。アイコンフォントはベクター画像なので拡大・縮小に強く、豊富なアイコンがライブラリとして提供されているのが魅力です。SVGもアイコンフォントと同様のベクター画像ですが、フォントにはない柔軟性を備えています。
一休.comは歴史のあるサービスのため、これらのアイコンが混在していますが、最近はSVGアイコンを使うことが多いです。
SVGの柔軟性
SVGは、HTMLに直接埋め込んで使用可能です。そのため、CSSによるスタイリングが可能です。アイコンフォントも色の設定やサイズの調整が可能ですが、パーツ毎に色を塗り分けたりするような柔軟なスタイリングは、SVGでしかできません。また、JavaScriptから操作しやすいという特徴もあります。
一休.comにおけるSVGアイコン使用の問題点
一休.comでのSVGアイコンの使用方法は、いくつかの変遷をたどっています。
はじめはimgタグでsvgファイルを読み込む使い方でしたが、これではSVGの柔軟性で挙げた特徴はほとんど活用できません。
<img src="/path/to/icon.svg" />
インラインSVGにすると、柔軟性は得られますが、記述が煩雑になります。
<svg ..(アイコンにもよるが、200バイトくらい).. />
そこで、Vueコンポーネント化が試みられました。以下のように、1つのSVGアイコンに対して、1つのVueコンポーネントを作る設計です。
<template> <svg .../> </template> <script> export default { name: 'some-icon', }; </script>
これによってSVGのスタイリングの柔軟性は増しましたが、この方式には2つ問題がありました。
1つはパフォーマンスで、アイコン1個あたり1KB(minify+gzip)、アイコン20個で約20KBものJSサイズ増加が発生したことです。本来SVGアイコンは1個200~300byte程度で、gzipするとさらに縮みます。Vueコンポーネント化することで、本来のサイズの10倍ほどに膨らんでしまっています(これはVueコンポーネント設計のまずさに起因しているため、個別にコンポーネントを作るのではなく、汎用的なSVGアイコンコンポーネントを導入するようにしていれば、パフォーマンスへの悪影響は緩和できたと思います)。
もう1つの問題は、VueコンポーネントはVueのコンテキストの中でしか使えないことです。一休.comはフルSPAではないので、サーバサイド(aspx/cshtml)で出力している部分もあります。サーバサイドで出力している部分では、Vueコンポーネントは使えないため、imgタグなどを使う必要があります。
これらの問題を解消し、
- SVGの機能をフル活用できる
- パフォーマンス上のオーバーヘッドが最小限である
- JSフレームワークに依存せず、どこでも使える
という条件を満たすソリューションを検討しました。
SVGスプライトとuse要素
SVGについて調べたところ、use要素というものがあることがわかりました。ページ内の別のSVGに定義されている要素を呼び出して使うことができます。
<!-- アイコン定義 --> <svg> <symbol id="someIcon" ... /> </svg> <!-- アイコン使用 --> <svg> <use xlink:href="someIcon" /> <svg>
use要素を使う場合、必要なアイコンをSVGスプライトにまとめて、それをインラインSVGとしてHTML内に書き出す必要があります。インラインSVGはネットワークリクエストが発生しないため、パフォーマンス面ではアイコンを個別に読み込むよりも有利です。
この方式を採用すると、
- SVGの機能をフル活用できる
- パフォーマンス上のオーバーヘッドが最小限である
- JSフレームワークに依存せず、どこでも使える
という要件を全て満たせることがわかりました。
残る課題は現行の開発環境への組み込みです。
gulpによるSVGスプライト
SVGスプライト化にはgulpを使いました。webpackオンリーの環境ならwebpackでやっても良いと思います。webpackの役割をあまり増やしたくないのと、SVGスプライト関係のサンプルコードはgulpを使っているものが多かったこともあり、gulpを使用しました。
SVGスプライトのためのgulpタスクは以下のようになっています。
const gulp = require('gulp'); const path = require('path'); const svgmin = require('gulp-svgmin'); const svgstore = require('gulp-svgstore'); const cheerio = require('gulp-cheerio'); const commonDir = 'path/to/common'; gulp.task('svg-sprite',() => { gulp .src(commonDir + 'icon/*.svg') .pipe( svgmin(file => { const prefix = path.basename( file.relative, path.extname(file.relative), ); return { plugins: [ { cleanupIDs: { prefix: prefix + '-', minify: true, }, }, ], }; }), ) .pipe(svgstore({ inlineSvg: true })) .pipe( cheerio({ run: function($) { $('svg').attr('style', 'display:none'); }, parserOptions: { xmlMode: true }, }), ) .pipe(gulp.dest(commonDir + 'icon-dist')); });
このタスクは以下の流れで処理を行います。
- common/icon 配下にあるsvgファイルを取得
- gulp-svgminを使ってSVGを圧縮
- gulp-svgstoreを使ってSVGを結合
- gulp-cheerioを使って、結合したSVGファイルを非表示に
- common/icon-dist にファイル(icon.svg)を書き出し
このようにして、 https://www.ikyu.com/common/icon-dist/icon.svg (実際にサイトで使用しているSVGスプライト)は作成されています。また、このSVGスプライトは、サーバサイドでHTMLのbodyの開始直後に書き出されています。
利用側では、以下のように、svg要素の中にuse要素を配置し、use要素のxlink:href属性にSVGのid(#ファイル名)を指定することで、アイコンを参照できます。スタイリングはCSSで行います。
<svg class="l-header-search-icon"><use xlink:href="#search"></use></svg>
まとめ
以上、SVGスプライトを使用したアイコンの作り方について紹介しました。