0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nuxt.js入門 Vol.1 ~天気予報取得アプリNuxt化編~

Posted at

こんにちは!
LIFULLエンジニアの吉永です。
直近業務でここ一か月間はNuxtにてSPAを開発してましたので、業務で得た知見を忘れないうちにアウトプットしたいと思います!
※やっぱり業務でやるのが一番身に付きますね・・・7:2:1の法則を改めて実感しました

本記事の概要

Vue.js入門 Vol.1 ~jQueryとの対比編~
Vue.js入門 Vol.2 ~jQueryとの対比編~
Vue.js入門 Vol.3 ~基礎まとめで簡易家計簿を作る編~
Vue.js入門 Vol.4 ~外部API呼び出し編~
Vue.js入門 Vol.5 ~業務効率改善ツール作成編~
Vue.js入門 Vol.6 ~computedとwatchの使い分け編~
上記6記事の続きで、Vol.4で作成したVue.jsアプリケーションをNuxt.jsに対応させてみたので、要点をまとめていきます。

本記事で利用させていただいた外部APIと注意事項について

Vue.js入門 Vol.4 ~外部API呼び出し編~の記載内容と重複しておりますので、詳細はこちらを参照してください。

作成するアプリケーションの仕様

Vue.js入門 Vol.4 ~外部API呼び出し編~の記載内容と重複しておりますので、詳細はこちらを参照してください。
今回はNuxt化するにあたり、SPAっぽい動作を加えたかったので、取得した三日間の天気予報情報を詳細ページとして、それ以外の情報を一覧ページにまとめました。
よって作成するページ数は2ページとなります。

画面仕様

初期表示

image.png

天気予報表示時

image.png

日別天気予報詳細表示時

image.png

エラー発生時

image.png

成果物について

下記のリポジトリにてソースを公開しておりますので、ローカルで動作確認などされる際にはクローンしてdocker-compose up -dを実行してください。
https://github.com/yuta-yoshinaga/nuxt_weather

Nuxt.jsについて

既に色々な入門記事にて解説されておりますので、ここでは私がNuxtに触れてみて得た知識ベースでのことを書いていきます。
※間違ってたりしたらすいません・・・

  • Vue.jsでフロントエンドを開発する際のフレームワークの一種。
  • 素のVue.jsで開発する場合と比較すると、コンポーネント化したvueファイルのビルド環境、Vuexという状態管理ライブラリがセットアップ済み、など様々な恩恵を受けることができ、効率的に開発を進めることができる。
  • コンポーネントファイルの自動ロード機能があり、いちいちimportして登録してとかが必要なくて楽。
  • フレームワークなのでフォルダ構成や命名規則が決まっている為、複数人で開発する際にそういった規約類をまとめる必要がないので、余計な手間がかからない。
  • 上記に関連して、ある程度縛りが設けられている為、ソースレビュー時のレビューア負担がノンフレームワーク時と比べると低い。

といったところでしょうか。
後半二つはNuxtというよりかはフレームワークを導入することによるメリットなので、Nuxtに限った話ではありませんが、素のVue.jsで複雑な画面を構築していくと、いつかは破綻するような気は学習しながらしていたので、どうしてNuxt使うのか?という点においては結構重要な要因になっているような気がしました。

Vue.jsからNuxt.jsへ移行する際の覚書

URL定義

まずはページのURLをNuxtではどのように管理しているのかを確認していきます。
Nuxtではジェネレーターによって生成されたスケルトンプロジェクト配下にpagesというフォルダが出来上がるので、ここに任意の名前のvueファイルを作成することでURLを定義できます。
今回作成したアプリケーションではindex.vueとしていますが、例えばこのページの名称をweather-forecast.vueなどに変更すると、URLはhttps://{作成したアプリのドメイン}/weather-forecastになります。
サブフォルダを作ることも可能で、pages/weather/forecast.vueにすればURLはhttps://{作成したアプリのドメイン}/weather/forecastになります。

SPAの為の土台構築

pages/index.vue
<template>
  <div id="app" v-cloak>
    <component :is="this.$store.getters['component']" />
  </div>
