SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Vue.js 3.0で搭載される Composition APIをリリースに先駆けて試してみた

f:id:mkt0225:20191219220810p:plain

スマートキャンプでBiscuetのエンジニアをしている中川です。

本記事はスマートキャンプ Advent Calendar 2019 - Qiitaの19日目の記事です。

現在弊社のプロダクトであるBOXILとBiscuetは、そのどちらの開発チームもVue.jsを使用して開発しています。

Vue.jsの学習コストの低さやコンポーネント指向は少人数のチームでユーザーに素早く価値を届けていきたい弊社の開発においても重宝しています。

さてそんなVue.jsですが、2020年のQ1にバージョン3.0が正式リリースされることが予告されており、正式リリースに先駆けて目玉機能のひとつであるComposition APIが公開されました。

Composition API RFC | Vue Composition API

ドキュメントやAPIリファレンスのみならず、バージョン2系でプラグインとして利用できるコードも公開されていたので、本記事では実際に使ってみた上で従来のSFCのコードと比較することでComposition APIの特徴を掴んでいこうと思います!

Vue.js 3.0について

Vue.jsの次バージョンである3.0のリリース時期は前述の通り2020年のQ1を予定しており、その開発自体は今年のQ1からスタートしています。

アップデート内容は作者であるEvan You氏のこちらの記事(Plans for the Next Iteration of Vue.js - The Vue Point - Medium)にまとまっています。

本記事ですべてを紹介することはしませんが、個人的には以下の機能や変更が気になっています。

  • Composition API(本記事で触れる機能です)
  • TypeScriptサポートの強化
  • Reactivity APIのパブリック化
  • デバッグ能力の向上
  • ソースコードの軽量化
  • 処理の高速化
  • slotsメカニズムの向上
  • IE11サポート

盛りだくさんですね。 Vue.js自体のロードマップは以下にカンバン形式で公開されているため、バージョン3.0に限らず今後Vue.jsがどういった方向に進んでいくのかの概観を掴むことが出来るようになっています。

Roadmap · GitHub

また、バージョン3.0自体はvue-nextとしてリポジトリ管理されています。

github.com

Composotion APIについて

これまでVue.jsは小〜中規模のアプリケーションにとって最適といわれてきましたが、月日の経過とともにアプリケーションは大きくなり、それに伴い不便を感じることが増えてきました。

なかでも、「複数の開発者が1つのSFCを長期間メンテや機能追加することによりリーダビリティが低下しコードジャンプを重ねないとどこに何があるのか分からない」や、「複数のSFCで繰り返し使っているが共通化されていない関数がある」といった事態はもはやあるあるとして語られるようになってきつつあるのかなと思います。

その問題を解決する切り口として提案されているのがComposition APIで、ドキュメントのMotivationの項目にもLogic Reuse & Code Organizationとして掲げられています。

Composition API RFC | Vue Composition API

また、同ドキュメント内の以下イメージが分かりやすかったです。 左が従来のScriptタグ内で1つのObjectをexportする形、右が同一の内容をComposition APIで書き直した形で、各色はそれぞれに関連するコードを表しています。

f:id:mkt0225:20191219185817p:plain
Composition API RFC(https://vue-composition-api-rfc.netlify.com/)より抜粋

従来の書き方では散らばっていた関連するコードがComposition APIではまとまっていることが見て取れますね!

導入

導入方法は以下の手順の通りです。

  1. vue-cliでvue createした直後に以下コマンドでComposition APIプラグインをインストールします。
$ npm install @vue/composition-api --save
  1. インストールしたプラグインをVue.useします。
import Vue from 'vue'
import App from './App.vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

以上で導入は終了です!

yarnやCDNでのインストールにも対応しているようなので、詳しい導入方法は以下のリポジトリのREADMEを参考にしてください。 github.com

試してみる

さて、それでは実際にCompositionAPIを使ってみましょう!

サンプルとしてボタンをクリックすることでカウントアップする簡単なコンポーネントを作ってみます。

setup, reactive

まずはcountという変数名のdataを初期値0でセットしてtemplateタグ内で表示してみます。

これを従来のOptions APIで書いてみるとこのようになります。

Options API

<template>
  <div>
    <p>count:{{ count }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { 
      count: 0
    }
  }
}
</script>

なんの変哲もない見慣れた形ですね。 特に説明することはなさそうです!

次にこの内容をComposition APIを使って書いてみます。

Composition API

<template>
  <div>
    <p>count:{{ state.count }}</p>
  </div>
</template>

<script>
import { reactive } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({
      count: 0
    })

    return { state }
  }
}
</script>

いきなり雰囲気がガラッと変わりましたね。

まずはsetup関数なるものが登場しました。

これは従来のdataや、computedmethodsなどの種類に関わらず、templateタグ内で使用したりリアクティブな値として保持しておきたいものを定義し、returnするObjectに含めます。

次にreactive関数ですが、この関数に引数としてObjectを渡すことでそれぞれの値がリアクティブ化されます。 従来のdataの役割に近そうです。

また、templateで参照するにあたってcountからstate.countstateを挟むようになっています。

これは後述するrefという関数で別の書き方が出来るので、その場合は従来通りのcountとして参照することができます。

function

次にボタンをクリックするごとにcountの値を1ずつ増加させる処理を実装します。

Options API

<template>
  <div>
    <p>count:{{ count }}</p>
    <button @click="increment">Count Up</button>
  </div>
</template>

<script>
export default {
  data() {
    return { 
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

Options APIではdata関数と並んでmethodsオブジェクトが登場しました。

Composition APIでは以下の形になります。

Composition API

<template>
  <div>
    <p>count:{{ state.count }}</p>
    <button @click="increment">Count Up</button>
  </div>
</template>

<script>
import { reactive } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({
      count: 0
    })

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    }
  }
}
</script>

