"BOKU"のITな日常

BOKUが勉強したり、考えたことを頭の整理を兼ねてまとめてます。

Nuxtでライブラリ「Vuex」で状態を管理し、vue.jsの「v-if」で状態を参照して分岐する

f:id:arakan_no_boku:20190509225706p:plain

目次

はじめに

リライトして目次を追加しました。

内容は2019年5月当時のままです。

僕がNuxt.jsの各機能を理解するために、ひとつひとつ確認したことをまとめていこうと考えています。

インストール・環境構築は、以下の手順でできている前提です。

arakan-pgm-ai.hatenablog.com

Nuxt.jsを整理する方針

Nuxt.js関連資料やサンプルソースを見ると混乱することが多いのは、Nuxt.jsはVue.jsの機能を包含していて、ひとつのソースの中で、どれがNuxt.js特有の機能で、どれがVue.jsの機能かの区別がつけにくいからだと感じています。

その混在しているイメージを図にしてみると、こんな感じに見えます。

f:id:arakan_no_boku:20190511100242p:plain

なので、僕はアプローチとして、各技術要素にわけて、一回に少しずつ要素を確認していくことにしました。

分け方としては、以下のようになると考えています。

  1. Nuxt.js独自の要素ー基本
  2. Vue.js独自の要素
  3. BulmaのCSS要素(Burfyのclassは、Bulmaを使っている場合が多い)
  4. Buefy独自の要素
  5. Nuxt.js独自の要素ー応用
  6. テストフレームワーク他モジュールの要素

今回は、Nuxt.js独自の要素ー応用として、状態管理を整理したいと思ってます。

状態管理ライブラリ「Vuex」

状態とは、例えば。

  • 認証情報
  • 検索クエリ
  • フォームの入力情報
  • 個々のコンポーネントの動作の状態

などなど・・フロントエンドでページをまたいで管理すべきものです。

従来のWebアプリケーションの場合、画面の書き換え都度状態はリセットされます。

でも。

SPA( single-page application)の様な、画面を部分更新していくタイプのアプリケーションの場合、画面遷移しても状態がリセットされません。

それもあって。

規模が大きくなって管理すべき状態が増えたり、状態を更新するページが多岐にわたるようになると、非常に複雑になります。

複雑になると当然ながら状態管理は困難な仕事になります。

なので。

予測可能な方法によってのみ状態の変異を行うというルールを保証してくれる 状態管理パターン + ライブラリ「Vuex」の出番がでてくる・・というわけです。

vuex.vuejs.org

 

Vuexの仕様を整理する

この図につきるわけですが。

f:id:arakan_no_boku:20190529223427p:plain

状態は「state(ステーツ)」に保存します。

状態は認証情報なら、ログインしているか、していないかみたいなことなので、Vueコンポーネントはこの「state」を参照して処理をすることで、いちいち、自前でログインチェックなどをする必要がなくなります。

この状態「stats」を変更したいときは「Action(アクション)」を使います。

この「Action」が何をするかというと、「mutation(ミューテーション)」をコミットします。

この「mutation(ミューテーション)」のコミットと言われても意味がわかりませんが、ようするに状態「stats」を変更するということです。

なぜかというと、「state(ステーツ)」を変更する唯一の方法が「mutation(ミューテーション)」をコミットすることと決められているからです。

ただし、ちょっと気をつけないといけないのが。

「Action」は非同期の処理を含むことができるのですが、「mutation(ミューテーション)」は同期的である必要があることです・・なんて書きつつ、このへん僕も完全には理解できてませんけど(笑)。

とりあえず。

stateで状態を表し、mutationをコミットするActionを使ってstateを更新する。

そして、Nuxt.jsでは、このstateとかmutationを「store」フォルダで管理する。

このへんを理解しとけばいいかなと思ってます。

Vuexのヘルパー関数

Vuexには、便利なヘルパー関数があります。

vuex.vuejs.org

今回は以下の3つを使います。

  • mapState
  • mapMutations
  • mapActions

色んな使い方ができますが、一番スッキリとバインドを書けるやり方でやります。

使い方のポイントなどは、ソースの説明と一緒にします。

 

状態管理を使ったサンプル

状態管理をつかって、簡単なサンプルを作ってみます。

Vuexの2つのモード

NuxtでVuexを使うには2つのモードがあります。

  • クラッシックモード
  • モジュールモード

です。

でも、クラッシックモードはもう「非推奨」で「廃止予定」です。

実際には「モジュールモード」一択です。