</template>

<script>
import List from "~/components/List.vue";
import Detail from "~/components/Detail.vue";
import "~/assets/sass/style.scss";

export default {
  components: {
    "component-list": List,
    "component-detail": Detail,
  },
  created() {
    // ページ切り替え
    this.$store.dispatch("setCurrent", "list");
    // ページ位置を先頭へ戻す
    scrollTo(0, 0);
  },
};
</script>

SPAにする為に、現在有効なコンポーネントを選択する為の記述と初期表示設定が必要になります。

<component :is="this.$store.getters['component']" />

が現在選択中のコンポーネントを判別して切り替え表示する為のコードになります。
this.$store.gettersについての詳細は後述します。

this.$store.dispatch("setCurrent", "list");

が初期表示をlistページにする為のコードです。

  components: {
    "component-list": List,
    "component-detail": Detail,
  },

が、切り替え対象となるコンポーネントの名称と、実際のコンポーネント定義ファイルとの紐づけを行っています。

Vuexについて

Vuexとは何か?
にて解説されておりますので、要約しますが、Vue.jsアプリケーションにおいて、共通で参照したいデータを安全に格納してくれるので、複数コンポーネント間でグローバルに参照したい値を実現することが可能です。
通常のグローバル変数では、だれが、いつ、どの、タイミングで書き換えたか?などが度々問題になり、バグの温床になりやすいので、基本的には疎結合を目指すことになります。
反面、アプリケーション内でデータキューを用いてデータの受け渡しをしたり、関数の引数の数が増大したりなど、複雑化してしまう側面もあり、グローバル変数に近い形で参照はできるものの、上記で記したような課題を解決してくれるものが必要になることもあり、Vuexは誕生したのでは?と勝手に予測しています。
※違ってたらすいません
今回作成したサンプルアプリでは、APIの戻り値や現在表示中のページなどをVuexで管理して、複数コンポーネント間で簡単にデータの共有を出来るようにしました。
Vuexではまず管理したい値をstore/state.jsに定義します。

store/state.js
export default () => ({
    componentTypes: {
        list: "component-list",
        detail: "component-detail",
    },
    current: "",
    currentDate: "",
    curWether: null,
    hasError: false,
    errorMessage: "",
    citys: null,
    curPref: null,
    curCity: null,
    loading: false,
})

store/state.jsで定義した値を参照する場合はstore/getters.jsにゲッター関数を定義します。

store/getters.js
export default {
    component: (state) => {
        return state.componentTypes[state.current];
    },
    currentDate: (state) => {
        return state.currentDate;
    },
    curWether: (state) => {
        return state.curWether;
    },
    hasError: (state) => {
        return state.hasError;
    },
    errorMessage: (state) => {
        return state.errorMessage;
    },
    citys: (state) => {
        return state.citys;
    },
    curPref: (state) => {
        return state.curPref;
    },
    curCity: (state) => {
        return state.curCity;
    },
    loading: (state) => {
        return state.loading;
    },
}

上記のゲッター関数はthis.$store.getters[{ゲッター関数名}]にて呼び出すことが可能です。


store/state.jsで定義した値を変更する場合はstore/mutations.jsにセッター関数を定義します。

store/mutations.js
export default {
    current: (state, setCurrent) => {
        state.current = setCurrent;
    },
    currentDate: (state, setCurrentDate) => {
        state.currentDate = setCurrentDate;
    },
    curWether: (state, setCurWether) => {
        state.curWether = setCurWether;
    },
    hasError: (state, setHasError) => {
        state.hasError = setHasError;
    },
    errorMessage: (state, setErrorMessage) => {
        state.errorMessage = setErrorMessage;
    },
    citys: (state, setCitys) => {
        state.citys = setCitys;
    },
    curPref: (state, setCurPref) => {
        state.curPref = setCurPref;
    },
    curCity: (state, setCurCity) => {
        state.curCity = setCurCity;
    },
    loading: (state, setLoading) => {
        state.loading = setLoading;
    },
}

上記のセッター関数はthis.$store.commit({セッター関数名},{パラメーター})にて呼び出すことが可能です。


