HAKUTAI Tech Notes

IT関連、数学のことなどを主として思いつきで色々書き溜めていきます

Vuexを使ったサンプル色々【後編】

Vuexを使ったサンプル色々【前編】からの続きということで、残り一つの機能「天気予報の取得」weatherForecastを作成する。

ディレクトリ構成

前編でも載せていたが、ディレクトリ構成を再度記載しておく。 さりげなく置かれていたassetsディレクトリの中身はここで利用する。

└── src
    ├── App.vue
    ├── assets
    │   ├── json
    │   │   └── city_id_data.json
    │   └── pic
    │       └── loading.gif
    ├── components
    │   ├── solveEquation
    │   │   ├── Input.vue
    │   │   ├── Result.vue
    │   │   └── SolveEquationContainer.vue
    │   ├── timer
    │   │   ├── Buttons.vue
    │   │   ├── Display.vue
    │   │   └── TimerContainer.vue
    │   └── weatherForecast
    │       ├── AreaSelect.vue
    │       ├── WeatherForecastContainer.vue
    │       └── WeatherTable.vue
    ├── main.js
    └── store
        ├── index.js
        └── modules
            ├── solveEquation.js
            ├── timer.js
            └── weather.js

3. 天気予報取得機能の実装

外部のAPIを利用して天気予報を取得し、天気・気温・降水確率などの情報を表示する。
ここでは非同期処理を含むアクションについて触れる。

実装の準備

Vuexの説明から脱線するが、天気予報を取得するための準備が少し必要になる。
使用するのは天気予報 API(livedoor 天気互換)のAPIで、ベースとなるURLに地域IDを追加してリクエストするだけで実行当日を含む3日間の天気情報が取得できるという便利なものだ。 全国の地点定義表のxmlに都道府県名、地域名、地域IDがまとまっているので、必要な情報だけ抽出し使い勝手良くJSONファイルに加工した。

{
  "prefectures": [
    {
      "name": "北海道",
      "id": "01",
      "cities": [
        {
          "name": "稚内",
          "id": "011000"
        },
        {
          "name": "旭川",
          "id": "012010"
        },
            :
           省略
            :
        {
          "name": "江差",
          "id": "017020"
        }
      ]
    },
            :
           省略
            :
  ]
}

weatherモジュールの定義

import axios from 'axios'

export const weather = {
  namespaced: true,
  state: {
    cityId: '', // 地域ID
    isLoading: true // 情報取得中フラグ
  },
  mutations: {
    setCityId (state, id) {
      state.cityId = id
    },
    setLoadingFlag (state, flag) {
      state.isLoading = flag
    }
  },
  actions: {
    updateCityId (context, id) {
      context.commit('setCityId', id)
    },
    getWeatherInfo (context) {
      context.commit('setLoadingFlag', true)
      // 【⑫】
      return new Promise((resolve, reject) => {
        axios.get(`https://weather.tsukumijima.net/api/forecast/city/${context.state.cityId}`)
          .then(result => {
            context.commit('setLoadingFlag', false)
            resolve(result.data)
          })
          .catch(err => {
            context.commit('setLoadingFlag', false)
            reject(err)
          })
      })
    }
  }
}

【⑫】アクション内の非同期処理
Vuexのアクション内には非同期処理を含めることができる。 axiosgetメソッドでリクエストしている部分がそれに当たる。 アクションはPromiseを返すこともできるので、このアクションをディスパッチしているコンポーネント側で処理の完了を待つこともできる。
ここでは、アクションの先頭でstatesetLoadingFlagをtrueに設定しておき、天気予報データの取得が完了(もしくは何らかのエラーが発生)したらsetLoadingFlagをfalseに更新している。 コンポーネント側でsetLoadingFlagを参照しているので、データ取得処理中のボタン操作の制限や「処理中」を示すスピナーを表示するなど色々できる。

このように非同期でstateの更新が必要な場合は、ミューテーションに直接非同期処理を入れるのではなく、アクションに非同期処理を任せてその中から適宜ミューテーションを実行することになる。

コンポーネントの実装

AreaSelect.vue

天気情報を取得したい地域を選択する。 1つめのプルダウンで都道府県を選択すると、2つめのプルダウンでその都道府県にある観測地点を選べるようになる。 天気予報データの取得処理中はプルダウンを非活性にする。

<template>
  <div class="selector">
    <label>都道府県:</label>
    <select name="prefecture" v-model="selectedPrefId" @change="onChangePref" :disabled="isLoading">
      <option v-for="pref in prefectures" :key="pref" :value="pref.id">{{ pref.name }}</option>
    </select>
    <label>地域:</label>
    <select name="city" v-model="selectedCityId" @change="onChangeCity" :disabled="isLoading">
      <option v-for="city in cities" :key="city" :value="city.id">{{ city.name }}</option>
    </select>
  </div>
</template>
<script>
import { useStore } from 'vuex'
import { ref, onMounted, computed } from 'vue'
import cityIdDataJson from '@/assets/json/city_id_data.json'

