面白駆動人生

やっほー

実践 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を使った状態管理については、これまでいくつか詳しい記事が出ています。

加えて、2020年2月に行われる、vuejs.amsterdamで、 vueコアチームの方が、「Vuexいらないかも?」というテーマで登壇されるようです。

<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サポートに向けて開発が進んでいるようです。

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を!