とここまでで、Vuexにおける、データ定義、参照、更新が実現できているのですが、Vuexにはもう一人登場人物がいます。
それがstore/actions.jsです。

store/actions.js
export default {
    setCurrent: ({ commit }, current) => {
        commit("current", current);
    },
    setCurrentDate: ({ commit }, currentDate) => {
        commit("currentDate", currentDate);
    },
    setCurWether: ({ commit }, curWether) => {
        commit("curWether", curWether);
    },
    setHasError: ({ commit }, hasError) => {
        commit("hasError", hasError);
    },
    setErrorMessage: ({ commit }, errorMessage) => {
        commit("errorMessage", errorMessage);
    },
    setCitys: ({ commit }, citys) => {
        commit("citys", citys);
    },
    setCurPref: ({ commit }, curPref) => {
        commit("curPref", curPref);
    },
    setCurCity: ({ commit }, curCity) => {
        commit("curCity", curCity);
    },
    setLoading: ({ commit }, loading) => {
        commit("loading", loading);
    },
    getWeather: ({ state, dispatch }, $axios) => {
        dispatch("setHasError", false);
        dispatch("setErrorMessage", "");
        dispatch("setCurWether", null);
        dispatch("setLoading", true);
        $axios
            .get(
                "https://weather.tsukumijima.net/api/forecast/city/" + state.curCity
            )
            .then(
                function (response) {
                    if (response.data) {
                        if (response.data.error) {
                            dispatch("setHasError", true);
                            dispatch("setErrorMessage", response.data.error);
                        } else {
                            // API戻り値を設定
                            dispatch("setCurWether", response.data);
                        }
                    }
                }
            )
            .catch(
                function (error) {
                    dispatch("setHasError", true);
                    dispatch("setErrorMessage", response.data.error);
                }
            )
            .finally(
                function () {
                    dispatch("setLoading", false);
                }
            );
    },
}

上記アクションはthis.$store.dispatch({アクション関数名}, {パラメーター})にて呼び出すことが可能です。

store/action.jsについては下記の図を参照してもらうのが早いのですが、図によれば基本的にコンポーネントから直接mutationsを呼び出すことは考慮されていないように見えます。※実際にはactionを経由しなくても、コンポーネントから直接mutationsを呼び出すことは可能です。
というのも、mutationsは同期処理でなければならず、actionは非同期も可能ということもあり、コンポーネントからmutationsを直接呼び出すのではなく、actionを経由して値の更新を行った方が良いということが理由のようです。
この辺についてはこちらの記事が分かりやすく解説してくれていたので、参照してみてください。


出典:Vuex とは何か? https://vuex.vuejs.org/ja/

上記の例でもgetWeatherはaxiosを使って外部APIとの通信を行っており、APIの戻り値を最終的にcommitしてmutationsの処理を呼び出すようになっています。

各コンポーネントについて

Vue.jsにおけるコンポーネントとは再利用することを目的としたHTMLテンプレートコードとJavaScriptやCSSをひとまとめにしたパーツです。
今回作成したアプリケーションでは下記が一番ベースとなるコンポーネントになっています。

components/Parts/Common/Box.vue
<template>
  <div class="box">
    <div class="field">
      <label class="label">{{ curTitle }}</label>
      <div class="control" v-html="curPropaty" @click="clickEvent"></div>
    </div>
  </div>
</template>

<script>
export default {
  props: ["curTitle", "curPropaty", "callback", "callbackParam"],
  methods: {
    clickEvent() {
      if (this.callback) {
        this.callback(this.callbackParam);
      }
    },
  },
};
</script>

propsとはコンポーネント利用側からコンポーネントに対して渡されるパラメーターで、上記パーツは4つのパラメーターを受取り、ボックス内の見出しや内容の表示を行っています。
また、一覧表示ページから日別の天気予報閲覧ボタンを押された時のイベント受け取り用にcallbackcallbackParamを受取り、callbackが定義されていればコールバック関数を呼び出すようになっています。


続いて、上記パーツをラップして、横並びや縦並びにするパーツです。

