Vue.js + Vuex + VuetifyとFirebaseでSPAを作る(その3)

2019-11-23TIPS,Vue.jsFirebase,Vuetify,Vuex

Vue, Vuex, VueRouter, Vuetifyおよび Firebase を使用してハリネズミ紹介 Web サイトを作成していきます。

上のヘッダー画像は作成するウェブサイトの完成図です。

各工程は以下の4部構成に分かれています。

これまでVueを使用したことがない初心者の方でも困らないよう詳しめに解説していきます。

前回行った内容

前回の投稿ではVue Routerを使用してアプリのページ間ナビゲーションを追加し、コンポーネントを追加しました。
今回はまずメニューページを作り込んでいきVuexでの状態監視を組み込んでいきます。


APIの用意

今回作成しているアプリからアクセスするAPIを用意します、
以前Django REST Frameworkを使用して作成したAPIがあるのでこちらを使用していきます。


メニューページの作成

メニューページでは3種類のパイルカラーごとにハリネズミの紹介を表示します。
まず最初にユーザーが好みのパイルカラーを選択できるようにHomePlansコンポーネントに変更を加えましょう。

ユーザーがカラーを選択できるように各パイルカラーのカードにボタンを追加します。
このボタンをWebサイトの訪問者がクリックすると該当のカラーが紐づけられたハリネズミ達がカードで表示されます。
ただし、コンポーネントがホームページに表示されているときにこれらのボタンが表示されるのは望ましくないため表示されているページによってボタンの表示/非表示を切り替えてみましょう。

まずHomePlansのv-card-textセクションの下に v-card-actions セクションを追加します。
このセクションにはユーザーがパイルカラーを選択するためのボタンを含ませます。
v-cardコンポーネントに以下のコードを追加します。

<v-card-actions v-if="['menu'].includes($route.name)">
  <v-btn
    outlined
    block
    color="green"
    @click="showHedgehogs(1)"
    data-cy="plansSPBtn"
  >Select This Color</v-btn>
</v-card-actions>

追加したボタンには outlinedblock のpropをセットしています。
このボタンをクリックすると showHedgehogs メソッドを呼び出し、選択肢たカラーのidがパラメータで渡されます。

同様に他の2つのパイルカラーについてもボタンを設置します、最終的なコードは以下の通りです。

HomePlans.vue

