目次
はじめに
リライトして目次を追加しました。
内容は2019年5月当時のままです。
僕がNuxt.jsの各機能を理解するために、ひとつひとつ確認したことをまとめていこうと考えています。
インストール・環境構築は、以下の手順でできている前提です。
Nuxt.jsを整理する方針
Nuxt.js関連資料やサンプルソースを見ると混乱することが多いのは、Nuxt.jsはVue.jsの機能を包含していて、ひとつのソースの中で、どれがNuxt.js特有の機能で、どれがVue.jsの機能かの区別がつけにくいからだと感じています。
その混在しているイメージを図にしてみると、こんな感じに見えます。
なので、僕はアプローチとして、各技術要素にわけて、一回に少しずつ要素を確認していくことにしました。
分け方としては、以下のようになると考えています。
- Nuxt.js独自の要素ー基本
- Vue.js独自の要素
- BulmaのCSS要素(Burfyのclassは、Bulmaを使っている場合が多い)
- Buefy独自の要素
- Nuxt.js独自の要素ー応用
- テストフレームワーク他モジュールの要素
今回は、Nuxt.js独自の要素ー応用として、状態管理を整理したいと思ってます。
状態管理ライブラリ「Vuex」
状態とは、例えば。
- 認証情報
- 検索クエリ
- フォームの入力情報
- 個々のコンポーネントの動作の状態
などなど・・フロントエンドでページをまたいで管理すべきものです。
従来のWebアプリケーションの場合、画面の書き換え都度状態はリセットされます。
でも。
SPA( single-page application)の様な、画面を部分更新していくタイプのアプリケーションの場合、画面遷移しても状態がリセットされません。
それもあって。
規模が大きくなって管理すべき状態が増えたり、状態を更新するページが多岐にわたるようになると、非常に複雑になります。
複雑になると当然ながら状態管理は困難な仕事になります。
なので。
予測可能な方法によってのみ状態の変異を行うというルールを保証してくれる 状態管理パターン + ライブラリ「Vuex」の出番がでてくる・・というわけです。
Vuexの仕様を整理する
この図につきるわけですが。
状態は「state(ステーツ)」に保存します。
状態は認証情報なら、ログインしているか、していないかみたいなことなので、Vueコンポーネントはこの「state」を参照して処理をすることで、いちいち、自前でログインチェックなどをする必要がなくなります。
この状態「stats」を変更したいときは「Action(アクション)」を使います。
この「Action」が何をするかというと、「mutation(ミューテーション)」をコミットします。
この「mutation(ミューテーション)」のコミットと言われても意味がわかりませんが、ようするに状態「stats」を変更するということです。
なぜかというと、「state(ステーツ)」を変更する唯一の方法が「mutation(ミューテーション)」をコミットすることと決められているからです。
ただし、ちょっと気をつけないといけないのが。
「Action」は非同期の処理を含むことができるのですが、「mutation(ミューテーション)」は同期的である必要があることです・・なんて書きつつ、このへん僕も完全には理解できてませんけど(笑)。
とりあえず。
stateで状態を表し、mutationをコミットするActionを使ってstateを更新する。
そして、Nuxt.jsでは、このstateとかmutationを「store」フォルダで管理する。
このへんを理解しとけばいいかなと思ってます。
Vuexのヘルパー関数
Vuexには、便利なヘルパー関数があります。
今回は以下の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(黒)です。
ここで「左を有効にする」ボタンを押します。
stateが1になって、左側のアイコンがActive(紫)になります。
今度は「右を有効にする」ボタンを押します。
state==2になるので、今度は右側のアイコンがActive(紫)になります。
ここで、上段の「ルートのページに戻ります」リンクでトップページに一旦戻ります。
そして「状態管理のデモ」リンクで、先ほどの画面に戻ります。
state==2が維持されていますね。
OKです。
実にシンプルかつ予測可能な形で状態管理ができてます。
Vuexは最初ややこしく感じますが、うまく使えれば、非常に便利です。
今回はこんなところで。
ではでは。