components/Parts/Common/BoxWrap.vue
<template>
  <div class="box">
    <div class="field">
      <label class="label">{{ label }}</label>
      <div class="control columns" v-if="isColums">
        <div class="column" v-for="(element, index) in elements" :key="index">
          <PartsCommonBox
            :curTitle="element.title"
            :curPropaty="element.propaty"
          />
        </div>
      </div>
      <div class="control" v-else>
        <PartsCommonBox
          v-for="(element, index) in elements"
          :key="index"
          :curTitle="element.title"
          :curPropaty="element.propaty"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: ["label", "elements", "isColums"],
};
</script>

こちらのpropsでは先ほどのcomponents/Parts/Common/Box.vueに渡すための各種パラメーターの配列を受取り、横並びかそれ以外かでHTMLテンプレートコードを切り替えるような作りになっています。

コンポーネントの呼び出し方

また、自身で作成したコンポーネントの呼び出し方ですが、components配下から対象のvueファイルまでのパスをPascal形式で記述します。※詳細は公式ドキュメントを参照してください。

mixinについて

ここまでで、自身で定義したコンポーネントの呼び出し方、コンポーネントへのパラメーターの渡し方が理解できたと思いますが、コンポーネントに対して常にpropsを渡すのは煩わしい、複数コンポーネント間で共有できるような処理を作りたいという時にはmixinという機能を使うと便利です。

components/mixin.js
export default {
    methods: {
        getCurWether() {
            return this.$store.getters['curWether'];
        },
        getCurForecast() {
            let res = this.getCurWether().forecasts.filter(
                (current) => current.date === this.$store.getters['currentDate']
            );
            return res.length > 0 ? res[0] : null;
        },
    }
}

mixinを使用する場合はまず、複数コンポーネント間で共通で利用する関数群を上記のようなファイルに定義します。
続いて、mixinを利用する側ですが、下記のような形で読み込みます。

components/List.vue
<template>
  <div>
    <h1 class="title">天気予報</h1>
    <PartsListInput />
    <PartsListError />
    <div v-if="getCurWether()">
      <PartsListHead />
      <PartsListDescription />
      <PartsListForecasts />
      <PartsListArea />
      <PartsListCopyright />
    </div>
  </div>
</template>

<script>
import Mixin from "./mixin";
export default {
  mixins: [Mixin],
};
</script>

まずは対象となるmixinファイルをimportし、Vueオブジェクトのmixinプロパティに読み込んだファイルを設定します。
この記述を行うだけ、複数コンポーネントで共通で利用する関数を使えるようになります。

結局コンポーネント間のデータ共有はどのように使い分けた方が良いのか?

コンポーネントとコンポーネント利用側とでデータを受け渡したい場合には先述したようにいくつか選択肢があります。
今回は紹介しなかったのですが、コンポーネント利用側へコンポーネントからイベント通知をする為の仕組みもVue.jsでは用意されています。詳細は公式ドキュメントを参照してください。

基本的にはpropsを使うのが、コンポーネント間が疎結合になるので、良いと思いますが、複数コンポーネント間で共通で利用したい処理をmixinで定義しておくのはコードがすっきりするので、ケースバイケースで併用したり使い分けるのが良さそうです。
今回のmixin処理ではVuexのゲッターからAPI戻り値を参照していますので、グローバルに参照したい値をmixin経由で取得するっていうのも、データの保存先がローカルではなくまた別のAPI経由とかになった際の変更にも強くなると思いますので、このような作りにしましたが、直接各コンポーネントからVuexのゲッターを呼び出すのもなしではないと思いますので、この辺もプロジェクトの規模やチーム内での規約などに従って実装できると良いのではと思いました。

Vuexで管理している値をv-modelで双方向データバインディングする際の注意点

Vue.jsではv-modelを用いて、簡単に双方向データバインディングを実現できますが、Vuexで管理している値をv-modelで双方向データバインディングする際には注意が必要です。
というのも、Vuexでは基本的にmutations以外での値変更を禁止しており、Nuxtの動作モードによってはエラーでアプリケーションが終了してしまいます。
今回具体的にVuexの値をv-modelしているコンポーネントは下記になるのですが、

