開発合宿でMTGアプリを作ってきたお話


はじめに

年明けすぐに、社内の有志を中心としたメンバーで開発合宿に行ってきました。
今回は、千葉の「土善旅館」さんのお世話になり、猫と戯れながらの二泊三日で開発したアプリのお話です.

作ったもの

MtG 今日の一枚

構成

  • vue.js
  • vue-cli
  • Vuex
  • Vue-Router
  • mtg-sdk
  • Bootstrap4

デプロイ先には、Github Pagesを利用しました.

mtg-sdkとは

mtg-sdkについてですが、このmtg、ミーティングのことではありません。
今回のmtgは、20年以上の歴史を誇るトレーディングカードゲーム、「Magic the Gathering」のことです。

このゲームのカードについての情報を取得するAPIが提供されており、mtg-sdkはこのAPIのjavascript向けSDKです。

Vuex

今回、初めてVuexを使ったアプリに挑戦しました。
APIから取得したカード情報、絞り込み機能の状態管理に利用します。
実際のコードは以下のようになりました。

store.js
import Vue from 'vue'
import Vuex from 'vuex'
import API from '@/api/api'
import { FORMAT } from '@/const'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    card: null,
    jpnCard: null,
    gameFormat: FORMAT.STANDARD.param,
    types: '',
    colors: [],
    isColorSearchAnd: false
  },
  mutations: {
    setCardInfo: (state, payload) => {
      state.card = payload
    },
    setJpnCardInfo: (state, payload) => {
      state.jpnCard = payload
    },
    setFormat: (state, payload) => {
      state.gameFormat = payload
    },
    setTypes: (state, payload) => {
      state.types = payload
    },
    setColors: (state, payload) => {
      state.colors = [...state.colors, payload]
    },
    removeColors: (state, payload) => {
      state.colors = state.colors.filter(color => color !== payload)
    },
    setIsColorSearchAnd: (state, payload) => {
      state.isColorSearchAnd = payload
    }
  },
  actions: {
    fetchRandomCard: async ({ commit, state }) => {
      commit('setCardInfo', null)
      commit('setJpnCardInfo', null)
      // GETパラメータ組み立て
      let colors = state.isColorSearchAnd ? state.colors.join() : state.colors.join('|')
      const params = {
        gameFormat: state.gameFormat,
        types: state.types,
        colors: colors
      }
      /**
       * 日本語の存在するカード
       * @type {Object}
       */
      return API.fetchRandomCard(params).then(card => {
        commit('setCardInfo', card)
        // 日本語カードがあればcommit
        const jpnCard = card.foreignNames.find(foreignName => {
          return foreignName.language === 'Japanese'
        })
        commit('setJpnCardInfo', jpnCard)
      }).catch(() => {
        alert('APIエラー')
      })
    },
    buildColorsParam: ({ commit, state }, color) => {
      if (state.colors.includes(color)) {
        commit('removeColors', color)
      } else {
        commit('setColors', color)
      }
    }
  },
  getters: {
    getCard: state => {
      return state.jpnCard ? state.jpnCard : state.card
    }
  }
})

actionのfetchRandomCardでカード取得APIをラップした関数を呼び出します。
呼び出す前にカード情報を一度空にしていますが、これは画面表示の都合です。

ここでは、取得したカード情報(英語)と、日本語版カードの情報(存在すれば)をセットするmutationをコミットしています。

ちなみに、actionはPromiseを返すので、それを利用してカード情報取得中にスピナー(ロード中を表す、くるくる回るアレ)を出しています。

Vue-Router

構成には入っているものの、今回は時間の都合のため全く使っていません。
本当は「使い方」みたいなページを作ってリンク貼るくらいはしたかった……

コンポーネント設計

あまり機能の多くないアプリなので、それほどちゃんとした設計はしていません。
ただ、絞り込み機能で使うプルダウンリストやチェックボックスなどは、ある程度使い回しが効くように工夫しました。
プルダウンリストの基本コンポーネントは、以下のようになりました。

BaseSelect.vue
<template>
  <div class="form-group">
    <label :for="selectorId">
      <slot></slot>
    </label>
    <select :id="selectorId" class="form-control" @change="selectChange">
      <option v-for="(content, index) in contents" :key="index" :value="content.param">
        {{content.name}}
      </option>
    </select>
  </div>
</template>

