Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Google Material IconsをVueコンポーネントとして書き出す

こんにちは。フロントエンドエンジニアの小林(baco16g)です。

デザインシステムの構築を進めるにあたり、各種アイコンの再設計する必要があります。 再設計の手段としてGoogle Material Iconsを利用する案が生まれたため、Vueコンポーネントとして自動出力する処理を検討しました。

Google Material Iconsの一覧を取得する

まずは、Google Material Iconsとして定義されているアイコン一覧を取得する必要があります。

今回はGoogle Fonts上のMetadataを利用します。

type MetaData = {
  host: string;
  /** 各種アイコンのパステンプレート */
  asset_url_pattern: string;
  /** Outlined, RoundなどのVariants */
  families: string[];
  /** アイコンリスト */
  icons: Icon[];
};

type Icon = {
  /** アイコン名 */
  name: string;
  /** バージョン */
  version: string;
  /** ピクセルサイズ */
  sizes_px: number[];
};

const fetchMetaData = async (): Promise<MetaData> => {
  const res = await axios.get<string>(
    'http://fonts.google.com/metadata/icons',
    { responseType: 'text' }
  );
  const data = JSON.parse(res.data.slice(5));
  return data;
}

アセットURLを作成する

Metadataのasset_url_patternを利用して、各アイコンのダウンロードURLを生成する関数を定義します。asset_url_patternは、/s/i/{family}/{icon}/v{version}/{asset}という形式です。

const buildAssetUrl = (
  metadata: Pick<MetaData, 'host' | 'asset_url_pattern'>,
  icon: Icon,
  family: MetaData['families'][number]
): string => {
  const targetSize = icon.sizes_px.find(size => size === 24)?.toString();
  if (!targetSize) throw new Error('24px is not found in sizes_px.');
  return (
    `https://${metadata.host}` +
    metadata.asset_url_pattern
      .replace('{family}', family.toLowerCase())
      .replace(/\s/g, '')
      .replace('{icon}', icon.name)
      .replace('{version}', icon.version)
      .replace('{asset}', `${targetSize}px`) +
    '.svg'
  );
};

Fetcherを作成する

Metadataを引数として受け取ってFetch関数を返す、高階関数を定義します。 Fetch関数では、書き出し対象のアイコンやVariantの情報を引数として取ります。

import axios from 'axios';

export const createIconFetcher =
  (metadata: Pick<MetaData, 'host' | 'asset_url_pattern'>) =>
  async (
    icon: Icon,
    families: MetaData['families']
  ): Promise<(readonly [family: string, svg: string | undefined])[]> => {
    const res = await Promise.all(
      families.map(async family => {
        const res = await axios
          .get<string>(buildAssetUrl(metadata, icon, family), { responseType: 'text' })
          .catch(() => ({ data: undefined }));
        return [family, res.data] as const;
      })
    );
    return res;
  };

VueコンポーネントのBuilderを定義する

ファイルの見通しを良くするために、コンポーネントのテンプレートを用意しました。

Propsは最小限な titlesizeのみとしています。

<!-- THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) -->

<template>
  <span
    :aria-hidden="!title"
    :aria-label="title"
    role="img"
    v-bind="$attrs"
    v-on="$listeners"
  >
    {{ svg }}
  </span>
</template>

<script>
export default {
  name: '{{ name }}',
  props: {
    title: {
      type: String,
      default: undefined,
    },
    size: {
      type: Number,
      default: 24,
    },
  },
};
</script>

テンプレート文字列の{{ svg }}{{ name }}を対象アイコンの情報に置換しています。 また、widthheightはProps値を用いるため、v-bindに置換しています。

import fs from 'fs';
import path from 'path';

export const build = async (body: string, name: string) => {
  const template = await fs.promises.readFile(
    path.resolve(process.cwd(), './fixtures/Icon.vue'),
    {
      encoding: 'utf-8',
    }
  );
  const code = template
    .replace('{{ name }}', name)
    .replace(
      '{{ svg }}',
      body
        .replace(/width="\d+"/, ':width="size"')
        .replace(/height="\d+"/, ':height="size"')
    );
  return format(code, 'vue');
};

これらの処理を連結させることで、Google Material IconsをVueコンポーネントに自動書き出しすることができました。

さいごに

ここまで技術検証をしておいて...という感じではありますが、ヤプリではGoogle Material Iconsを利用せず、オリジナルアイコンを整備する方針となりました。オリジナルアイコンに関しては今回の処理を一部流用して、Figma APIを使用する予定です。

Yappliでは全方面でソフトウェアエンジニアを募集中です。もしYappliに興味がありましたら是非カジュアル面談でお話しましょう!

open.talentio.com