【Vue3に備える】実務で使うComposition APIについて考える
◆はじめに
どうもこんにちは。Vueが好きすぎて社内でVueを布教している@_slontです。
今回のテックブログでは、これからリリースされる待望のVue 3を万全の体制で迎えるべく、新機能の中でも個人的にアツいと思っているComposition APIについての考察をしたいと思います。
Composition APIは一体僕たちにどんな驚きをプレゼントしてくれるのか。それを確かめるために我々はアマゾンの奥地へとむk(ry
…
さて、近々リリース予定のVue 3は、パフォーマンス改善の他、Composition API, Fragment, Portal, Suspenceなど様々な新機能があります。
その中でも、大規模プロジェクトに弱いと言われていたVueの銀の弾丸として(?)、ユーザが待ち望んだComposition APIが、2系のプラグイン@vue/composition-apiとして先取りできるということで、僕の担当するプロジェクトでも人柱として利用しています。
今回はこのComposition APIを実際に使ってみて感じる、メリット・デメリットや実務での使い所を考察してみようと思います。
免責になりますが、これはあくまで個人的な考察であり、Vueやステークホルダーの思想を汲み取った実装方針ではない可能性もあることをご了承ください。
◆対象
この記事の対象は
- Composition APIを検討している
- Composition APIプラグインを導入して触り始めている
といった方です。
「そもそもComposition APIって何?」
という方は、前述したkazuponさんのスライド(まもなくやってくる Vue.js 3)で、Vue 3の素敵機能に目を通すと良いと思います。
◆ 実装例
早速ですが、イメージを掴んでもらうために、実装例の一部を紹介します。実際に使っている中でもよくある、データ配列をフェッチして、テーブルで表現するといった画面です。
今回使った例のリポジトリは以下に上げてあります
また、Composition API Ver.で利用している各種モジュールの詳細は、この記事の後半に載せています。
イメージ
これはよくあるテーブルでのデータ表示と、左にあるチェックボックスでの実装です。
今回は選択状態を管理するためのTableModuleと、データ管理をするItemModuleで実装しました。
非常に簡潔で、切り出す必要があるかを疑問に思いますが、たかがテーブルと言えども、各ページで多種多様なフィルタや検索機能を設けると、このレベルのコードでも重複して肥大化しがちなのがVueなのです。
ソースコード
Vue Syntax Ver.(従来の書き方)
/VueSyntax.vue
<template>
<div id="vue-syntax">
<h3 class="title is-3">Vue Syntax Ver.</h3> <table class="table is-fullwidth">
<thead>
<tr>
<th class="cell-checkbox">
<b-checkbox :value="selectedFlag" :true-value="2" :false-value="0" :indeterminate="1 === selectedFlag"
@input="checkAll"/>
</th>
<th class="cell-name">名前</th>
</tr>
</thead> <tbody v-if="isLoading"><tr><td>読み込み中</td></tr></tbody>
<tbody v-else>
<tr v-for="item in items" :key="item.id">
<td class="cell-checkbox">
<b-checkbox v-model="item.selected"/>
</td>
<td class="cell-name" @click="onClick(item)">
{{ item.name }}
</td>
</tr>
</tbody>
</table>
</div>
</template><script lang="ts">
import Vue from 'vue'
import {Item, Selectable} from '@/types'
import ItemApi from '@/apis/ItemApi' type Test = Item & Selectable export default Vue.extend({
data() {
return {
// データ
items: [] as Test[],
isLoading: false,
// Viewでは使わない
isLast: false,
// Viewでは使わない
cursor: null as string | null
}
},
computed: {
// for テーブル操作
selectedFlag() {
if (this.items.length && this.items.every((item: Selectable) => item.selected)) {
return 2
} else if (this.items.some((item: Selectable) => item.selected)) {
return 1
}
return 0
}
},
created() {
this.getList()
},
methods: {
// for テーブル操作
checkAll(flag: number) {
this.items.forEach((item: Selectable) => item.selected = 2 === flag)
},
onClick(item: Item) {
// 前処理してから画面遷移など
// ...
this.$router.push(`/items/${item.id}`)
},
// Viewでは使わない
getList() {
if (this.isLoading || this.isLast) return this.isLoading = true
return new ItemApi().getList({cursor: this.cursor}).then((res: any) => {
this.items.push(...res.data.items)
if ((this.cursor = res.data.nextCursor) == null) {
this.isLast = true
}
}).finally(() => this.isLoading = false)
}
}
})
</script>
Class Based Component
では無く、Vue.extend
による実装です。こちらは生のVueに近い書き方が可能なので、僕はこっちの方が好きです。
中身は普通なのですが、こうして見ると、テーブル操作とデータ操作の変数・関数がごちゃ混ぜになっていることがわかるかと思います。
このレベルのコンポーネントでこれなので、これ以上複雑な画面だと如何にscript
部が肥大化するかは想像に難くありません。
Composition API Ver.
さて、Composition APIでの実装も見てみましょう。
CompositionApi.vue
<template>
<div id="composition-api">
<h3 class="title is-3">Composition API Ver.</h3> <table class="table is-fullwidth">
<thead>
<tr>
<th class="cell-checkbox">
<b-checkbox :value="selectedFlag" :true-value="2" :false-value="0" :indeterminate="1 === selectedFlag"
@input="checkAll"/>
</th>
<th class="cell-name">名前</th>
</tr>
</thead> <tbody v-if="isLoading"><tr><td>読み込み中</td></tr></tbody>
<tbody v-else>
<tr v-for="item in items" :key="item.id">
<td class="cell-checkbox">
<b-checkbox v-model="item.selected"/>
</td>
<td class="cell-name" @click="onClick(item)">
{{ item.name }}
</td>
</tr>
</tbody>
</table>
</div>
</template><script lang="ts">
import {defineComponent} from '@vue/composition-api'
import ItemModule from '@/modules/item'
import TableModule from '@/modules/table' export default defineComponent({
setup(props, ctx) {
const itemModule = ItemModule(ctx)
const tableModule = TableModule(itemModule.items) /** Init **/
itemModule.getList() return {
...itemModule,
...tableModule,
}
}
})
</script>
template
部こそ一緒ですが、かなりすっきりしましたね。
ぱっと見て、テーブルに関するモジュールと、Itemに関するモジュールで分かれているのがわかります。ItemModule
はctx
という変数を引数にしていますが、これこそがVueにおけるコンテキストそのもので、こいつを引き回すことで、例えばemit
や$router
、$store
などに関する処理も外部に切り出すことが可能です。
◆Composition APIを利用するにあたって
まずこのComposition APIは、以下に該当する人には取っ付きづらく感じるかなと思います。
- サーバサイドの経験がない
- 小中規模のフロントしか経験がない(〜十数ページ程度の画面しかないSPAなど)
- VueやReact、AngularのView層(HTML、テンプレート、スタイル)しか触れる機会がない
これは、Composition APIが解決する問題が、いわゆるモデル層、ないしビジネスロジックに関連することが多いと推測されるからです。
そもそもフロントにとって、このレイヤーの処理のほとんどはサーバサイドに隠蔽され、JSON色付け係よろしく、ある程度の規模では無視しても問題ないものです。
しかしながら、プロダクト次第では、ビジネスロジックもある程度フロントに内包せざるを得ない場合があります。僕が所属する金融ドメインも、その一例かと思います。
そういったプロダクトの背景や、メンバーのスキルセットによって、Composition APIを採用するかを検討するべきかなと思います。
個人的に、以下のようなプロジェクトでは、積極的にComposition APIを採用しなくても良いと考えます
- デザインパターンに詳しい人や、(クリーン)アーキテクチャに強い人が、メンバーに居ない
- 高々10ページ程度しかない簡素なアプリである
- コンポーネント間での重複ロジックがあまりない
- 複雑なロジック持つメソッドを持たない、コンポーネントが持つプロパティが多くない
- Vueのシンタックスでしかコードを書いたことがない
- Vueのthisやコンテキストがなんなのか良くわかってない
もちろん、単なる技術的興味などでの採用も考えられますが、Vueのシンタックスは、あれはあれで非常に良いものです。
無理にメンバーの学習コストを上げて新機能を取り込む必要もないので、十分検討しましょう。
◆大規模Vue開発の問題点
以下からはTypeScriptでの開発を前提とします。
Vueの大規模開発では、度々以下が問題になると思います。
- this (VueComponent) のコンテキストを巻き込んだロジックを切り出しづらい
- 重複したロジックが、各種コンポーネントに存在する
- 重複したロジックを、タイプセーフで再利用しづらい
- コンポーネントのscript部が肥大化、可読性の低下
- data, computed, methodsを、ロジック毎にまとめられない
- template部で利用するプロパティのみにスコープを切れない(切りづらい)
Composition APIは、これらの問題を解決するのに比較的適しています。
特に、型推論を十分に使えるようになることは大きなメリットです。
Composition APIのメリット
これはもちろん、前述の課題を解決できることが大きなメリットですが、特には
- 型推論を十分に使えるようになる
- コンテキスト(this)を巻き込んだロジックを切り出しできる
- 再利用性が高まり、コードが圧縮できる
- (しっかりとした設計ができれば)可読性が高まる
- ViewModelとModelを分離できる→実装担当を分割できる
あたりかなと思います。
特に、thisのコンテキストがなくなり、関数的に利用できるようになったことで、Vueのコンテキストを利用したロジックもタイプセーフに切り出すことができるようになったのは、非常に便利だと感じました。
Composition APIのデメリット
- プログラミング力が試される
Composition APIは、Vueの暗黙的なコンテキストを取っ払った代わりに、そこを意識したプログラミングが必要になります。
つまり、今まで書いていた「Vueだから動く」コードではなく「ここまではロジックで、ここからはVueの世界」ということを、きちんと理解して書く必要があるため、少なからずプログラミング力が問われると思います。
余談ですが、Vueの素晴らしいところは、こういったプログラミング特有の煩わしさを隠蔽して、デザイナ含め、ある程度の初学者でも書けるようなところだと、個人的には思っています。
- より、設計が重要になる
ありがちな話ですが、ロジックが重複しているからといって、何でもかんでも共通化するわけにはいきません。
そのロジックは「たまたま」重複しているだけではないのか?
を常に意識しなければ、切り出した後に、それぞれの箇所で細かな仕様変更が発生した時に、当初予定外のIF文が発生したり、結局元のコンポーネントに戻したりという作業が発生する可能性があります。
サーバサイドでは非常に馴染みが深い話ですが、フロントではそこまでクリティカルではないと思われていると感じているので、ここはしっかり設計する必要があります(きちんと考えている強強エンジニアの方はごめんなさい><)。
◆ 実装詳細解説
さて、ここでは実装例で紹介したモジュールの中身を見ていきましょう。
/modules/table.ts
import {computed, Ref} from '@vue/composition-api'
import {Selectable} from '@/types'export default (itemsRef: Ref<Selectable[]>) => {
const selectedFlag = computed(() => {
if (itemsRef.value.length && itemsRef.value.every((item: Selectable) => item.selected)) {
return 2
} else if (itemsRef.value.some((item: Selectable) => item.selected)) {
return 1
}
return 0
})function checkAll(flag: number) {
itemsRef.value.forEach((operable: Selectable) => operable.selected = 2 === flag)
}return {
selectedFlag,
checkAll
}
}
tableModule
ではトップのチェックボックス操作で全体を操作とその状態管理を行っています。細かいことをやるなら、各種Item毎のチェックボックスクリック時の操作も書くかもしれませんが、まあそこまでは良いかなと思って入れてません。
実装には、Composition APIからcomputed
とRef
という型を利用しています。computed
が切り出しされていますが、これが通常のVueのシンタックスではできなかったことですね。
Flagが1
のときは、[-]
という表示になります。
/modules/item.ts
import {reactive, toRefs} from '@vue/composition-api'
import {SetupContext} from '@vue/composition-api/dist/component/component'
import {Item, Selectable} from '@/types'
import ItemApi from '@/apis/ItemApi'export default ({root}: SetupContext) => {
const state = reactive({
items: [] as (Item & Selectable)[],
isLoading: false
})
let isLast = false
let cursor: string | null = null// フェッチ
function getList() {
if (isLast || state.isLoading) returnstate.isLoading = true
return new ItemApi().getList({cursor: cursor}).then((res: any) => {
state.items.push(...res.data.items)
if ((cursor = res.data.nextCursor) == null) {
isLast = true
}
}).finally(() => state.isLoading = false)
}// リストアイテムクリック
async function onClick(item: Item) {
await root.$router.push(`/items/${item.id}`)
}return {
getList, onClick,
...toRefs(state)
}
}
itemModule
では、データのフェッチやItemをクリックした時の処理、ローディングや追加読み込み管理を任せています。
どれをどこまで切り出すかは設計に依ると思いますが、ここに各種プロパティ(テーブルカラム)固有のフィルタ検索なども書いたりしてます(本例では省略してます)。
余談ですが、reactive
とref
どちらを使うかで意見が様々なようですが、個人的には従来のdata
として、明確にテンプレート側にバインドしたいものは全てreactive
で宣言する、みたいにした方がわかりやすいかと思って、僕はreactive
を使っています。ref
はオブジェクトを取れないという点も注意ですね。
まあModuleのreturnでtoRefs
を使ってますし、実はcomputed
での宣言もReadOnly<Ref>
なので、Ref
自体は内部的に使ってるんですけどね。
所感
実際のコードと全く同じではないですが、このような雰囲気で書いてみて思うことは、ロジックとして管理すべきコードと、ViewModelとして管理すべきコードを、明確に意識するようになったことです。本例ではコードが短いので微妙かもですが、同種別のロジック関連の変数や関数をまとめて切り出せるのはかなり秀逸です。
後は、なんと無くcreated
に書いていた処理や、取り扱いに困っていた定数・変数の所在を明確にすることができる(ようなポテンシャルを感じる)ので、えいやで書こうとすることが減った気がします。
もちろん、きちんとしたロジックの分離ができると、テスタビリティも確実に向上します。特に単体テストでは威力を発揮するのではないでしょうか。
とはいえ、冒頭にも書いたように、Composition APIの恩恵を受けるためにはしっかりした設計が必要なので、本当に必要になったタイミングで十分に検討して、一部分から導入すると良いと思います。
既存のSyntaxやMixinなどと併用できるのも、Composition APIの良いところですね。
◆おわりに
Finatextグループでは、一緒に働くエンジニアや仲間を募集しています!!
従来の金融ビジネスに縛られない、新たな金融サービスを開発したい方は是非ご連絡ください!!お待ちしてます!!