こんにちは。フロントエンドエンジニアの小林(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は最小限な title
とsize
のみとしています。
<!-- 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 }}
を対象アイコンの情報に置換しています。
また、width
とheight
は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に興味がありましたら是非カジュアル面談でお話しましょう!