<template>
  <v-container grid-list-lg>
    <v-layout row>
      <v-flex
        xs12
        class="text-xs-center display-1 font-weight-black my-5"
      >THE DOMINANT WHITE-BELLIED HEDGEHOG COLORS</v-flex>
    </v-layout>
    <v-layout row wrap class="meal-plans">
      <v-flex xs12 sm12 md4>
        <v-card>
          <v-responsive>
            <v-img
              src="https://nmomos.com/wp-content/uploads/2019/08/IMG_3978-768x768.jpg"
              height="500px"
            >
              <v-container fill-height fluid>
                <v-layout fill-height>
                  <v-flex xs12 align-end flexbox>
                    <span class="headline white--text">SALT & PEPPER</span>
                  </v-flex>
                </v-layout>
              </v-container>
            </v-img>
          </v-responsive>
          <v-card-text>
            <div>
              <h3 class="headline mb-0">SALT & PEPPER</h3>
              <div>
                The spines are white, banded with black.
                No more than 5% of the quills are to be solid white.
                The face is white with a black mask, ears and nose.
                The underbody hair is white
                Black mottling of the underbelly is extensive.
                Skin on the shoulders is jet-black.
                The nose is black.
              </div>
            </div>
          </v-card-text>
          <v-card-actions v-if="['menu'].includes($route.name)">
            <v-btn
              outlined
              block
              color="green"
              @click="showHedgehogs(1)"
              data-cy="plansSPBtn"
            >Select This Color</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
      <v-flex xs12 sm12 md4>
        <v-card>
          <v-responsive>
            <v-img
              src="https://nmomos.com/wp-content/uploads/2019/08/IMG_0825-1-768x768.jpg"
              height="500px"
            >
              <v-container fill-height fluid>
                <v-layout fill-height>
                  <v-flex xs12 align-end flexbox>
                    <span class="headline white--text">CINNAMON</span>
                  </v-flex>
                </v-layout>
              </v-container>
            </v-img>
          </v-responsive>
          <v-card-text>
            <div>
              <h3 class="headline mb-0">CINNAMON</h3>
              <div>
                Spines are white, banded by light cinnamon brown.
                No more than 5% of the spines are to be solid white.
                The face is not masked.
                The underbelly is white and mottling of the skin is not preferred.
                Skin on the shoulders is pink.
                The nose is liver.
              </div>
            </div>
          </v-card-text>
          <v-card-actions v-if="['menu'].includes($route.name)">
            <v-btn
              outlined
              block
              color="green"
              @click="showHedgehogs(2)"
              data-cy="plansCINNAMONBtn"
            >Select This Color</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
      <v-flex xs12 sm12 md4>
        <v-card>
          <v-responsive>
            <v-img
              src="https://nmomos.com/wp-content/uploads/2019/08/IMG_4001-768x768.jpg"
              height="500px"
            >
              <v-container fill-height fluid>
                <v-layout fill-height>
                  <v-flex xs12 align-end flexbox>
                    <span class="headline white--text">APRICOT</span>
                  </v-flex>
                </v-layout>
              </v-container>
            </v-img>
          </v-responsive>
          <v-card-text>
            <div>
              <h3 class="headline mb-0">APRICOT</h3>
              <div>
                Spines are white, banded by pale orange-beige.
                The underbody is white.
                The face is not masked.
                The eyes are ruby-red.
                The nose is pink.
                <br />
                <br />
                <br />
              </div>
            </div>
          </v-card-text>
          <v-card-actions v-if="['menu'].includes($route.name)">
            <v-btn
              outlined
              block
              color="green"
              @click="showHedgehogs(4)"
              data-cy="plansAPRICOTBtn"
            >Select This Color</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

これで以下のようなボタンが生成されました。


ハリネズミのリストを取得する

ユーザーが 「SELECT THIS COLOR」ボタンをクリックすると呼び出されるメソッドを実装していきます。
このメソッドは外部のAPIからリストを取得します、今回の例では上述した以前作成したDjangoプロジェクトを使用します。

まずターミナルから次のコマンドを実行しaxiosをインストールする必要があります。

npm install axios

続いてインストールしたaxiosを使用するためにHomePlansコンポーネントの<script>タグの中でimportしましょう。

import axios from 'axios';

次にexport default セクションにshowHedgehogsメソッドを追加します。
このメソッドはcolorというパラメータを取ります。

axiosはAPIの呼び出しに成功した時と失敗した時の処理をそれぞれ用意することができます。
.then に続けて成功時のロジックを、 .catch に続けて失敗時のロジックを記述します。

呼び出しに成功した場合はDjangoから返される配列を、hedgehogs配列にセットします。
失敗した場合は空の配列をセットしてみましょう。

export default {
    name: 'HomePlans',
    data() {
        return {
            hedgehogs: []
        };
    },
    methods: {
        showHedgehogs(color) {
            axios
                .get(`${state.apiUrl}hedgehogs/?color=`+ color);
                .then(response => {
                    response = response.data;
                    this.hedgehogs = response;
                })
                .catch(() => {
                    this.hedgehogs = [];
                });
        }
    }
};

注:APIからデータを取得するためにコンポーネント内で直接axiosを使用する方法を示しましたが、次の工程でVuexを使用する方法に切り替えます。
この工程で作成した上記のコードは最終的に使用しませんが、処理の流れをわかりやすくするための参考として記述しました。


Vuexを使用する

前項ではaxiosを使用しパイルカラーごとのハリネズミのリストを取得し可愛い画像と名前、短い説明を表示してみました。

しかしこの機能をコンポーネントに全て実装していくのは好ましくありません。
コンポーネント上のコードはあくまでもViewの部分に留め、ロジックは外部に切り出したいです。
そこでVuexを使いボタンをクリックしたときにコンポーネント外でAPIを叩き値を受け取る仕組みを実装していきましょう。

