第4回 – Vuex入門 – パート1
2019.05.27
妻と娘が里帰りして、一人と一匹のわんこで毎日生活している、むつたくです。
ワーワーキャーキャー騒がしかった日々が懐かしいです。
居なくなるとシーン…としてまして、世の一人暮らしをしている方々はよく耐えられるな、
と常々思ってしまいます。
あ、でも!静かでは無いですよ!
わんこのイビキが聞こえてますのでw
さて、今回はVue.jsに話を戻しまして、Vuexについて触れていこうと思います。
Vuexは覚えるのは大変ですが、中〜大規模のプロジェクトでは、真価を発揮すると思いますので
習得していて絶対損は無いです。
小規模では、あまり発揮しませんが、最初から導入しておくことで、改修に追加に…と、
どんどん規模が大きくなってきても、対応出来ますので、必要であれば、導入しておくことを
オススメします。
やること
- Vuexとは?
- 状態管理パターン
- Vuexを使用する (State / Mutations / Actinos)
(1) Vuexとは?
公式の言葉を拝借しますと、
Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。
うーん…、何を言っているのだろう、となりますが、簡単に言いますと、
データの管理を一元化するためのライブラリです。
数えられる程度のコンポーネントで、それを利用したアプリケーションであれば問題ないのですが、
これが数十個になってくると、このコンポーネントでは今はこのデータを扱っていて、あのコンポーネントはあのデータで、あのデータをそのコンポーネントに渡して…とかやってますと、管理が大変ですよね?
親コンポーネントから子、孫、ひ孫コンポーネントに(その逆も然り)データ受け渡しとかのバケツリレー(propsや$emit)も面倒だと思います。
それらを解決してくれるのが、Vuexとなります。
自分がどの階層にいて親コンポーネントはこれで、子コンポーネントはあれで、なんて考えなくて済みます。データを一元管理してくれますので。
StoreのStateと呼ばれるところにデータを集約し、それを介してコンポーネント同士でデータのやり取りをさせます。
どこにいてもVuexライブラリにアクセスしてしまえば、データを取得できますから。便利ですね!
このことから、少数のコンポーネントで構築したアプリは、恩恵をあまり受けることができないかもしれません。
(2) 状態管理パターン
Vuexは、状態管理を単方向データフローで行っており、公式の図解は以下のようになっています。
Actions
API等と非同期通信を行い、データを取得します。
取得後、データをコミットし、Mutationsを呼び出します。
Mutations
Actionsまたは、コンポーネントよりデータがコミットされたら、呼び出されます。
唯一State内のデータを変更することが出来ます。
State
各コンポーネントで使用するデータはここに集約します。
(3) Vuexを使用する
Vue-CLI 3.*系を前提とします。インストール手順は以前のブログで紹介しています。
1 | vue create <ProjectName> |
で行った後、Manually select features を選択すると、使用できるものが一覧化されてますが、この中にVuexがありますので、選択しましょう。
後から追加する場合は、
1 | vue add vuex |
を実行します。
準備が整いました。
<ProjectName>ディレクトリ直下にsrcディレクトリがあると思いますが、その中にstore.jsがあります。
ない方は、store.jsを作成するなり、storeディレクトリを作成後、index.jsを作成するなりしてください。
store.jsの中身は以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 | //store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: {}, mutations: {}, actions: {} }) |
5行目の
1 | Vue.use(Vuex) |
は、書き忘れないようにしてください。このアプリケーション内でVuexを使用しますと言う宣言になります。Vuexが使用できない場合は、描き忘れが無いか確認してください。
ここから簡単な使用方法をカウンターアプリを作成しつつ説明します。
Stateについて
Storeのデータ群をここ(State)で保持
Mutationsでのみ変更される
store.jsを以下のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 | import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { counter: 0 // ★これを追加 }, mutations: {}, actions: {} }) |
componentsディレクトリにcounter.vueを作成し、以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 | //counter.vue <template> <div class="counter"> {{ $store.state.counter }} </div> </template> <script> export default { name: "counter" }; </script> |
$store.state.counter でStore内のStateに登録した変数[counter]にアクセスしています。
次にviewsディレクトリにHome.vueを作成して、以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //Home.vue <template> <div class="home"> <counter></counter> <!-- ★これを追加 --> </div> </template> <script> import counter from "@/components/counter.vue"; // ★これを追加 export default { name: "home", components: { // ★components{...}を追加 counter } }; </script> |
これでcounter.vueが表示されたと思います。この状態だと、「0」が表示されています。
試しに、store.jsのcounterの数値を変更してみてください。
初期値が変更した値になっているはずです。
もう一つコンポーネントを作成してみましょう。reset.vueとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 | //reset.vue <template> <div class="reset"> <p>値のリセット</p> {{ $store.state.counter }} <br> </div> </template> <script> export default { name: "reset" }; </script> |
ついでにcounter.vueからreset.vueが表示できるように、counter.vueを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //counter.vue <template> <div class="home"> {{ $store.state.counter }} <br> <hr> <reset></reset> <!-- ★これを追加 --> </div> </template> <script> import reset from "@/components/reset.vue"; // ★これを追加 export default { name: "counter", components: { // ★components{...}追加 reset } } </script> |
これでどちらのページからもcounter.vueが閲覧できるようになりました。実際にlocalhostへアクセスしてみてください。
propsや$emitを使わずに値をそれぞれのコンポーネントで表示できたかと思います!
Mutationsについて
Storeを変更する
Storeのデータをコミットすることで呼び出される
同期処理を行う
実際にStoreをコミットしてMutationsを呼び出してみましょう。
store.jsを以下のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { counter: 0 }, mutations: { increment (state) { // ★increment{...}追加 // インクリメント用関数 state.counter++ }, reset (state) { // ★reset{...}追加 // カウンターリセット用関数 state.counter = 0; } }, actions: {} }) |
これでstoreのcounterの状態を管理する関数が作成できました。関数名の通り、[increment]ではカウントアップを、[reset]ではカウンタを0に戻します。
また、各関数(ハンドラ)は第一引数にVuexの状態を取得します。なので、[increment]も[reset]も第一引数に[state]を定義しています。
引数名は当たり前ですが、何でも構いません。ここではあえてstoreと命名しているだけです。
ちなみに第二引数もあります。通称ペイロードと呼ばれていますが、任意の値を渡したいときに使用します。基本的にオブジェクトで渡します。
※使い方はMutationsの最後の方で。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | //store.js // ...省略... mutations: { increment (state, payload) { // インクリメント用関数 state.counter += payload.incrementval }, reset (state, payload) { // カウンターリセット用関数 state.counter = payload.resetval; } }, actions: {} }) |
次に、mutationsに登録した関数をそれぞれのコンポーネントから呼び出せるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //counter.vue <template> <div class="home"> {{ $store.state.counter }} <br> <button type="button" name="button" @click="increment">カウントアップ</button><!-- ★これを追加 --> <br> <hr> <reset></reset> </div> </template> <script> import reset from "@/components/reset.vue"; export default { name: "counter", components: { reset }, methods: { // ★methods:{...}追加 increment() { this.$store.commit('increment'); } } } </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //reset.vue <template> <div class="reset"> <p>値のリセット</p> {{ $store.state.counter }} <br> <button type="button" name="button" @click="reset">値リセット</button> <!-- ★これを追加 --> </div> </template> <script> export default { name: "reset", methods: { // ★methods:{...}追加 reset() { this.$store.commit('reset'); } } }; </script> |
ここまで出来ましたら、再度localhostにアクセスしてみましょう。各コンポーネントにボタンがそれぞれ追加されてます。
試しにcounter.vue側のボタン[カウントアップ]をクリックします。すると、counter.vueと、reset.vueのcounter変数値がインクリメントされてます。
ついでにreset.vue側のボタン[リセット]ボタンもクリックしてみます。どちらのコンポーネントのcounter変数値が初期値に戻ってると思います。
無事、propsや$emitに頼らず、コンポーネント間での値参照に成功しました。
大事なのは、storeで定義したデータを更新させるのはstore内でしか行えず、storeの関数をコールするには、commitします。その際にコールするmutationsの関数名を定義してあげます。そうすることで、mutationsによる値更新を行うことができるようになります。
下記のコードのように、直接store値を変更したり、mutations関数をコールすると、エラーになるので、注意してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //counter.vue //...省略... <script> import reset from "@/components/reset.vue"; export default { name: "counter", components: { reset }, methods: { comp_increment() { //見分けるために関数名変更 increment(); //または this.$store.counter++; } } } </script> |
また、mutationsの関数に任意の値を渡したい場合、commitの第二引数を以下のように設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //counter.vue //...省略... <script> import reset from "@/components/reset.vue"; export default { name: "counter", components: { reset }, methods: { increment() { this.$store.commit('increment', { incrementval: 2 }); // ★ここを変更 } } </script> |
こうすることによって、任意の数値分インクリメント(ここでは2ずつ)されるようになりました。
さらに任意の値を渡したい場合は、オブジェクトの中にどんどん追記してあげれば、コールした関数先でも参照することが出来ます。
Actions
commitして、mutationsをコールする
非同期処理を行う
Promiseをリターンする
stateは変更しない
dispatchで呼び出される
mutationsと似ていますが、Actionsはdispatchでコールされるのと、非同期処理を行い、それをPromiseでリターンするという役割を担っています。
では、実際にコードを書いてみましょう。…これ非同期にする必要なくね?とかは受け付けません!
まずは、actionsをdispatchするコンポーネントを作成します。countup.vueとしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //countup.vue <template> <div id="timer"> <button type="button" name="button" @click="countup">重たい処理</button> </div> </template> <script> export default { name: 'timer', methods: { countup() { //actionsをコール(Promiseがリターンされるので、ここではこの形式) this.$store.dispatch('act_countup', { timer: 2000 }) .then(() => { console.log('countup complete'); }) .catch(() => { console.log('countup failed...'); }); } } } </script> |
ついでにcountup.vueをcounter.vueに組み込みましょう。それから、store.jsのacrionsの箇所も変更しちゃいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //counter.vue <template> <div class="home"> {{ $store.state.counter }} <br> <button type="button" name="button" @click="increment">カウントアップ</button> <br> <hr> <reset></reset> <br> <hr> <countup></countup> <!-- ★ここ追加 --> </div> </template> <script> import reset from "@/components/reset.vue"; import countup from "@/components/countup.vue"; // ★ここ追加 export default { name: "counter", components: { reset, countup // ★ここ追加(1行上のカンマも忘れずに) }, methods: { increment() { this.$store.commit('increment', { val: 2 }); } } } </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 | //store.js //...省略... actions: { act_countup({ state }, payload) { return new Promise((resolve, reject) => { setTimeout(() => { state.counter++; resolve(); }, payload.timer); }) } } }) |
ここまで出来ましたら、localhostを再起動します。[重たい処理]ボタンが追加されてます。
クリックすると、dispatchによりactionsに定義した関数[act_countup]がコールされ、2秒後、ログに「countup complete」が出力されてると思います。
非同期処理なので、その間もカウントアップ、値リセットは動きます(使い方が違いますが…)
actionsもmutationsと同様に、第一引数及び、第二引数しかありません。第二引数はmutationsと同じになりますが、第一引数が異なります。
contextと呼ばれますが、オブジェクトになっており、以下6種類あります。
state | `store.state` と同じか、モジュール内にあればローカルステート |
getters | `store.getters` と同じか、モジュール内にあればローカルゲッター |
rootState | `store.state` と同じ。ただしモジュール内に限る |
rootGetters | `store.getters` と同じ。ただしモジュール内に限る |
commit | `store.commit` と同じ。mutationsをコールする時に使用。 |
dispatch | `store.dispatch` と同じ。actionsをコールする時に使用。 |
ローカル〇〇とか出てきますが、同じjsファイルで定義している場合、そこを参照することになります。今回も、これに該当します。同じjsファイルにstateもmutationsもactionsもありますので。
actions側ではこんな感じになります。
1 | act_countup({state, getters, rootState, rootGetters, commit, dispatch}, payload) { ... } |
さて、このように非同期処理をactionsでは書けますが、別にmutationsで書くこともできると思います。ただ、actionsで書いた方が融通が利きます。
なぜなら、コードでは書いてませんが、別のactionsへdispatchが出来たり、mutationsのcommitもできるので、処理を柔軟に組み合わせることが出来ます。
ちなみに、async/awaitシンタックスを書くことも出来るので同期的な処理も行えます。
まとめ
Vuexは、Vue.jsのライブラリの一つで状態管理をしてくれる。つまり、データをStoreで一元管理してくれる。
状態管理には3種類あり、state / mutations / actions である。
stateは、Store内のデータを保持してくれる。ここでは、データの変更は出来ない。
mutationsは、stateで保持しているデータの変更を行ってくれる。変更するときはcommitでコールする。
actionsは、stateで保持しているデータの変更を行ってくれる。変更するときはdispatchでコールする。
mutationsと似ているが、最大の違いは、非同期処理を行い、Promiseをリターンすることである。
詳しくは公式ドキュメント(日本語版)がありますので、そちらを参照してください。
おわりに
今回は、Vuexのこと、StoreのState、Mutations、Actionsについて簡単ですが、紹介しました。
少しでもVuexについて理解が深まれば、幸いです。
次回は、Vuexのgetterや、モジュールについて紹介していきます。
それでは、またの機会に!
↓↓↓ぜひチェックしてください
~提供中のヒューマンセンシング技術~
◆人物検出技術
歩行者・来店者数計測やロボット搭載も
https://humandetect.pas-ta.io
◆視線検出技術
アイトラッキングや次世代UIに
https://eyetrack.pas-ta.io
◆生体判定技術
eKYC・顔認証のなりすまし対策を!
https://bio-check.pas-ta.io
◆目検出技術
あらゆる目周りデータを高精度に取得
https://pupil.pas-ta.io
◆音声感情認識技術
会話から怒りや喜びの感情を判定
https://feeling.pas-ta.io
◆虹彩認証技術
目の虹彩を利用した生体認証技術
https://iris.pas-ta.io