"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

状態管理ライブラリ「Vuex」(store)とヘルパー関数を使ってみる/NUXT自己流チュートリアル(1)-8

今回は、 「Vue.js アプリケーションのための 状態管理パターン + ライブラリです」などと紹介されているVuexとヘルパの基本的な使い方を確認しつつ、画面遷移しても状態を表すデータを保持できていることを確認するデモを作ってみます。

いちおう、自己流チュートリアルの第一シリーズは一旦締めます。

f:id:arakan_no_boku:20190509225706p:plain

なお、この記事は「NUXT自己流チュートリアル(1)」として続き物で書いている記事の8回目です。

1回目から続けて読んでもらえることを想定して書いていますのでご了承ください。

arakan-pgm-ai.hatenablog.com

 

状態管理についてのおさらいから

 

状態とは、例えば。

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

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

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

でも。

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

それもあって。

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

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

なので。

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

vuex.vuejs.org

 

ざっくりとVuexの仕様を整理する

 

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

f:id:arakan_no_boku:20190529223427p:plain

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

この「state(ステーツ)」を変更する唯一の方法が「mutation(ミューテーション)」をコミットすることと決まってます。

ただし、この「mutation(ミューテーション)」は同期的である必要があります。

Vueコンポーネントはこの「state」を参照して処理をします。

で・・結果、状態を変更したいときは「Action(アクション)」を「dispatch(ディスパッチ)」します。

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

この「Action」は非同期の処理を含むことができます。

なんか、冗長に見えるのですが、「state」をクラスメンバ、「mutation」をセッター、「Action」がキューだと考えれば、複数スレッドの処理とかでは普通にする形ですから、複数のページから非同期に更新される「状態」を予測管理しやすい一方通行の更新で管理するには、当然の仕組・・じゃないのかなと思ってます。

で・・。

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

 

簡単なチュートリアルをやってみます

 

前提など

 

自己流チュートリアル(1)で「前提」と「チュートリアルの準備」として書いていることは、できている必要があります。

つまり。

インストール・環境設定・テスト専用Chromeショートカットの作成うんぬんですね。

まだの方は、下記記事を先に参照して、環境設定をお願いします。

arakan-pgm-ai.hatenablog.com

あとは、続き物なのでNUXT自己流チュートリアル(7)までのソースを一部引き継ぐ形でやってますので、逐次参照ください。

arakan-pgm-ai.hatenablog.com

 

今回使うVuexのモードについて

 

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

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

です。

でも、クラッシックモードはもう「非推奨」で「廃止予定」ですから、実際には「モジュールモード」一択です。

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

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

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

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

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

 

ヘルパーも使います

 

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

vuex.vuejs.org

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

  • mapState
  • mapMutations
  • mapActions

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

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

 

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コンポーネントの中で使っていきます。

 

vxdemo.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は最初ややこしく感じますが、うまく使えれば、非常に便利です。

今回はこんなところで。

ではでは。

 

追記

 

この回終了後の状態のソースコード(追加・修正したもののみ)を以下におきました。

github.com