まず最初に公式によるVuexの説明をみてみます。

VuexはVue.jsアプリケーション用の状態管理ライブラリです。
これは、アプリケーション内のすべてのコンポーネントの集中型のストアとして機能し予測可能な方法でのみ変更されることを保証するルールを備えています。

https://vuex.vuejs.org/ja/

少し解りづらいですね、C#を扱ったことがある方であれば ReactiveProperty を思い浮かべていただけるとイメージが付きやすいと思います。
とは言えこれだけでは少し理解が難しいのでもう少し踏み込んでみます。

まずVuexの構成を確認しましょう。ざっくらばんに以下の3つで成り立っています。

  • 状態、これは私達のアプリを動かす信頼できる情報源(the source of truth)です。
  • ビュー、これは状態のただの宣言的なマッピングです。
  • アクション、これはビューからのユーザー入力に反応して、状態の変更を可能にする方法です。

これをAPI呼び出しという処理になぞえるとこうなります。

  1. Vuexで予め定義したアクションをコンポーネントから呼び出す
  2. 呼び出した非同期アクションからAPIにリクエストを渡し、サーバーサイド側で定義した処理が実行される
  3. サーバーから返却されたJSONをCommitする
  4. Vuexの状態が同期処理で変更される
  5. 変更されたステートをコンポーネントにレンダリングする

最初にVue CLI 3を使用してアプリを作成したときにVuexを使用することを指定したのでsrcフォルダーの中に store.js というファイルが作成されています。

まずは状態を登録します。
store.jsファイルに新しいステートとして変数 hedgehogs を追加し、空の配列を割り当てます。
次に apiUrlという変数も追加し、APIを呼び出すURLをセットします。

store.js

export default new Vuex.Store({
  state: {
    hedgehogs: [],
    apiUrl: "http://127.0.0.1:8081/api/"
  },
  mutations: {},
  actions: {}
});

次に getHedgehogs というアクションを追加します。
このアクションはDjango APIからハリネズミ情報を取得します。

actions: {
  async getHedgehogs({ state, commit }, color) {
    try {
      let response = await axios.get(`${state.apiUrl}hedgehogs/?color=`+ color);
      commit("setHedgehogs", response.data);
    } catch (error) {
      commit("setHedgehogs", []);
    }
  }
}

APIの呼び出しには前項でも取り扱ったaxiosを使用しています。
先ほどはPromiseを使用しましたが、 async/ await を使用して同じ呼び出しするには getHedgehogs メソッドに async という接頭辞をつけるだけで実現できます。

また、 .then / .catch で制御していた成功時と失敗時の分岐はtry~catch句で表現できます。
呼び出し成功時はミューテーションを呼び出し、失敗時は空の配列を渡します。
最終的にstore.jsは次のようになります。

import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    hedgehogs: [],
    apiUrl: "http://127.0.0.1:8081/api/"
  },
  mutations: {
    setHedgehogs(state, payload) {
      state.hedgehogs = payload;
    }
  },
  actions: {
    async getHedgehogs({ state, commit }, color) {
      try {
        let response = await axios.get(`${state.apiUrl}hedgehogs/?color=`+ color);
        commit("setHedgehogs", response.data);
      } catch (error) {
        commit("setHedgehogs", []);
      }
    }
  }
});


HomePlans のお掃除

API呼び出しの処理はVuex側に実装したのでコンポーネントを掃除しておきます。
axiosのインポートとdata()オブジェクトを削除できます。

setHedgehogs メソッドからすべてのコードを削除しVuexストアでアクションを呼び出すためのコードを1行だけ用意しておきます。

this.$store.dispatch("getHedgehogs", color);

Vuexでアクションを呼び出すには上記のように dispatch を使用するだけでOKです。
最終的にHomePlans のscriptタグ内は以下のようになります。

src/components/HomePlans.vue

<script>
export default {
  name: "HomePlans",
  methods: {
    showHedgehogs(color) {
      this.$store.dispatch("getHedgehogs", color);
    }
  }
};
</script>


ハリネズミリストの表示

