Techvenience

Technology × Convenience - Vue / React / Next / Nuxt / ChatGPTなどのIT技術がもたらす便利さをお伝えします。最近はChatGPTなどのAI技術を使ってブログを書いています。

【Vue.js】Vuexでストアオブジェクトをモジュールに分割する【Vuex】

【Vue.js】Vuexでストアオブジェクトをモジュールに分割する【Vuex】

f:id:duo-taro100:20160218004611p:plain

【前回】Vuexの基本的な使い方
www.sky-limit-future.com


Vuexを使用する際に、単一のストアオブジェクトを使っているとストアオブジェクトが膨れ上がってきてしまいます。
すると管理が大変だったり、用途が不透明になったりとVuexのよくない面が出てきますので、特定の単位ごとにモジュールに分割するのが推奨されています。
今回は、モジュールでの分割を解説していきたいと思います。

モジュールとは

公式ドキュメントです。
https://vuex.vuejs.org/ja/guide/modules.html

前述した通りで、一つのストアオブジェクトにアプリケーションの状態を全て保持すると、ストアオブジェクトが膨大になりすぎてしまいます。
そのため、特定の単位でストアオブジェクトを分割しますが、その分割したものをモジュールといいます。
例えば、ランキングを集計するアプリケーションを作成していて、現在「寿司ランキング」と「肉ランキング」が存在するとしましょう。

f:id:duo-taro100:20180703154700p:plain

モジュールでの分割を用いない場合は、一つのstateに「寿司ランキング」と「肉ランキング」を保持する必要があります。
一方、モジュールを使って分割する場合は「寿司モジュール」と「肉モジュール」を用意することで、そのモジュールごとにランキングを保持することができます。
上記の例だとあまりメリットに感じることはできないですが、より大きなアプリケーションになっていくにつれてモジュールで分割することによって、大きなメリットを感じることができます。

モジュールを使わない実装

まずはモジュールを使わずにサンプルを作ってみたいと思います。
cssは省略します。

See the Pen vuex-no-module by duotaro (@duotaro100) on CodePen.


html
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Vuexでモジュールを使わないパターン</title>
  <!-- 全てCDNで導入 -->
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
  <script src="https://unpkg.com/vuex"></script>
</head>
<body>
	<main id="app" class="content">
	  <!-- component表示箇所 -->
	  <router-view></router-view>
	  <!-- ルーティング表示 -->
      <router-link to="/">home</router-link>
	  <router-link to="/page1">page1</router-link>
	  <router-link to="/page2">page2</router-link>
	  <router-link to="/page3">page3</router-link>
	</main>
</body>
</html>
store(Vuex)

Vuexのストアオブジェクトです。
今回はモジュールでの分割をしていません。
stateやmutationsが若干煩雑になっています。後ほどモジュール分割した時と比較してみて下さい。

/********************
         vuex
********************/
const store = new Vuex.Store({
  state: {
    page1Title: 'page1',
    page2Title: 'page2',
    page3Title: 'page3',
    page1Description: 'This is page1.',
    page2Description: 'This is page2.',
    page3Description: 'This is page3.',
    page1Num: 1000,
    page2Num: 600,
    page3Num: 800,
  },
  mutations: {
    page1Increment(state) {
      state.page1Num++
    },
    page2Increment(state) {
      state.page2Num++
    },
    page3Increment(state) {
      state.page3Num++
    }
  }
})
component

表示に使うコンポーネントを3つ用意しました。

/********************
     component
********************/
Vue.component('page1', {
  template: 
    '<div>' +
      '<p>title:  {{this.$store.state.page1Title}}</p>' +
      '<p>descripttion: {{this.$store.state.page1Description}}</p>' +
        '<p>num: {{this.$store.state.page1Num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page1Increment');
    }
  }
})

Vue.component('page2', {
  template: 
    '<div>' +
      '<p>title:  {{this.$store.state.page2Title}}</p>' +
      '<p>descripttion: {{this.$store.state.page2Description}}</p>' +
        '<p>num: {{this.$store.state.page2Num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page2Increment');
    }
  }
})

Vue.component('page3', {
  template:  
    '<div>' +
      '<p>title:  {{this.$store.state.page3Title}}</p>' +
      '<p>descripttion: {{this.$store.state.page3Description}}</p>' +
        '<p>num: {{this.$store.state.page3Num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page3Increment');
    }
  }
})
router

