一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

SVGスプライトアイコンの作り方・使い方

この記事は一休.com アドベントカレンダー 2018の2日目の記事です。

qiita.com


宇都宮です。宿泊事業本部でフロントエンドの開発を行っています。

今回は、最近一休.comに導入した、SVGスプライトによるアイコンの作り方・使い方について紹介します。

f:id:ryo-utsunomiya:20181201135110p:plain
StorybookのSVGスプライトアイコン一覧

アイコンの一般的な使い方

アイコンは、一般的に、以下のような方式で使用されると思います。

  1. ビットマップ画像(gif, png等)
  2. アイコンフォント(Font Awosome等)
  3. 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タグなどを使う必要があります。

これらの問題を解消し、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. JSフレームワークに依存せず、どこでも使える

という条件を満たすソリューションを検討しました。

SVGスプライトとuse要素

SVGについて調べたところ、use要素というものがあることがわかりました。ページ内の別のSVGに定義されている要素を呼び出して使うことができます。

<!-- アイコン定義 -->
<svg>
  <symbol id="someIcon" ... />
</svg>

<!-- アイコン使用 -->
<svg>
  <use xlink:href="someIcon" />
<svg>

use要素を使う場合、必要なアイコンをSVGスプライトにまとめて、それをインラインSVGとしてHTML内に書き出す必要があります。インラインSVGはネットワークリクエストが発生しないため、パフォーマンス面ではアイコンを個別に読み込むよりも有利です。

この方式を採用すると、

  1. SVGの機能をフル活用できる
  2. パフォーマンス上のオーバーヘッドが最小限である
  3. 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'));
});

このタスクは以下の流れで処理を行います。

  1. common/icon 配下にあるsvgファイルを取得
  2. gulp-svgminを使ってSVGを圧縮
  3. gulp-svgstoreを使ってSVGを結合
  4. gulp-cheerioを使って、結合したSVGファイルを非表示に
  5. common/icon-dist にファイル(icon.svg)を書き出し

このようにして、 https://www.ikyu.com/common/icon-dist/icon.svg (実際にサイトで使用しているSVGスプライト)は作成されています。また、このSVGスプライトは、サーバサイドでHTMLのbodyの開始直後に書き出されています。

f:id:ryo-utsunomiya:20181201133705p:plain

利用側では、以下のように、svg要素の中にuse要素を配置し、use要素のxlink:href属性にSVGのid(#ファイル名)を指定することで、アイコンを参照できます。スタイリングはCSSで行います。

<svg class="l-header-search-icon"><use xlink:href="#search"></use></svg>

まとめ

以上、SVGスプライトを使用したアイコンの作り方について紹介しました。

参考文献

GulpでSVGスプライトとアイコン一覧を一発生成 - Bit Journey's Tech Blog