export default {
  setup() {
    const $store = useStore()
    const cityIdData = JSON.parse(JSON.stringify(cityIdDataJson)) // 都道府県毎のcityIDの情報
    const selectedPrefId = ref('') // 選択中の都道府県
    const prefectures = ref([]) // 都道府県プルダウンの中身
    const cities = ref([]) // 地域プルダウンの中身

    const selectedCityId = computed(() => $store.state.weather.cityId) // 選択中の地域
    const isLoading = computed(() => $store.state.weather.isLoading) // 情報取得中のフラグ

    const onChangePref = () => {
      // 都道府県の選択値に応じて都市プルダウンの中身を切り替える
      cities.value = cityIdData.prefectures.find(pref => pref.id === selectedPrefId.value).cities
      $store.dispatch('weather/updateCityId', cities.value[0].id)
    }

    const onChangeCity = (e) => {
      $store.dispatch('weather/updateCityId', e.target.value)
    }

    onMounted(() => {
      prefectures.value = cityIdData.prefectures.map(pref => {
        return { name: pref.name, id: pref.id }
      })
      selectedPrefId.value = prefectures.value[0].id
      cities.value = cityIdData.prefectures.find(pref => pref.id === selectedPrefId.value).cities
      $store.dispatch('weather/updateCityId', cityIdData.prefectures[0].cities[0].id)
    })

    return { prefectures, cities, selectedPrefId, selectedCityId, isLoading, onChangePref, onChangeCity }
  }
}
</script>

準備段階で用意しておいた地域IDのJSONファイルをcityIdDataJsonとしてインポートし、JSON.parse()したものを使って都道府県、地域選択プルダウンを作っている。
都道府県、地域の選択を切り替える度にストアのcityIdを更新しているぐらいで、その他にVuexに関して特筆することはない。

WeatherTable.vue

天気情報の取得実行ボタンと、取得した結果を表示する。 天気予報データの取得処理中は取得実行ボタンを非活性にし、天気情報を表示する部分にスピナーのgif画像を表示する。

<template>
  <div>
    <button @click="onClick" :disabled="isLoading">天気情報を取得</button>
  </div>
  <h3>{{ title }}</h3>
  <div class="info-area" v-if="forecasts.length > 0 && !isLoading">
    <div class="info-container" v-for="(forecast, index) in forecasts" :key="index">
      <p><span>{{ displayDate(forecast.date) }}</span><span>{{ forecast.dateLabel }}</span></p>
      <img :src="forecast.image.url">
      <ul>
        <li class="max-temperature">最高:{{ forecast.temperature.max.celsius }}℃</li>
        <li class="min-temperature">最低:{{ forecast.temperature.min.celsius }}℃</li>
      </ul>
      <table border="1">
        <tr>
          <td></td>
          <td>0~6</td>
          <td>6~12</td>
          <td>12~18</td>
          <td>18~24</td>
        </tr>
        <tr>
          <td>降水</td>
          <td>{{ forecast.chanceOfRain.T00_06 }}</td>
          <td>{{ forecast.chanceOfRain.T06_12 }}</td>
          <td>{{ forecast.chanceOfRain.T12_18 }}</td>
          <td>{{ forecast.chanceOfRain.T18_24 }}</td>
        </tr>
      </table>
    </div>
  </div>
  <div class="loading" v-else>
    <img src="@/assets/pic/loading.gif" width="50" height="50">
  </div>
</template>
<script>
import { useStore } from 'vuex'
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    const $store = useStore()
    const title = ref('')
    const forecasts = ref([]) // 取得した天気情報
    const isLoading = computed(() => $store.state.weather.isLoading)

    const onClick = () => {
      $store.dispatch('weather/getWeatherInfo')
        .then(result => {
          title.value = result.title
          forecasts.value = result.forecasts
        })
        .catch(err => console.log(err))
    }

    // XX月XX日(X)形式に変換
    const displayDate = (dateString) => {
      const d = new Date(dateString)
      const week = ['日', '月', '火', '水', '木', '金', '土']
      const month = d.getMonth() + 1
      const date = d.getDate()
      const day = week[d.getDay()]
      return `${month}${date}日(${day})`
    }

    onMounted(() => {
      // 【⑬】
      $store.dispatch('weather/getWeatherInfo')
        .then(result => {
          title.value = result.title
          forecasts.value = result.forecasts
        })
        .catch(err => console.log(err))
    })

    return { title, forecasts, isLoading, onClick, displayDate }
  }

}
</script>

【⑬】非同期アクションの呼び出し
dispatchを用いる点は通常のアクションの呼び出しと何ら変わらないが、非同期処理を含みPromiseを返却するアクションを呼び出した場合はthencatchでアクションの完了を待って続きの処理を行うことができる。 もちろんasyncawaitも使える。

おわりに

前編、後編を通して一通りVuexの機能に触れた。 もっとも、この程度の規模のものであればコンポーネント間の値のやりとりにわざわざ状態管理を利用せずprops$emitを使うだけでもいいと思うが、Vuexを試しに使ってみることが目的だったのでまあよしとしよう。
Composition APIとOptions APIとの間に記述の仕方の違いや利用可能な機能の差があるので今回触れていないもの(mapStatemapMutationsなどはOptions APIでしか使えない)もあるが、これだけ作ればVuexの基本は十分でしょう。
ソースコード全体
デモページ


参考