はじめに
Vueの現在のバージョンは2.6.10ですが、Vue3.x系から新たなAPIの提供がなされるとのことで一足先に試してみました。
使って見た感じ結構良さそうだったので備忘録的に残しておきたいと思います。
ここで作成したコードは以下のリポジトリにアップしてありますので一緒に確認もできます。
https://github.com/tomopict/vue-composition-api-test
前提とする環境
- VueCLI v4.0.5
- node v10.16.0
- yarn v1.13.0
この記事の目的
- CompositionAPIとVue2.x系の書き方の比較
- ロジックの分離についてVue.js 2.x系との比較
CompositionAPIについて
公式では以下のように言及されています。
a set of additive, function-based APIs that allow flexible composition of component logic.
コンポーネントロジックの柔軟な合成を可能にする、追加の機能ベースのAPIのセット。
公式ドキュメント
https://vue-composition-api-rfc.netlify.com/
公式github
https://github.com/vuejs/composition-api
Vue3系から導入予定との事ですが、@vue/composition-api
をimport
することで、Vue2.x系でも使用することができます。
CompositionAPI導入の目的
CompositionAPIを導入する大きな目的として2つ挙げられています。
以下に公式のモチベーションの項にあるテキストを引用します。
1.コード(主にコンポーネント)解読の難しさの改善
The code of complex components become harder to reason about as features grow over time. This happens particularly when developers are reading code they did not write themselves. The root cause is that Vue's existing API forces code organization by options, but in some cases it makes more sense to organize code by logical concerns.
複雑なコンポーネントのコードは、時間の経過とともに機能が大きくなるにつれて推論するのが難しくなります。これは特に、開発者が自分で書いていないコードを読んでいるときに起こります。根本的な原因は、Vueの既存のAPIがオプションによるコード編成を強制することですが、場合によっては、論理的な懸念によりコードを編成する方が理にかなっています。
2.ロジック再利用の方法の改善
Lack of a clean and cost-free mechanism for extracting and reusing logic between multiple components.
複数のコンポーネント間でロジックを抽出して再利用するための、クリーンで費用のかからないメカニズムの欠如。
コード(主にコンポーネント)解読の難しさの改善
これはコンポーネントそのものの解読が難しいので改善したいというよりは、上述のテキストにもある
時間の経過とともにロジックが大きくなるにつれて推論するのが難しくなります
というのが今回導入するにあたっての大きな理由かと思います。
Vue3系以前でコンポーネントを作る場合
- data
- lifecycle
- methods
- etc...
などをそれぞれ別の場所で定義しなくてはなりません。
そのため一つのコンポーネントの中に多くのロジックがあった場合、俯瞰してみるとロジックが各プロパティのなかに分散していて、一瞥することが難しくなっているのが実情です。
またある程度の年数サービスを運用していくと、設計によってはコンポーネントが肥大化していきます。
自分が扱ったコンポーネントでも単一のコンポーネントで3,000行近いようなものもありました。
さらに複数の開発者によって開発がされていくとロジックに付随するデータなどがコンポーネントの方々に散ることもままあります。
一人で開発していたとしても過去の自分は信用ならないので、一人PJであっても見返した時に理解できない状態になってしまうこともよくあります。
CompositionAPIを使用してロジックをまとめることで、全体の見通しが良くなると思われます。
ロジック再利用の方法の改善
今まで複数コンポーネントで同じロジックを使用する際の方法はいくつかありましたが、どれも一長一短でした。
それぞれのメリットデメリットに関しては公式のビデオの3:30〜あたりからをご覧いただければと思います。
こちらについては本記事では複数コンポーネントにわたるようなロジックの受け渡しは試していないので、試した上で別途記事として書きたいと思います。
型推論の改善
また上記2点以外に挙げられている点として型推論の改善もあります。
今Vue2.x系でtypescriptを書く場合、デコレータを使用した書き方をすることが多い印象です。
CompositionAPIを使用することでデコレータに依存しない開発ができることになります。
実際に比較してみる
今回は既存のVue2.x系での組み方とcompositionAPIを使用した場合で同じロジックを組んでみて
- ロジックをどのようにまとめられるようになるか
- その結果見通しがどのように変わるのか
を実際に書いて確認して見たいと思います。
今回は主に以下の2つの機能を実装しています。
- Bitcoinのデータを取得して非同期に表示
- ボタンをクリックすると数値がインクリメントし、かつ2倍にした数を表示
またライフサイクルおよび値の受け渡しなどで以下の機能を確認しています。
- ライフサイクル
- computed
- mounted
- propsの受け渡し
- methodsの書き方
ライフサイクルについてはVue2.xから大きく変更があります。
created
とbeforeCreate
を除いてsetup()
関数の中で定義をします。
以下にVue2.x系とCompositionAPIにおけるライフサイクルの対照表を記載します。
Vue 2.x | CompositionAPI |
---|---|
created | set() |
beforeCreate | set() |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
インストール
インストールと各種記載の仕方に関してはこちらの記事を参考にさせていただきました。
https://qiita.com/ryo2132/items/f055679e9974dbc3f977
vue-cli 4.0.5
のバージョンでは一番初めの選択肢が以下のようになっていてtypescript
の項目があります。
これを選択するとvue-class-component
とvue-property-decorator
がデフォルトでインストールされます。
両方使う場合はこちらでも構いませんが、今回はManually select feature
で個別にインストールする項目を設定しました。
Vue CLI v4.0.5
? Please pick a preset: (Use arrow keys)
❯ typescript (router, vuex, sass, babel, typescript)
default (babel, eslint)
Manually select feature
Vue2.xでの書き方
まずは旧来のVue.2.xの書き方です。
型推論の改善についても同時に試してみたかったので、今回は以下の条件で書いています。
- typescript
- vue-class-componentの利用
- vue-property-decoratorの利用
<template>
<div>
<button @click="increment">Count is: {{ this.count }}, double is: {{ this.double }}</button>
<h2>{{ propHello }}</h2>
<h3>{{ reactiveMessage }}</h3>
<p>{{ info }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import axios from "axios";
import { AxiosPromise } from "axios";
@Component
export default class HelloWorld extends Vue {
// props
@Prop() private propHello!: string;
// data
reactiveMessage: string = "Hello";
info: any = {};
count: number = 0;
// computed
get double() {
return this.count * 2;
}
// mounted
mounted() {
this.getDate();
}
// methods
getDate() {
axios
.get("https://api.coindesk.com/v1/bpi/currentprice.json")
.then(response => (this.info = response));
}
increment() {
this.count++;
}
}
</script>
CompositionAPIを使用した書き方
次にCompositionAPIを利用した書き方です。
<template>
<div>
<button
@click="CountDouble.increment"
>Count is: {{ CountDouble.state.count }}, double is: {{ CountDouble.state.double }}</button>
<h2>{{ propsHello }}</h2>
<h3>{{ state.reactiveMessage }}</h3>
<p>{{ BitCoinData.info }}</p>
</div>
</template>
<script lang="ts">
import {
createComponent,
reactive,
onMounted,
computed,
ref
} from "@vue/composition-api";
import axios from "axios";
import { AxiosPromise } from "axios";
/**
* bitcoinのデータを取得して非同期に表示
*/
const getDataFromBitcoin = () => {
let info: any = ref(); // OK
// let info: any = {}; // NG
// refがないとreactiveにならない為、dataの中身が変わっても検知されない
// methods
const getData = () => {
axios
.get("https://api.coindesk.com/v1/bpi/currentprice.json")
.then(response => (info.value = response));
// refで定義された変数にはvalue.propertyでアクセスが可能
// refのvalueにobjectが定義された場合、深い階層までリアクティブになる
};
// mounted
onMounted(() => {
getData();
});
// 使用するデータは全てreturn
return {
info,
getData
};
};
/**
* ボタンをクリックすると数値がインクリメントし、かつ2倍にした数を表示
*/
const countDouble = () => {
// data
const state: any = reactive({
count: 0,
// computed
double: computed(() => state.count * 2)
});
// methods
const increment = () => {
state.count++;
};
// 使用するデータは全てreturn
return {
state,
increment
};
};
type Props = {
propHello: string;
};
export default createComponent({
props: {
propHello: {
type: String
}
},
setup(props: Props) {
// props
const propsHello = props.propHello;
// 各機能を定義
const BitCoinData = getDataFromBitcoin();
const CountDouble = countDouble();
// data
const state = reactive<{
reactiveMessage: string;
}>({
reactiveMessage: "Hello"
});
// 使用するデータは全てreturn
return {
BitCoinData,
CountDouble,
state,
propsHello
};
}
});
</script>
CompositionAPIとVue2.x系の書き方の比較
dataの持たせ方
CompositionAPIでは今までのdata()
にあたりリアクティブなプロパティについてはsetup()
の関数の中で定義します。
// data
reactiveMessage: string = "Hello";
info: any = {};
count: number = 0;
// data
const state = reactive<{
reactiveMessage: string;
}>({
reactiveMessage: "Hello"
});
なおreactiveMessage
プロパティ以外はfunctionで定義した各機能の中にそれぞれ持たせています。
またリアクティブなプロパティを定義するのにref
とreactive
の使い分けがきになるところではありますが、参考にさせていただいた記事にもありましたが、公式ではrefとreactiveについてはどちらも理解した上で使い分けるのが良い
と明記されています。
少し使用して見た感じではプロパティがもつkeyが単一の場合はref
を、複数のkeyをもつobejctとして定義する場合はreactive
を使っている印象です。
これらについてはまだ実装しながら確認している最中なので、また別途まとめたいと思います。
ライフサイクル
ライフサイクルについては前述しましたが、created
とbeforeCreate
についてはsetup()
関数そのものが、mounted
などのそのほかのライフサイクルついてはそれぞれsetup()
関数内で定義する形になっています。
// mounted
mounted() {
this.getDate();
}
// mounted(set()関数内で定義)
onMounted(() => {
getData();
});
computed
computedに関してはdata
を持たせる箇所で通常のstateと一緒に定義しています。
これまでの書き方とは結構異なるので、見慣れるまで少し時間がかかりそうです。
// computed
get double() {
return this.count * 2;
}
const state: any = reactive({
count: 0,
// computed
double: computed(() => state.count * 2)
});
ロジックの分離についてVue.js 2.x系との比較
今回自分が一番気になっていた部分としてどのようにロジック(機能)を分離するのか、できるのかというのがありました。
Vue2.x系では同一コンポーネント内にロジックが複数あった場合でも、data
はdata
の箇所にmethod
はmethod
に他の機能と一緒に格納されている形でした。
そのためロジックが増えれば増えるほどロジックに紐づくプロパティが方々に散ってしまって、一つのロジックを理解するためだけにスクロールを繰り返して確認することもありました。
それらを改善するという目的においてどのくらい改善されたのかを確認するために、前述のコードをロジックで使用しているプロパティ毎に色分けして比較してみます。
- Bitcoinのデータを取得して非同期に表示 → 赤
- ボタンをクリックすると数値がインクリメントし、かつ2倍にした数を表示 → 青
これをみると一目瞭然で機能ごとに分けられていることがわかります。
それぞれの機能の中でdata
やmethod
を定義し、ライフサイクルごとの処理を設定します。
その際にtempalate
で利用するプロパティについては全てreturn
でオブジェクトとして返却するようにします。
/**
* bitcoinのデータを取得して非同期に表示
*/
const getDataFromBitcoin = () => {
let info: any = ref(); // OK
// let info: any = {}; // NG
// refがないとreactiveにならない為、dataの中身が変わっても検知されない
// methods
const getData = () => {
axios
.get("https://api.coindesk.com/v1/bpi/currentprice.json")
.then(response => (info.value = response));
// refで定義された変数にはvalue.propertyでアクセスが可能
// refのvalueにobjectが定義された場合、深い階層までリアクティブになる
};
// mounted
onMounted(() => {
getData();
});
// 使用するデータは全てreturn
return {
info,
getData
};
};
そしてオブジェクトとして返却されたそれら機能をsetup()
関数の中で呼び出します。
// 各機能を定義
const BitCoinData = getDataFromBitcoin();
const CountDouble = countDouble();
こうすることで、各機能毎に使用するプロパティをまとめることができるようになります。
またより大きなコンポーネントをCompositionAPIで書き直した例として、VueCLIで使用されているFolderExplorer
を書き直したgithubのリポジトリがあります。
※参考 元のFolderExplorerのgithubリポジトリ
https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-ui/src/components/folder/FolderExplorer.vue#L198-L404
ちょっとしたハマりどころ
個人的にはまったところがあったので書き残しておきます。
ロジック内のreactiveなdataの扱い
各ロジック内で定義するdataについて、reactiveにしたいものについてはref
で定義しないと、非同期にデータを入れた時にコンポーネント内に反映されませんでした。
let info: any = ref(); // OK
let info: any = {}; // NG
最後に
お読みいただきありがとうございます。
最後に自身が感じたメリット、デメリットを記載して終わりにしたいと思います。
- メリット
- 機能毎にロジックの分割ができ、ロジックの見通しが良くなる
- デコレータを使用しなくても型推論が通る
- デメリット
- returnで都度オブジェクトを返すのが冗長に感じる
- 上記理由もあり、コードが長くなる(書き方次第?)
まだドキュメントが少ないこともありプロダションで使用するには早計な気もしますが、ロジックを分離して俯瞰できることは長く運用していくほどメリットになりうると感じました。
3系に移る前にある程度把握してスムーズに移行するように努めていきたいと思います。