【Vue】svg画像をそのまま出力して色を変える

Vue.jsでsvgを扱うときにはまった出来事です。

みなさんsvg使っていますか？
svgはどんな環境でもきれいに表示できますし、色もスタイルでかえられるので便利ですよね。

そのsvgをVue.jsで出力したいときにはいくつか方法があるかと思います。

1. テンプレートに直接書く
1. `img`タグの`src`にsvgファイルを指定する
1. svgファイルをそのまま出力する

エンジニアがsvgを書ける場合は`1.`の方法をとれば良いかと思うのですが、デザイナーがsvgを書いてそのファイルをそのまま読み込まなければならないこともあるかと思います。
そのときは`2.`か`3.`の方法になるのですが、`2.`では基本的に色を変えることができなく、svgのメリットを享受できません。(やる方法もないわけではないらしいですが)
ということで`3.`の「svgファイルをそのまま出力する」方法を共有します。

## 方針

以下の方針で進めていきます。

- 使いやすいようにコンポーネントに切り出す
- svgファイルは一箇所に集める
    - 今回は`@/assets/images/svg/`以下にsvgファイルが置いてある想定
        - `@/assets/images/svg/hogehoge.svg`
- コンポーネントにはファイル名と色を渡せるようにする
    - 色はcssで指定できる形 
    - 色の指定がなければ継承する

## 実装

- 色は直接`style`にバインド
    - 色の継承のため`currentColor`を使用 
- ファイル名はデフォルトスロットで受け取る
    - `prop`でも良かったが、`Vuetify`の作法に近い方法をとった
- `svg-inline-loader`を使って動的にsvgを読み込む
    - `webpack`が必要です


```vue:Icon.vue
<template lang="pug">
.icon(v-html="getImageSource()" :style="styles" @click="$emit('click')")
</template>

<script lang="ts">
Vue.extend({
    props: {
        color: {
            type: String,
            default: null,
        },
    },
    computed: {
        styles() {
            return {
                color: this.color || "currentColor",
            };
        },
    },
    methods: {
        getImageSource() {
            let source = "";
            try {
                source = require(`!svg-inline-loader!@/assets/images/svg/${this.getLabel()}.svg`);
            } catch (e) {
                // 読み込み失敗時は何もしない
            }
            return source;
        },
        // Vue.jsの仕様により$slotsはリアクティブでないため、computedにはできない
        getLabel() {
            const children = this.$slots.default || [];
            return (children[0] && children[0].text) || "";
        },
    },
});
</script>

<style lang="stylus" scoped>
.icon
    // 自由にスタイルをあててください
</style>
```

こうすると、以下のように使うことができます。
(`my-icon`としてコンポーネント登録した状態です。)

```pug
div
    my-icon(color="#fff") hogehoge
    span fugafuga
```

## 注意点

### svgファイル内での色指定

svgファイルで色が直指定されていたらそちらが優先されるので、そんなときはデザイナーと調整が必要です。

### `defs`での定義
svgファイルをはき出すアプリケーションによっては`defs`を使ってパス等を定義して、`use`を使って参照しているものがあります。
これらは基本idで管理されていて、svgファイル内ではユニーク性が保たれますが、複数のsvgファイルを扱うとidが重複してしまい、意図しない描画となってしまいます。
そんなときは読み込みのタイミングでidをsvgファイル毎にユニークにしてしまうと良いでしょう。

※ `uuid`というモジュールを利用しています。

```ts:Icon.vue(一部だけ切り出し)
const PREFIX = "my-icon";
Vue.extend({
    data: () => ({
        v4: "",
    }),
    mounted() {
        this.v4 = UUID.v4();
    },
    methods: {
        getImageSource() {
            let source = "";
            try {
                source = require(`!svg-inline-loader?idPrefix=${PREFIX}--!@/assets/images/svg/${this.getLabel()}.svg`);
            } catch (e) {
                // 読み込み失敗時は何もしない
            }
            // idをユニークにする
            const regExp = new RegExp(`id="${PREFIX}--.+?"`, 'g');
            source.match(regExp).forEach((result: string) => {
                const id = result.replace('id="', '').replace('"', '');
                const uuid = id.replace(PREFIX, `${PREFIX}--${this.v4}`);
                source = source.split(id).join(uuid);
            });
            return source;
        },
    },
});
```

# まとめ

Vue.jsでのsvgファイルの扱い方をまとめてみました。
いろいろ便利なsvgですが、自由度が高い分、扱い方はしっかりと決めておくのがよいかと思います。

コンポーネントにまとめておけばサイズとかも`prop`でもらえて便利なのでぜひ！