methodsのような関数をまとめるオブジェクトはなくなり、前述のsetup関数のなかでfunctionとして宣言してそれをreturn文に含めるという形に変化しています。

この書き方であればまとめておきたい一連の変数や関数の宣言を並べることが出来るため可読性が向上する効果が期待できます。

また、この形であればimportした関数をreturn文にそのまま含めることも出来ますね。

ref

refはrefferenceの略で、その名の通り引数として渡した値の参照をreturnする関数です。

<template>
  <div>
    <p>count:{{ count }}</p>
    <button @click="increment">Count Up</button>
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'

export default {
  setup() {
    let count = ref(0)

    function increment() {
      count.value++
    }

    return {
      count,
      increment
    }
  }
}
</script>

この書き方には以下の特徴があります。

  1. プリミティブな値であってもstateのようにオブジェクトにまとめる必要がなくなり、ソースコード上の利用する関数等と近しい位置に書くことができるようになる
  2. templateタグ内で従来通りcountとして参照することが出来る
  3. 値を更新する場合はcount.valueなど、.valueをつける必要がある

特に1.の理由で使用することが多そうな気配がします!

computed, watch

次にcomputedとwatchを使ってみます。 countの値を2倍にするcomputedであるdoublecountをwatchして3の倍数のときにalertを出すwatcherを実装します。

Options API

<template>
  <div>
    <p>count:{{ count }}</p>
    <button @click="increment">Count Up</button>
    <p>double count:{{ double }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { 
      count: 0
    }
  },
  computed: {
    double() {
      return this.count * 2
    }
  },
  watch: {
    count(v) {
      if(v % 3 === 0) alert('Multiple of 3')
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

Options APIではdata, methodsに加えてcomputedとwatchが登場しました。

対してComposition APIでは以下の形になります。

Composition API

<template>
  <div>
    <p>count:{{ state.count }}</p>
    <button @click="increment">Count Up</button>
    <p>double count:{{ state.double }}</p>
  </div>
</template>

<script>
import { reactive, computed, watch } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    watch(() => {
      if(state.count % 3 === 0) alert('Multiple of 3')
    })

    return {
      state,
      increment
    }
  }
}
</script>

computed、watchどちらもsetupの中に収まる形になっていますね。

もちろん関数の宣言順の決まりはなく、見通しのよいと判断した位置に記述できます。

lifecycle hooks

最後にlifecycle hooksについて見てみましょう!

mountedのタイミングでconsoleを出してみます。

Options API

<template>
  <div>
    <p>count:{{ count }}</p>
    <button @click="increment">Count Up</button>
    <p>double count:{{ double }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { 
      count: 0
    }
  },
  computed: {
    double() {
      return this.count * 2
    }
  },
  watch: {
    count(v) {
      if(v % 3 === 0) alert('Multiple of 3')
    }
  },
  mounted() {
    console.log('mounted!')
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

対してComposition APIでは以下の形になります。

Composition API

<template>
  <div>
    <p>count:{{ state.count }}</p>
    <button @click="increment">Count Up</button>
    <p>double count:{{ state.double }}</p>
  </div>
</template>

<script>
import { reactive, computed, watch, onMounted } from '@vue/composition-api'

export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    function increment() {
      state.count++
    }

    watch(() => {
      if(state.count % 3 === 0) alert('Multiple of 3')
    })
    
    onMounted(() => {
      console.log('mounted!')
    })

    return {
      state,
      increment
    }
  }
}
</script>

onMountedなどonXXXの形で各lifecycleの関数が提供されているので使うものを都度importしてsetup内に記述する形になりました。

また、従来のcreatedbeforeCreatedについては対応するonXXXはなく、setupに置き換わっているので注意が必要そうです。

その他のonXXXについてなど詳細は以下のAPIリファレンスをご覧ください。

API Reference | Vue Composition API

まとめ

駆け足でComposition APIにおける主要なシンタックスを見てみました。

雑感としては以下です。

Pros

  • 従来のdatacomputed等に散らばっていた関連するロジックをsetup関数のなかで自由に宣言出来るのは思った以上に自由度が高くなる
    • 関連するロジックをまとめるなど、リファクタリングとしてもComposition APIに移行する価値はありそう
    • あるコンポーネントにひとつの機能を追加するときなど、「これをdataに定義してこれをcomputedに定義して〜」といったコードジャンプがなくなるのは可読性の面でもよさそうだし実装スピードも上がりそう
  • setup関数のreturn文を見るだけで何がtemplateタグから使用できるのか一目瞭然
  • 今回は紹介できなかったがTSXサポートもあるので3.0のTypeScriptサポート強化と併せてTypeScriptがさらに負担少なく導入出来るようになりそう

Cons

  • 従来のシンタックスから大きく離れることになるので既存プロジェクトからの移行はある程度の手間を覚悟しないといけなそう
    • Composition API自体あくまでオプショナルなので3.0以降でも従来通りのOptions APIで書くことはもちろん可能
  • setup関数内の自由度があまりに高いので一定のルールが無いとカオスになりそう
    • これに関してはeslint-plugin-vueのように全プロジェクトで統一されたルールを適用することは難しいのでプロジェクト内であらかじめルール決めが必要そう

既存プロジェクトでいますぐ導入することはなかなか難しそうですが強力なシンタックスである片鱗は存分に感じたので、新規プロジェクトや個人開発などでガンガン使っていきたいと思います!

Vue.js 3.0、たのしみですね!