Nuxtをモジュールモードで動作させる条件は以下です。

  • storeフォルダに「index.js」が存在しない。
  • または、index.jsの中で「export default store」していない。

モジュールモードでは、storeフォルダにおいた「.js」ファイルの「ファイル名」の名前のモジュールとして名前空間に登録される仕様です。

そして「index.js」は、グローバルの名前空間に登録される・・というわけです。

今回は「index.js」を作らないパターンでやってみます。

state・mutation・actionの定義

storeフォルダに、「infos.js」という名前で作ります。

store/infos.js

export const state = () => ({
  inActive: 0
})

export const mutations = {
  setActiveState (state, inActive) {
    state.inActive = inActive
  }
}

export const actions = {
  toLeftSide ({ commit }) {
    commit('setActiveState', 1)
  },
  toRightSide ({ commit }) {
    commit('setActiveState', 2)
  },
  toDefault ({ commit }) {
    commit('setActiveState', 0)
  }
}

補足です。 

inActiveというstate(状態)を、初期値「0」で定義します。

mutationsで、stateを変更するセッターを定義します。

そして、actionsで、セッターを利用してstateの変更をcommitするメソッドを、3パターン定義しました。

  • state(状態)を「1」に変更する。
  • state(状態)を「2」に変更する。
  • state(状態)を「0」に戻す。

 の3つです。

これを、ページのVueコンポーネントの中で使っていきます。

 

状態管理とヘルパーを使うサンプル

pages/vxdemo.vueファイルを作ります。

4つのボックスを並べます。

そして。

  • state.inActiveが0:すべてのBOXがdark(アイコンが黒)
  • state.inActiveが1:左側のみBOXがActive(アイコンが紫)
  • state.inActiveが2:右側のみBOXがActive(アイコンが紫)

でコントロールします。

まずソースです。

pages/vxdemo.vue

<template>
  <div>
    <div class="container content has-text-centered">
      <NLink to="/">
        <p>ルートのページへ戻ります</p>
      </NLink>
    </div>
    <div class="tile is-ancestor">
      <div class="tile content is-parent is-vertical is-2" />
      <div class="tile content  is-parent is-vertical is-2">
        <article class="tile is-child box">
          <p class="title">
            Icon1
          </p>
          <div v-if="inActive == 1" class="block">
            <b-icon pack="fas" icon="tachometer-alt" size="is-large" type="is-primary" />
            <span class="subtitle">速度計</span>
          </div>
          <div v-else class="block">
            <b-icon pack="fas" icon="tachometer-alt" size="is-large" type="is-dark" />
            <span class="subtitle">・・・</span>
          </div>
        </article>
      </div>
      <div class="tile content is-parent is-vertical is-2">
        <article class="tile is-child  box">
          <p class="title">
            Icon2
          </p>
          <div v-if="inActive == 1" class="block">
            <b-icon pack="fas" icon="tty" size="is-large" type="is-primary" />
            <span class="subtitle">電話</span>
          </div>
          <div v-else class="block">
            <b-icon pack="fas" icon="tty" size="is-large" type="is-dark" />
            <span class="subtitle">・・・</span>
          </div>
        </article>
      </div>
      <div class="tile content is-parent is-vertical is-2">
        <article class="tile is-child box">
          <p class="title">
            Icon3
          </p>
          <div v-if="inActive == 2" class="block">
            <b-icon pack="fas" icon="angry" size="is-large" type="is-primary" />
            <span class="subtitle">怒り顔</span>
          </div>
          <div v-else class="block">
            <b-icon pack="fas" icon="angry" size="is-large" type="is-dark" />
            <span class="subtitle">・・・</span>
          </div>
        </article>
      </div>
      <div class="tile content is-parent is-vertical is-2">
        <article class="tile is-child box">
          <p class="title">
            Icon4
          </p>
          <div v-if="inActive == 2" class="block">
            <b-icon pack="fas" icon="anchor" size="is-large" type="is-primary" />
            <span class="subtitle">イカリ</span>
          </div>
          <div v-else class="block">
            <b-icon pack="fas" icon="anchor" size="is-large" type="is-dark" />
            <span class="subtitle">・・・</span>
          </div>
        </article>
      </div>
      <div class="tile content is-parent is-vertical is-2" />
    </div>
    <div class="container content has-text-centered">
      <p>{{ inActive }}</p>
    </div>
    <div class="container  has-text-centered">
      <b-button type="is-primary" @click="toLeftSide">
        左を有効にする
      </b-button>
      <b-button type="is-primary" @click="toRightSide">
        右を有効にする
      </b-button>
      <b-button type="is-primary" @click="toDefault">
        初期状態にする
      </b-button>
    </div>
  </div>
