はじめに
こんにちは! ギフティでエンジニアをしている岸本と申します。
普段は主に to B 向けのプロダクトや社内システムの開発を担当しています。
さて今年度は giftee Mileage Campaign System というプロダクトを新たに立ち上げ、そのフロントエンドフレームワークとしてリリースされてまもない Vue 3 を採用しました。
この記事では Vue 3 の目玉機能であり、 giftee Mileage Campaign System でも利用している Composition API について紹介できればと思います。
giftee Mileage Campaign System とは
まずは簡単にプロダクトの紹介です。
giftee Mileage Campaign System は例えば、「ポイントを一定数貯めることで抽選に参加し、当選したらeギフトがもらえる」のような、抽選会を電子化するシステムになります。
参加者は応募URLを読みこむだけで簡単にポイントを貯め、抽選に参加していただけます。
応募URLは SNS やメールなどで直接配布することもできますし、二次元バーコードとして商品ラベルやレシートに印字するなど、様々な手段で参加者に配布することができます。そのため、ご利用いただく企業様は簡単に購買促進のキャンペーンを実現することができます。
Vue 3 について
Vue 3 となり、メジャーバージョンアップらしい様々な機能の追加と変更がありました。
詳細については公式のドキュメントを参照いただければと思いますが、slot や v-model の仕様変更、 TypeScript のサポート強化など実際に利用したい機能が豊富に搭載されています。
Composition API について
Vue 3 の目玉である Composition API の導入でコンポーネント定義が大きく変わりました。
利用するビジネスロジックを機能ごとにまとめ、再利用可能な形で抽出することで柔軟なコンポーネント定義が可能になります。
例えば従来型のコンポーネント定義(Options API)の場合、アプリケーションのスケールに従って以下のような課題に直面することが多いかと思います。
Options API ごとにコードが整理されてしまうため、ロジックの関心ごとにコードを整理できず、結果的に可読性や保守性が低下する。
複数のコンポーネント間でロジックを抽出して再利用するための、クリーンでコストのかからないメカニズムが存在しない。
これらの課題を解決するには、コンポーネントで取り扱う関心事に従ってロジックを分割し、複数コンポーネント間で利用できるようにすることが効果的です。
コードを書いて確認する
それでは実際に確認してみましょう。 サンプルとなるコードは、「獲得したeギフトの一覧画面」をイメージしており、以下のような仕様と仮定します。
- 利用可能なギフト(
available === true
)のみを表示する - "利用する"をクリックすることで、ギフトが交換済み(
available === false
)になり、画面から非表示になる - 表示するギフトはデフォルトで3件。"もっとみる"をクリックすると残りが表示される。
テンプレートは Vue 2、 Vue 3 で共通の物を利用します。
<template> <div> <h1>Gift List</h1> <li v-for="(gift, i) in paginated" :key="i"> {{ gift.name }} <button @click="exchangeGift(gift.name)">- 利用する</button> </li> <button v-if="paginated.length - count >= 0" type="button" @click="showMore" > もっとみる </button> </div> </template>
Vue 2 の場合
まずは Vue 2系のコンポーネント定義(Options API)で実装した場合です。
import Vue from 'vue'; export default Vue.extend({ data() { return { count: 3, // A. ページネーションを受け持つ gifts: [ // B. ギフトを扱う(APIで取得する想定) { name: "ギフト1", available: true }, { name: "ギフト2", available: true }, { name: "ギフト3", available: true }, { name: "ギフト4", available: true }, ], }; }, computed: { paginated() { // A. ページネーションを受け持つ return this.availableGifts.slice(0, this.count); }, availableGifts() { // B. ギフトを扱う return this.gifts .filter((gift) => gift.available == true) }, }, methods: { showMore() { // A. ページネーションを受け持つ this.count += 3; }, exchangeGift(name) { this.gifts = this.gifts.map((gift) => { if (gift.name === name) { return { name: gift.name, available: !gift.available }; } return gift; }); }, }, });
data
、computed
、methods
でスコープが区切られた お馴染みの書き方です。この状態ではロジックの記述が機能ごとにまとまっていません。
機能を大まかに分けると以下となりますが、これらのロジックがスコープを跨いで存在しており可読性がよくありません。
- A: ページネーションを受け持つ
count
・・・ ギフトの表示数を表す。paginated
・・・ countの数分ギフトを表示する。showMore
・・・ ギフトの表示数を増やす。
- B: ギフトを扱う
availableGifts
・・・ 利用可能なギフトを返す。exchangeGift
・・・ ギフトを利用済みにする。
コンポーネントの責務が大きくなるにつれ、ロジックが散乱していくことが容易に想像できます。
Vue 3 の場合
では同様の機能を Composition API で実装してみましょう。
import { defineComponent, ref, reactive, computed } from "vue"; import { defineComponent, ref, reactive, computed } from "vue"; export default defineComponent({ name: "Campaign", setup() { // A. ページネーションを受け持つ const count = ref(3); const paginated = computed(() => availableGifts.value.slice(0, count.value) ); const showMore = () => { count.value += 3; }; // B. ギフトを扱う const state = reactive({ gifts: [ { name: "ギフト1", available: true }, { name: "ギフト2", available: true }, { name: "ギフト3", available: true }, { name: "ギフト4", available: true }, ], }); const availableGifts = computed(() => state.gifts.filter((gift) => gift.available) ); const exchangeGift = (name) => { state.gifts = state.gifts.map((gift) => { if (gift.name === name) { return { name: gift.name, available: !gift.available }; } return gift; }); }; return { showMore, exchangeGift, paginated }; }, });
data
、computed
、methods
でスコープが区切られることがなくなり、機能ごとに必要なデータと関数をまとめることができました。
代わりに、ロジックやデータは全て setup
関数の中で宣言し、テンプレートで利用するものを return
します。
さらに、まとめたロジックはコンポーネントの外に切り出して利用することが可能です。
以下の例では setup
関数の外に切り出していますが、別ファイルから import して利用することもできます。
import { defineComponent, ref, reactive, computed } from "vue"; export default defineComponent({ name: "Campaign", setup() { return { ...usePagination(), }; }, }); // A. ページネーションを受け持つ const usePagination = () => { const { availableGifts, exchangeGift } = useGifts(); const count = ref(3); const paginated = computed(() => availableGifts.value.slice(0, count.value)); const showMore = () => { count.value += 3; }; return { count, showMore, paginated, exchangeGift, }; }; // B. ギフトを扱う const useGifts = () => { const state = reactive({ gifts: [ { name: "ギフト1", available: true }, { name: "ギフト2", available: true }, { name: "ギフト3", available: true }, { name: "ギフト4", available: true }, ], }); const exchangeGift = (name) => { state.gifts = state.gifts.map((gift) => { if (gift.name === name) { return { name: gift.name, available: !gift.available }; } return gift; }); }; const availableGifts = computed(() => state.gifts.filter((gift) => gift.available) ); return { exchangeGift, availableGifts, }; };
ロジックを setup
関数の外に切り出すことで、コンポーネントへの依存を減らして表現できました。
例えば複数のコンポーネントで共通の処理を実装したい場合など、mixin などの手法を使うことなく、ロジックを再利用することが可能になりました。
実際に使ってみて
よかったところ
setup
関数の中で自由に宣言できるようになり、自由度と可読性が大きく増した。個人的に、Vue 2 以前のコンポーネント定義ではロジックを関心ごとにまとまっていなかったため、コードリーディングにかけるコストが大きかったように感じます。
アロー関数を積極的に利用できるようになった。
data
へアクセスする際にthis.
を指定する必要がなくなりました。これにより、method
、computed
などので中でES6に準拠した記法を扱えるようになりました。return
文の中を見るだけで template で利用されている関数やオブジェクトを判別でき、見通しがよくなる。
注意すること
Composition API の利便性は非常に大きい一方、コンポーネントの設計方針をより厳密に定義する必要がありそうです。 実装の自由度が増した分、ロジックをまとめる粒度、抽象化にはプロジェクト内で一定のルールを設け負債化しないよう努めねばなりません。
終わりに
今回は giftee Mileage Campaign System に導入した Vue 3 の Composition API について紹介しました。 Vue 3 では TypeScript の型サポートも強化されており、開発体験も向上しています。
既存のプロダクトで Vue を採用している場合、 Vue 3 へのマイグレーションには大きなコストがかかります。一方、今後新たに Vue の導入を検討しているのであれば Composition API などの新機能を積極的に利用してみてはいかがでしょうか。
他新機能の紹介もしたいところですが、長くなってしまったので別の記事でできればと思います。引き続きよろしくお願いいたします!