<script>
export default {
  name: 'BaseSelect',
  props: {
    selectorId: {
      type: String,
      required: true
    },
    contents: {
      type: Array,
      required: true
    }
  },
  methods: {
    selectChange: function (event) {
      this.$emit('select-change', event.target.value)
    }
  }
}
</script>

<style scoped>

</style>

labelタグの間にslotタグが入っており、具体的な使用箇所で文字を埋め込んで、プルダウンリストのラベルにしています。
リストとして選択させたい項目を配列で渡すと、それを表示するというシンプルなものです。
ユーザが選択した項目のvalueが、emitを介して親コンポーネントに渡されます。

親コンポーネントで表示させるラベルと選択させたい項目をそれぞれ渡して、子コンポーネントがemitしたイベントに紐付く処理を書くだけで、簡単にプルダウンリストが設置できるようになりました。

このように、コンポーネント内部または親子間で完結するようなデータであれば、Vuexを介さずprops/emitで管理する方がシンプルでいいでしょう。
逆に、子→親→子のような流れであったり、2世代以上の親子関係でのデータのやりとりであれば、素直にVuexを使う方がわかりやすいと思います。

Github Pages

この手のシンプルなSPAを無料でサクッと公開するという点においては、Github Pagesは最強のプラットフォームだと思います。
vue-cliで作ったプロジェクトをGithub Pagesに公開するための設定と手順を簡単にまとめます。

  • vue.config.jspablicPathを設定する。

Github PagesのURLは、https://<ユーザ名>.github.io/<リポジトリ名>/になります。
このURLに対応させるため、pablicPathに本番ビルドの時だけ/<リポジトリ名>/のURLに対応させる設定を入れる必要があります。

  • npm run buildを実行する。

vue-cliでプロジェクトを作成した時点で用意されているnpm-scriptで、本番ビルドを行います。
ビルドしたものはdist以下に配置されます。

  • gh-pagesを導入して使う。

最後に、npm i --save-dev gh-pagesを叩いて、gh-pagesというライブラリを導入します。
これを使うと、Github Pagesで公開したいファイルだけをgh-pagesブランチにプッシュすることができます。
今回公開したいファイルはdist以下のみなので、以下のコマンドだけ打ちます。

gh-pages -d dist

ビルドからgh-pagesブランチのプッシュまでを、npm-scriptで一つのコマンドにまとめてしまうと幸せです。

  • リポジトリの設定からGithub Pagesを有効にする

最後に、リポジトリのSettings > Options > Github Pagesから対象のブランチをgh-pagesブランチに設定して、有効化します。

以上4ステップで、SPAを公開することができました!
もちろんドメインを取得して、それを設定することもできます。

まとめ

APIから取得した複雑な構造のデータであったり、複数のコンポーネント間で共有される状態が存在する場合、Vuexが力を発揮することを実感しました。
Vuex自体はやや面倒な制約や順番が多いですが、それらをしっかり守ることで状態管理の見通しがよくなるだけでなく、Vue Devtoolの支援も受けられるようになるので非常に便利でした。

今後アプリに機能を追加する際は別ページとして実装し、Vue-Routerで遷移させるような仕組みにしようと思います。

また、Github Pagesは簡単設定で爆速デプロイが出来るので、ちょっとした物を作って公開する時などに便利でした。

余談

当初作ろうと思っていたもの

今回作ったアプリは、「ボタンを押すとカード画像が表示される」だけのものです。
実は、当初はデッキ管理アプリを目指していました。
日本語名や様々な絞り込み条件でカードを探し、デッキリストをブラウザ上でお手軽に管理出来るような物を作ろうと思っていたのですが……

2019年1月時点で、APIの他言語絞り込み機能が壊れています
日本語でカードが探せないのにデッキ管理なんて出来るか!と悪態をつきながら、開発合宿の初日に方向転換を余儀なくされました。

短期間でAPI叩いて表示するアプリを作るときは、そのAPIの事前調査が重要だということを思い知らされました。

土善旅館

開発合宿でお世話になった旅館ですが、ここは結構有名な場所らしいですね。
大画面のスクリーンとプロジェクタ、開発用のモニタや人をダメにするソファ等の様々な物をレンタルすることが可能で、素晴らしい環境で開発することができました。
人慣れしきった猫ちゃんが旅館におり、適度に猫に邪魔されながら開発することが出来る点が個人的に高評価です。