components/Parts/List/Input.vue
<template>
  <div>
    <div class="columns">
      <div class="column">
        <label class="label">都道府県</label>
        <select v-model="curPref" @change="prefChange">
          <option v-for="(pref, index) in prefs" :key="index">
            {{ pref.name }}
          </option>
        </select>
      </div>
      <div class="column">
        <label class="label">地域</label>
        <select v-model="curCity">
          <option
            v-for="(city, index) in citys()"
            :value="city.id"
            :key="index"
          >
            {{ city.name }}
          </option>
        </select>
      </div>
      <div class="column">
        <button :class="btnClass" @click="getWeather" :disabled="canSendBtn">
          取得
        </button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      prefs: this.$INFO_TBL.prefs,
    };
  },
  computed: {
    btnClass() {
      return {
        button: true,
        "is-primary": true,
        "is-loading": this.loading(),
      };
    },
    canSendBtn() {
      return this.curPref && this.curCity && !this.loading() ? false : true;
    },
    curPref: {
      get() {
        return this.$store.getters["curPref"];
      },
      set(value) {
        this.$store.dispatch("setCurPref", value);
      },
    },
    curCity: {
      get() {
        return this.$store.getters["curCity"];
      },
      set(value) {
        this.$store.dispatch("setCurCity", value);
      },
    },
  },
  methods: {
    citys() {
      return this.$store.getters["citys"];
    },
    loading() {
      return this.$store.getters["loading"];
    },
    prefChange() {
      let pref = this.prefs.filter((current) => current.name === this.curPref);
      if (pref.length != 0) {
        this.$store.dispatch("setCitys", pref[0].citys);
        this.$store.dispatch("setCurCity", null);
      }
    },
    getWeather() {
      this.$store.dispatch("getWeather", this.$axios);
    },
  },
};
</script>

上記のcomputedで定義した算出プロパティcurPrefcurCityをv-modelで双方向データバインディングしています。
通常のcomputedと少々書き方が異なると思いますが、C#などの近代言語でプロパティを扱ったことがある人には馴染みがある記述になっており、getがゲッターsetがセッターとなっています。

    curPref: {
      get() {
        return this.$store.getters["curPref"];
      },
      set(value) {
        this.$store.dispatch("setCurPref", value);
      },
    },

このように、値を書き換える側からは通常の変数のようにふるまい、実際にはアクセッサメソッドが動作するようになっているので、算出プロパティなんだなと改めて勉強になりました。
アクセッサメソッドを経由することで、Vuexの値を直接更新することなく、actionを呼び出して、mutationsの処理で安全にVuexの値を変更することができます。
上記手法のあまり良くない点としては、$store.gettersはキャッシュされるようになっており、computedもキャッシュされるので、二重でキャッシュされてしまうという点があります。
これを回避するには、ローカルのdataプロパティにVuexの値のディープコピーを設定し、ローカルデータの値の変更をwatchで監視して、変更されたタイミングでactionを呼び出すなどすることで回避できますが、少々複雑になるので、2重キャッシュのデメリットと、多少複雑化しても余計なキャッシュをさせない手段をとるかはプロジェクトごとの判断になってくるのかなと思います。
その他にも良い解決手段などあれば今後も模索していきたいです。

最後に

いかがでしたでしょうか?
Nuxt.jsはフレームワークなので、まずはフレームワークのお作法を覚える必要もあり、かつVue.jsの知識がある前提になっているので、まずはVue.jsの基礎を固める必要はありますが、Nuxt.jsを習得すると効率よくフロントエンドを開発する技術が身につくと思いますし、フレームワークはそれぞれ思想や色が違いますが、例えば今後ReactベースのNext.jsの学習をする際には、Nuxtで言うところのこの機能はNext.jsではどれに当たるのか?など、既に持っている知識の差分で習得することもできる場面があると思います(と言っても私はReactもNext.jsも全く知らないので予想なんですが・・・)ので、何かしらのフレームワークを学習しておくことは後々にも役に立つかなと思いました!

それではまた次の記事でお会いしましょう!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?