</template>
<script>
import { mapState, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('infos', ['inActive'])
  },
  methods: {
    ...mapMutations([]),
    ...mapActions('infos', [
      'toLeftSide',
      'toRightSide',
      'toDefault'
    ])
  }
}
</script>

ポイントです。

まず、ヘルパーを使っている<script>~</script>内です。

<script>
    import { mapState, mapMutations, mapActions } from 'vuex'

    export default {
        computed: {
            ...mapState('infos', ['inActive'])
        },
        methods: {
            ...mapMutations([]),
            ...mapActions('infos', [
                'toLeftSide',
                'toRightSide',
                'toDefault'
            ])
        }
    }
</script>
    

ヘルパーをimportして、computedブロック内で「mapState」、methodsブロック内で「mapMutations」「mapActions」を定義しています。

(結局、mapMutationsは空定義になりましたが)

どちらも第一引数は「ファイル名=名前空間名」、第二引数にリストでそれぞれ定義した「state名」や「action名」を記述しています。

こうすることで、<templete></templete>内で、stateやactionをそのままの名前で参照できるようになります。

なので。

<div class="block" v-if="inActive == 1">

のように、stateを参照してIF条件として使えます。

同様にActionも。

<b-button type="is-primary" v-on:click="toLeftSide">左を有効にする</b-button>

のように、メソッドのようにイベントに対応して使えます。

 

おまけ:画面遷移用に内部リンクを定義

画面遷移しても状態が維持されているのを確認するために、以下の内部リンクを追加した「index.vue」を用意しておきます。

pages/index.vue

 
<template>
  <div>
    <div class="container content has-text-centered">
      <img src="~assets/00_nuxt.JPG" alt="image01">
    </div>
    <div class="container content has-text-centered">
      <NLink to="/about">
        <h3>{{ linkname1 }}</h3>
      </NLink>
    </div>
    <div class="container content has-text-centered">
      <NLink to="/cityapi">
        <h3>{{ linkname2 }}</h3>
      </NLink>
    </div>
    <div class="container content has-text-centered">
      <NLink to="/awesome">
        <h3>{{ linkname4 }}</h3>
      </NLink>
    </div>
    <div class="container content has-text-centered">
      <NLink to="/vxdemo">
        <h3>{{ linkname5 }}</h3>
      </NLink>
    </div>
    <div class="container content has-text-centered">
      <a :href="outurl">
        <h3>{{ linkname3 }}</h3>
      </a>
    </div>
  </div>
</template>

<script>
import moment from 'moment'
export default {
  head: {
    title: 'First page'
  },
  data () {
    return {
      outurl: 'https://ja.nuxtjs.org/',
      linkname5: '状態管理(vuex)サンプル',
      linkname4: 'WEBフォントサンプル',
      linkname3: '外部リンク(v-bind)サンプル',
      linkname2: '市区町村(Select)サンプル',
      linkname1: '郵便番号(click)サンプル'
    }
  },
  computed: {
    today () {
      return moment().format('YYYY/MM/DD dddd')
    }
  }
}
</script>


index.vueは、内部リンクで画面遷移だけできればよいです。

 

実行イメージ

プロジェクトフォルダをカレントにして「npm run dev」して、例によって「http://localhost:3000」でindex.vueを表示して、内部リンクで遷移します。

初期は「state==0」なので、すべてのアイコンがdark(黒)です。

f:id:arakan_no_boku:20190531233602p:plain

ここで「左を有効にする」ボタンを押します。

f:id:arakan_no_boku:20190531233736p:plain

stateが1になって、左側のアイコンがActive(紫)になります。

今度は「右を有効にする」ボタンを押します。

f:id:arakan_no_boku:20190531233850p:plain

state==2になるので、今度は右側のアイコンがActive(紫)になります。

ここで、上段の「ルートのページに戻ります」リンクでトップページに一旦戻ります。

f:id:arakan_no_boku:20190831112019p:plain


そして「状態管理のデモ」リンクで、先ほどの画面に戻ります。

f:id:arakan_no_boku:20190531234127p:plain

state==2が維持されていますね。

OKです。

実にシンプルかつ予測可能な形で状態管理ができてます。

Vuexは最初ややこしく感じますが、うまく使えれば、非常に便利です。

今回はこんなところで。

ではでは。