基礎からメモ: Vue.js CH8-p270 モジュールでストアを分割する
モジュールの機能を使って、ストアを名前空間で分割し管理することができる。
moduleA と moduleB という名前でストアを分割。ストアには modulesオプションに登録する。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const moduleA = { state: { count: 1 }, mutations: { update(state) { state.count += 100 } } } const moduleB = { state: { count: 2 }, mutations: { update(state) { state.count += 200 } } } const store = new Vuex.Store({ modules: { moduleA, moduleB } }) export default store
main.js
に2つのストアを出力するコードを書いて確認してみる。
console.log(store.state.moduleA.count) // -> 1 console.log(store.state.moduleB.count) // -> 2 store.commit('update') console.log(store.state.moduleA.count) // -> 101 console.log(store.state.moduleB.count) // -> 202
AとB2つのモジュールはupdate
という同じ名前のミューテーションを持っている。一度update
にコミットしただけでAとB両方のミューテーションが実行される。
ネームスペースで分けてコミットする
モジュール定義でnamespaced
オプションをtrue
に設定することで、ネームスペース別にコミットさせることが可能。
const moduleA = { state: { count: 1 }, namespaced: true, mutations: { update(state) { state.count += 100 } } } const moduleB = { state: { count: 2 }, namespaced: true, mutations: { update(state) { state.count += 200 } } }
コミットやディスパッチの際、ディレクトリパスのように/
で区切った書き方でネームスペースを付けて呼び出す。
commit('<ネームスペース>/<データ名>')
console.log(store.state.moduleA.count) // => 1 console.log(store.state.moduleB.count) // => 2 store.commit('moduleA/update') console.log(store.state.moduleA.count) // => 101 console.log(store.state.moduleB.count) // => 2 store.commit('moduleB/update') console.log(store.state.moduleA.count) // => 101 console.log(store.state.moduleB.count) // => 202
AとBのupdate
それぞれが別個にコミットされているのがわかる。
ゲッターの呼び出しは次のような形になる。
const moduleA = { state: { count: 1 }, namespaced: true, getters: { count(state) { return state.count } } } const moduleB = { state: { count: 2 }, namespaced: true, getters: { count(state) { return state.count } } }
console.log(store.getters['moduleA/count']) // => 1 console.log(store.getters['moduleB/count']) // => 2
フォームからのデータ変更をネームスペースで分ける
インプットフォームからの入力でメッセージを変更するコードをメッセージAとメッセージBで別々に変えてみる。
[store.js]
const moduleA = { state: { message: '初期メッセージA' }, namespaced: true, mutations: { setMessage: (state, message) => { state.message = message } }, getters: { message: state => state.message } } const moduleB = { state: { message: '初期メッセージB' }, namespaced: true, mutations: { setMessage: (state, message) => { state.message = message } }, getters: { message: state => state.message } }
App.vueのテンプレートにはメッセージをAとBの2つ並べる。算出プロパティで各メッセージを得る場合にネームスペース付きでモジュール別のゲッターを呼ぶ。
[App.vue]
<template> <div id="app"> <p>{{ messageA }}</p> <p>{{ messageB }}</p> <EditForm/> </div> </template> <script> import EditForm from '@/components/EditForm.vue' export default { name: 'App', components: { EditForm }, computed: { messageA() { return this.$store.getters['moduleA/message'] }, messageB() { return this.$store.getters['moduleB/message'] }, } } </script> <style> #app { text-align: center; color: #2c3e50; margin-top: 60px; } </style>
フォームのコンポーネントでは、インプットフォームをAとBの2つ用意し、$refs
の名前、バインドするメッセージ名、イベントハンドラ名などもAとBで切り分ける。
[EditForm.vue]
<template> <div class="edit-form"> <p>メッセージAを変更: <input type="text" ref="inputA" :value="messageA" @input="setMessageA"></p> <p>メッセージBを変更: <input type="text" ref="inputB" :value="messageB" @input="setMessageB"></p> <!-- <input type="text" ref="input"> --> </div> </template> <script> export default { name: 'EditForm', computed: { messageA() { return this.$store.getters['moduleA/message'] }, messageB() { return this.$store.getters['moduleB/message'] }, }, methods: { setMessageA() { this.$store.commit('moduleA/setMessage', this.$refs.inputA.value) }, setMessageB() { this.$store.commit('moduleB/setMessage', this.$refs.inputB.value) } } } </script>
これでインプットフォームのAとBの入力で別々に表示が同期する。
名前空間以外は、呼び出すミューテーション名が同じなので記述を簡略化できる。
ヘルパーでネームスペースを指定する
コンポーネントでヘルパーを使う際に、/
で区切った形でネームスペースを指定するか、第一引数にネームスペースを指定する。
methods: { // `/`で区切った形でネームスペースを指定 ...mapActions({ add: 'moduleA/add'}) // 第一引数にネームスペースを指定 ...mapActions('moduleA', ['add', 'update', 'remove']) }
先の例に使ってみる。データを更新するストアのアクション名はシンプルにset
で統一してみた。第二引数にイベントオブジェクトが渡されることに注意する。
[store.js]
const moduleA = { state: { message: '初期メッセージA' }, namespaced: true, mutations: { setMessage(state, payload) { state.message = payload } }, actions: { set({commit}, ev) { commit('setMessage', ev.target.value) } }, getters: { message: state => state.message } } const moduleB = { state: { message: '初期メッセージB' }, namespaced: true, mutations: { setMessage(state, payload) { state.message = payload } }, actions: { set({commit}, ev) { commit('setMessage', ev.target.value) } }, getters: { message: state => state.message } }
フォームコンポーネントでヘルパーをネームスペース付きで呼び出す形にする。
[EditForm.vue]
<template> <div class="edit-form"> <p>メッセージAを変更: <input type="text" ref="inputA" :value="messageA" @input="setA"></p> <p>メッセージBを変更: <input type="text" ref="inputB" :value="messageB" @input="setB"></p> </div> </template> <script> import { mapActions } from 'vuex' export default { name: 'EditForm', computed: { messageA() { return this.$store.getters['moduleA/message'] }, messageB() { return this.$store.getters['moduleB/message'] }, }, methods: { ...mapActions({setA: 'moduleA/set', setB: `moduleB/set`}), } } </script>
モジュールのネスト
モジュールのmodules
オプションに登録することで、モジュールからさらに別のモジュールを読み込むことができる。
const modyleA = { namespaced: true, modules: { moduleC } }
ディレクトリと同じように、/
で区切った表現で呼び出す。
store.commit('moduleA/moduleC/update')
ネームスペース付きモジュールから外部にアクセス
moduleA
がネームスペースを持っていると仮定すると、ゲッターのentries
オプションに渡される引数で、自分のstate
やgetters
、さらにルートのステートやゲッターが渡される。
getters: { entries(state, getters, rootState, rootGetters) { // 自分自身のitemゲッター( getters['moduleA/item'] ) getters.item // ルートのuserゲッター rootGetters.user } }
アクションでは第一引数のオブジェクトからrootGetters
を受け取ることができる。コミットやディスパッチする場合に、第3引数のroot
オプションをtrue
にする。
actions: { actionType({ dispatch, commit, getters, rootGetters}) { // 自分のupdateアクションをディスパッチ dispatch('update') // ルートのupdateをディスパッチ dispatch('update', null, {root: true}) // ルートのupdateをコミット commit('update', null, {root: true}) // ルートに登録されたモジュールBのupdateをコミット commit('moduleB/update', null, {root: true}) } }
自分自身に登録されている別のモジュールは相対パスのような書き方でアクセスできる。
commit('moduleC/update')
モジュールをファイルごとに分ける
モジュールは個別にファイルに分割して読み込むこともできる。
Vuexモジュールはsrc
ディレクトリ直下に、慣例として「store」とか「vuex」といった名前のサブディレクトリにまとめる。
例えばモジュールAの定義をsrc/store/a.js
に
export default { state: { .... }, mutations: { .... } }
ストアのルートはsrc/store.js
とかsrc/store/index.js
なので、その中で各モジュールを読み込みmodules
オプションに登録する。
import moduleA from '@/store/a.js' import moduleB from '@/store/b.js' const store = new Vuex.Store({ modules: { moduleA, moduleB } })
モジュールの再利用
モジュールもコンポーネントと同様にstate
を関数にすることで再利用できる。
const myModule = { namespaced: true, state() { return { entries: [] } }, mutations: { set(state, payload) { state.entries = payload } }, actions: { load({ commit }, file) { axios.get(file).then(response => { commit('set', response.data) }) } } }
ルートのストアにモジュールを登録する。
const store = new Vuex.Store({ modules: { moduleA: myModule, moduleB: myModule } })
モジュールAとモジュールBはともにmyModule
という同じモジュール定義。呼び出す際にネームスペース付きで、渡すパラメータを替えるなどして呼び出すことで定義を使いまわすことができる。
store.dispatch('moduleA/load', 'path/a.json') store.dispatch('moduleB/load', 'path/b.json')