前項まででVuexを使用しDjangoAPIからハリネズミのリストを取得し配列に格納しました。
この格納したデータを表示するための新しいコンポーネントが必要になります。

componentsフォルダに Hedgehogs.vue を作成します。
この新しいコンポーネントでは以下のようにしてVuexストアから値を取得するようにします。

<script>
export default {
  name: "Hedgehogs",
  computed: {
    hedgehogs() {
      return this.$store.state.hedgehogs;
    }
  }
};
</script>

続いてtemplateタグを更新します。

Vuetifyはページに表示される項目間のマージンを開けてくれるグリッドシステムを提供しています。
このグリッドシステムは以下のように <v-contsinger> に配置することで使用できます。

<v-container grid-list-lg>
</v-container>

そして <v-container> の中に <v-layout> を配置し、その中に v-flex row wrap を指定します。
また、配列内のすべての募集情報をループ処理したいため、v-flex を使用し v-for も併用することにします。

<v-container grid-list-lg>
    <v-layout row wrap>
        <v-flex xs12 sm6 md6 lg4 v-for="hedgehog in hedgehogs">
        </v-flex>
    </v-layout>
<v-container>

最後にVuetifyの v-card を使用して各募集を表示します。
これはHomePlans コンポーネントで使用したレイアウトと似た構成でハリネズミの画像、名前、簡単な説明を表示するために使用します。

<template>
  <v-container grid-list-lg>
    <v-layout row wrap>
      <v-flex xs12 sm6 md6 lg4 v-for="hedgehog in hedgehogs">
        <v-card data-cy="hedgehogEntry">
          <v-responsive>
            <v-img :src="hedgehog.image"></v-img>
          </v-responsive>
          <v-card-text>
            <div class="title my-5">{{ hedgehog.name }}</div>
            <div class="subheading">Description</div>
              {{ hedgehog.description }}
          </v-card-text>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

これでコンポーネントが完成したので例の如く Menu.vue にこの新しいコンポーネントを表示するよう定義してやります。

<template>
  <div>
    <home-plans></home-plans>
    <hedgehogs></hedgehogs>
  </div>
</template>
<script>
import HomePlans from "@/components/HomePlans";
import Hedgehogs from "@/components/Hedgehogs";
export default {
  name: "Menu",
  components: {
    HomePlans,
    Hedgehogs
  }
};
</script>
<style scoped>
</style>

これで準備完了です。

npm run serve を実行しブラウザでMENUでパイルカラーを選ぶと以下のようにDjangoAPIから取得した情報がレンダリングされるようになりました!


最後に次の章で使用するためのボタンを設置しておきましょう。
v-card の中の </v-card-text> の下に <v-card-actions> を配置し、その中に v-btn を置きます。

デフォルトではVuetifyはボタン内のテキストカラーを黒色にするため、dark ディレクティブを追加しテキストカラーを白色に変更します。

<v-card-actions>
  <v-btn color="green" dark>good</v-btn>
</v-card-actions>

最終的なHedgeHogsコンポーネントは次のようになります。

<template>
  <v-container grid-list-lg>
    <v-layout row wrap>
      <v-flex xs12 sm6 md6 lg4 v-for="hedgehog in hedgehogs">
        <v-card data-cy="hedgehogEntry">
          <v-responsive>
            <v-img :src="hedgehog.image"></v-img>
          </v-responsive>
          <v-card-text>
            <div class="title my-5">{{ hedgehog.name }}</div>
            <div class="subheading">Description</div>
              {{ hedgehog.description }}
          </v-card-text>
          <v-card-actions>
            <v-btn color="green" dark >good</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>
<script>
export default {
  name: "Hedgehogs",
  computed: {
    hedgehogs() {
      return this.$store.state.hedgehogs;
    }
  }
};
</script>
<style scoped>
</style>


まとめ

この章ではaxiosを使用してDjangoからデータを取得し、Vuexストアに格納するところまで実装しました。
次回はFirebaseを使用した認証について実装していきます。

完成版のソースコードはGitHubにアップロードしてあります。

2019-11-23TIPS,Vue.jsFirebase,Vuetify,Vuex

Posted by Kenny