vue-routerの定義です。
Homeコンポーネントはここで定義定義しています。

/********************
     vue-router
********************/
const Home =  { template: '<div>This is home</div>' }
const Page1 = { template: '<page1></page1>' }
const Page2 = { template: '<page2></page2>' }
const Page3 = { template: '<page3></page3>' }

const routes = [
  { path: '/', component: Home },
  { path: '/page1', component: Page1 },
  { path: '/page2', component: Page2 },
  { path: '/page3', component: Page3 }
]

const router = new VueRouter({
  routes
})
vueインスタンス

用意したrouterやstoreをインジェクトします

var app = new Vue({
  el: '#app',
  router,
  store
})

モジュールを使った実装

さて、実際にモジュールを使った実装を見てみましょう。

See the Pen vuex-use-module by duotaro (@duotaro100) on CodePen.

変更が必要な、componentとVuex(store)だけ記載します。

store(Vuex)

Vuexのストアオブジェクトです。
モジュールで分割しました。今回はページごとに分割しています。
分割単位はアプリケーションの規模によって決めていただくのがいいと思います。
モジュール分割しない場合と比べると、stateやmutationsがスッキリしました。

/********************
         vuex
********************/
const modulePage1 = {
  // モジュールごとにmutationsなどを分割できる(同じ名前でも)
  namespaced: true,
  state: { 
  	title : 'page1',
  	description : 'This is page1.',
  	num : 1000
  },
  mutations: { 
  	increment(state) {
      state.num++
    }
  }
}

const modulePage2 = {
  // モジュールごとにmutationsなどを分割できる(同じ名前でも)
  namespaced: true,
  state: { 
  	title : 'page2',
  	description : 'This is page2.',
  	num : 600
  },
  mutations: { 
  	increment(state) {
      state.num++
    }
  }
}

const modulePage3 = {
  // モジュールごとにmutationsなどを分割できる(同じ名前でも)
  namespaced: true,
  state: { 
  	title : 'page3',
  	description : 'This is page3.',
  	num : 800
  },
  mutations: { 
  	increment(state) {
      state.num++
    }
  }
}

const store = new Vuex.Store({
  modules: {
  	page1 : modulePage1,
  	page2 : modulePage2,
  	page3 : modulePage3
  }
})

「namespaced: true」とすることで、モジュールごとにmutationなどを区別することができます。
一度、「namespaced: true」をコメントアウトして動かして見ると、page1〜page3のボタン、どれを押しても各モジュールが持つnumを更新してしまします。
このように共通して更新しに行くという目的で使う場合もありますが、モジュールごとに管理する方がわかりやすくてオススメです。

component
/********************
     component
********************/
Vue.component('page1', {
  template: 
    '<div>' +
      '<p>title:  {{this.$store.state.page1.title}}</p>' +
      '<p>descripttion: {{this.$store.state.page1.description}}</p>' +
        '<p>num: {{this.$store.state.page1.num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page1/increment');
    }
  }
})

Vue.component('page2', {
  template: 
    '<div>' +
      '<p>title:  {{this.$store.state.page2.title}}</p>' +
      '<p>descripttion: {{this.$store.state.page2.description}}</p>' +
      '<p>num: {{this.$store.state.page2.num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page2/increment');
    }
  }
})

Vue.component('page3', {
  template:  
    '<div>' +
      '<p>title:  {{this.$store.state.page3.title}}</p>' +
      '<p>descripttion: {{this.$store.state.page3.description}}</p>' +
        '<p>num: {{this.$store.state.page3.num}}</p>' +
      '<button @click="increment">ボタン</button>' +
    '</div>',
  methods : {
    increment : function(){
      this.$store.commit('page3/increment');
    }
  }
})

先ほどとの違いはstateの呼び出し方法と、commit(mutationsの呼び出し)の方法です。
例えばpage1のモジュールのstateを呼び出す方法は、

this.$store.state.page1

とします。
commit(mutationsの呼び出し)は、「モジュール名/ミューテーション名」で行います。
page1のincrementを呼び出すには以下のようにします。

this.$store.commit('page1/increment');

ここでは、commitをコンポーネント内で実行していますが、本来であればactionsを経由して行うのが一般的です。
上記の例ではあまりモジュールを使うメリットはないですが、サービスの規模などを考えてモジュール分割を取り入れるのもいいかと思います。

次回は、今回説明しなかったactionsやgettersを使ってより便利なVuexの使い方を解説していきたいと思います。