実践 Vue Composition API
はじめに
Vue Advent Calendar 2019 9日目の記事です。 担当は@yktm31です。
Composition APIとは、Vue3から導入予定の新しいAPIです。 海外Vueカンファレンスでは、今や必ずトピックに上がるほど注目されています。
本記事では、そんなComposition APIでモリモリ開発する際の実装方針やTipsについて書いていきます。
扱う内容は、以下の6つです。
1. ディレクトリ構成 / 設計方針
2. Route
3. 状態管理
4. ComposableなRepositoryFactory
5. ComposableなPolling
6. テスト
今回、上記の要素を含んだ簡易なサンプルを作成しましたので、宜しければ参考にしてください。 Github
<目次>
Main Contents
1. ディレクトリ構成 / 設計方針
まずは、ディレクトリ構成について。 基本はContainer/Presentationalをベースにするとスッキリまとめられそうです。 下記がディレクトリ構成例になります。configファイル等は省略しています。
src ├-- views ① ページに相当する ├-- presentationals ② 見た目/デザインに責務を持つ ボタンなど ├-- containers ③ ユーザのアクションに対して、ロジックを実行しレスポンスする責務を持つ ├-- repositories ④ REST APIなど、外部リソースへのアクセスに責務を持つ ├-- compositions ⑤ Composition Function ├-- store ├-- router ├-- App.vue ├-- main.ts ├-- shims-tsx.d.ts └-- shims-vue.d.ts
①〜③ コンポーネント分割方針
Composition APIで書く = コードが構造的になる ではありません。RFCでは、こう表現されています。
More Flexibility Requires More Discipline
Composition APIの恩恵で、より柔軟にロジックを組めるようになります。 しかし、だからこそちゃんと設計しないと、ロジックが散乱しカオスな状況に陥りかねません。
そこで、プロジェクトで通底する構造を作っていく必要があります。
本記事では、Container/Presentational/view の3つを基本としたコンポーネント分割をしていきます。
それぞれの責務は以下になります。
- container: ロジックを実行し、状態を操作する。
- presentational: containerを親に持ち、Propsで受け取ったデータに基づきHTMLを返す。
- view: containerを組み合わせてページを作る
ベースになっているのは、Container/Presentationalという、redeux開発者のDan Abramov氏が提唱した考えです。 こちらの記事が元記事です。
実はこの考え、2013年に出されたもので少し古いものです。 元記事の中でも、今ではHookがあるので、この考えである必要はないと言っています。 しかし、この考えに基づき、コンポーネントの責務を明確にすることは有用だと考え、採用しています。 全体を把握しやすく変更・拡張が容易な構造を作るため、 シンプルなルール・わかりやすい関係性を意識しています。
④ Repository
こちらは、RepositoryFactoryという考えに基づき、REST APIなど外部リソースへのアクセスを隠蔽します。 この考え方は、Vue evangelistのJorge氏が2018年にmediumで出した記事(日本語訳)で提唱されています。
そこで、この考えをベースに、Composition APIライクなRepositoryFactoryを実装します。 詳細は、この先で触れます。
⑤ Composition Function
コンポーネントから抽出したComposition Functionを置きます。 さてここで、「Composition Functionに切り出す出さないの判断基準は?」という疑問が湧いてくるかと思います。
自分としては
ベースとしているのは
- SFCの考えを踏襲する
- ロジックの関心ごとに分ける という二つの観点です。
本記事では、Polling処理をComposition Functionとして切り出す例を紹介します。 詳細は後述します。
2. Router
ここまで、考え方的な部分に触れてきました。 ここからは、実装よりの話になっていきます。
まずは、Routerについてです。早速実装を見ます。
<script lang="ts"> import { createComponent, SetupContext, ref, onMounted } from "@vue/composition-api"; import Button from "@/presentationals/Button.vue"; export default createComponent({ components: { Button }, setup(_, context: SetupContext) { // ページ遷移を行う。 function moveToNextPage() { context.root.$router.push({ name: "home" }); } return { moveToNextPage }; } }); </script>
Vue2で、Vueインスタンス内部からrouterを呼ぶ際には、 this.$routerを使っていました。
this.$router.push({ name: 'user', params: { userId: 123 }})
Vue3では、thisは廃止され、代わりにsetup()の第二引数、SetupContextからアクセスできます。
3. 状態管理
Composition APIを使った状態管理については、これまでいくつか詳しい記事が出ています。
- Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか?
- 【Composition API】StoreパターンでVuexを使わずに状態管理をする
- Vue Composition APIのコラムっぽいもの集#Vuexはいらなくなる?
加えて、2020年2月に行われる、vuejs.amsterdamで、 vueコアチームの方が、「Vuexいらないかも?」というテーマで登壇されるようです。
"You might not need VueX", according to #vue Core Team Member @N_Tepluhina🤩.
— vuejs.amsterdam (@vuejsamsterdam) December 13, 2019
Natalia is a Senior Frontend Engineer @gitlab and a🗜️@GoogleDevExpert.
See you in Feb 2020 @N_Tepluhina 😃 pic.twitter.com/5jpUOHqxDo
<2020/3/16追記> この発表資料はこちらです。 https://speakerdeck.com/ntepluhina/you-might-not-need-vuex
個人的には、いまいまグローバルに状態管理したいときはVuexを使っています。 理由としては、Composition APIを使った実装だと、DIしたりと実装が重くなると考えているからです。 Vue3/Composition APIでの状態管理、 Vuexが追従するのか、新しいライブラリが出てくるのか、要チェックなところです。
<2020/3/16追記> Vuex4にて、Vue3への追従が行われることが発表されました。 Vuex4 2020/3/16現在、TypeScriptのサポートは無いようですが、 READMDを見る限り、TypeScriptサポートに向けて開発が進んでいるようです。
Vuex 4.0 branch has opened, and 4.0.0-alpha.1 is released! 🌟 This is the Vue 3 compatible version of Vuex. It has the exact same API with Vuex 3, except for the installation process. Please provide us feedback if you find anything 🙌https://t.co/qt4g43uH93
— Kia King Ishii (@KiaKing85) March 15, 2020
4. ComposableなRepositoryFactory
上で書いた通り、RepositoryFactoryという考えに基づき、REST APIなど外部リソースへのアクセスを隠蔽します。 このRepositoryFactoryを、Composition APIを利用して、実装する例を紹介します。
Composition APIを使って実装するメリットとしては、コンポーネントのなかで、asyncを使ったRepository呼び出しが必要がない点です。 レスポンスやローディング中かどうかなど、状態も含めてComposition Functionの中に押し込められます。
これにより、よりシンプルにAPI呼び出しがかけるようになります。
以下、実装例を見ていきます。
バックエンドとして、flaskで簡単なAPIサーバを立てています。
import logging import time from datetime import datetime from flask import Flask, jsonify, request from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/api/sample/get', methods=['GET']) def get(): now = datetime.now().isoformat() return jsonify({'time': now}) @app.route('/api/sample/get2', methods=['GET']) def get2(): msg = request.args.get('msg') return jsonify({'msg': msg}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
まずはaxiosをラップしてRepositoryを作ります。
import axios, { AxiosInstance } from "axios"; const baseDomain = "http://localhost:5000"; const baseURL = `${baseDomain}/api`; let Repository: AxiosInstance = axios.create({ baseURL: baseURL }); export default Repository;
次に、ファクトリを作ります。
import useSampleRepository from "./SampleRepository" import { Ref } from '@vue/composition-api'; interface Repositories { [key: string]: Function; } const repositories = { sample: useSampleRepository } as Repositories; export const RepositoryFactory = { create: (name: string): any => { return repositories[name]; } };
こちらが、Composition APIを使ったRepositoryになります。
import Repository from "./Repository"; import { ref } from "@vue/composition-api"; const resource = "/sample"; export default function useSampleRepository() { let response1 = ref(); async function getSample() { const { data } = await Repository.get(`${resource}/get`); // レスポンスをrefで包んだ変数に格納。 response1.value = data; return data; } let response2 = ref(); async function getSample2(msg: string) { const { data } = await Repository.get(`${resource}/get2`, { params: { msg: msg } }); response2.value = data; return data; } return { getSample, getSample2, response1, response2 }; }
コンポーネントでの使用例はこちらです。
<script lang="ts"> import { createComponent, SetupContext, ref } from "@vue/composition-api"; import Button from "@/presentationals/Button.vue"; import TextField from "@/presentationals/TextField.vue"; import { RepositoryFactory } from "@/repositories/RepositoryFactory"; export default createComponent({ components: { Button, TextField }, setup() { // Composition FunctionとしてのAPI呼び出しメソッド、およびそれらのレスポンスを受け取る変数を準備する。 const { getSample, getSample2, response1, response2 } = RepositoryFactory.create("sample"); // templete内で参照・呼び出しできるように、returnする。 return { getSample, getSample2, response1, response2, value }; } }); </script>
5. Polling
Pollingについても、Composition APIを使うと、簡単に実装できます。
利点としては、コンポーネントでポーリング自体のロジックを組む必要がなくなることです。 また、ポーリングに関する状態もComposition Functionが持っているので、コンポーネントから制御することができます。
例えば、ページを離れる際にポーリングを終了したい時は以下のようにできます。
<script lang="ts"> // ・・・略・・・ const { polling, pollingDisable } = usePolling(); // コンポーネントマウント前にポーリングを開始する。 onBeforeMount(() => { polling(someFunction); }); // コンポーネント切り替え時に、ポーリングを停止させる。 onBeforeUnmount(() => { pollingDisable.value = true; }); // ・・・略・・・ </script>
実際の実装はこちらになります。
import { ref } from "@vue/composition-api"; // delayミリ秒待機する。任意の第二引数を結果として返す。 async function sleep(delay: number, result?: any) { return new Promise(resolve => { setTimeout(() => resolve(result), delay); }); } export default function usePolling() { // ポーリング制御用のフラグ let pollingDisable = ref<Boolean>(false); // ポーリングで実行する関数と、ポーリング間隔時間を引数として受け取る。 async function polling( fn: Function, intervalTimeMsec: number = 3000 ) { // 無限ループを回し、ポーリングする。 // pollingDisableの値がtrueになれば、ポーリングを終了する。 for (;;) { await sleep(intervalTimeMsec).then(status => { fn(); }); if (pollingDisable.value) { break; } } } return { polling, pollingDisable }; }
6. テスト
テストについては、以下の2つの記事が参考になります。
とはいえ、丸投げにするわけには行かないので、上で取り上げたRepositoryFactoryのユニットテストを書いていきます。
import { createLocalVue, mount, shallowMount } from "@vue/test-utils"; import VueCompositionApi from "@vue/composition-api"; import useSampleRepository from "@/repositories/SampleRepository"; import Repository from "@/repositories/Repository" // composition APIを有効にする。 const localVue = createLocalVue(); localVue.use(VueCompositionApi); // Repositoryをモック化する。 const mockedRepository = Repository as jest.Mocked<typeof Repository> jest.mock('../../../src/repositories/Repository'); beforeEach(() => { mockedRepository.get.mockReset(); }) describe("SampleRepository", () => { it("getSampleTest", () => { mockedRepository.get.mockResolvedValue({data: "test"}); const { getSample, response1 } = useSampleRepository(); return getSample().then((res: string) => { expect(res).toEqual("test"); expect(response1.value).toEqual("test"); }); }); it("getSampleTest2", () => { mockedRepository.get.mockResolvedValueOnce({data: "no"}); const { getSample2, response2 } = useSampleRepository(); return getSample2("-").then((res: string) => { expect(res).toEqual("no"); expect(response2.value).toEqual("no"); }); }); });
ポイントとしては、
- Repositoryをモック化する。
- レスポンスを受け取るrefで包んんだ変数の値をチェックする の2点です。
最後に
最後まで読んでいただきありがとうございます。 長くなりましたが、これまでComposition APIを使い、試行錯誤して考えてきたことを紹介させていただきました。
尚、私の個人的な経験ベースでの話が多くなっています。 もし、もっといいやり方がある・それは良くない、という点があればコメントいただければ嬉しいです。
また、開発規模の大小によって適切な構成があると思いますので、ひとつの参考になればと思います。 一応、本記事では、小〜中規模くらい(ページ数が3〜7程度)を想定しています。
Composition APIはまだ成熟しきっていないと思います。 しかし、Vueにとって新しいパラダイムとなるのは確実だと思っています。
何より、Compoosition APIめちゃくちゃ楽しいのです! どんどん普及して、色々なプラクティスが出てくるといいなと思い、本記事を執筆しました。
それでは、楽しいVue Lifeを!