WEBOPIXEL

簡単なTODOアプリで Vue + Vuex を学んでみよう

Posted: 2017.05.09 / Category: javascript / Tag: 

前回、Vue.jsコンポーネント間の値のやりとりについてをやりましたが、この方法だとコンポーネントが増えていくといわゆるバケツリレー状態になり管理が難しくなります。
そんなときは状態管理パターン+ライブラリであるVuexを使用すれば解決してくれるみたいですよ。

Sponsored Link

vue 2.2.1
vuex 2.3.1
でお送りします。

準備

今回はvue-cliのwebpackで試してみます。
インストールしていない場合は下記で vue-cli をグローバルにインストール。

$ sudo npm install -g vue-cli

これでvueコマンドが使えるので、プロジェクトを作成しカレントにします。

$ vue init webpack-simple vuex-test
$ cd vuex-test

npmパッケージをプロジェクトにインストールします。
vuexとbabel-plugin-transform-object-rest-spreadを追加でインストールしましょう。

$ npm install
$ npm install vuex -S
$ npm install babel-plugin-transform-object-rest-spread -D

babel-plugin-transform-object-rest-spread をインストールすることでJavaScriptの新しい機能を使用することができます。
ルートにある.babelrcpluginsに指定することで、この機能を使えるようにしましょう。

.babelrc

{
  "presets": [
    ["latest", {
      "es2015": { "modules": false }
    }]
  ],
  "plugins": ["transform-object-rest-spread"]
}

IE11対応

VuexはPromiseというのを使用していますが、IE11は対応していません。
これはbabel-polyfillをインストールすることで動くようになります。

npm install babel-polyfill -D

webpack.config.jsentryの部分を下記のようにします。

webpack.config.js

entry: ["babel-polyfill", './src/main.js'],

これでVue+Vuexを使用する準備が整いました。
下記コマンドを実行すると自動的にブラウザが起動しVueのロゴが表示されるのを確認してください。

$ npm run dev

Vuexデータの流れ

下図はVuex公式ドキュメントで説明されている図です。

Vuexデータの流れ

細かい部分は置いといて、大きい流れは、
Vue ComponentsからActionsを実行する。
ActionsからMutationsを実行する。
MutationsStateを更新する。
Vue Componentsに反映される。
という流れになります。

コンポーネントだけで作成するTODOアプリ

簡単なTODOアプリをサンプルを作成してみます。
最初はVuexを使わないでコンポーネントだけで作成してみます。

src/App.vue

<template>
  <div id="app">
      <ul>
          <item v-for="item in items" :item="item"></item>
      </ul>
      <div>
          <input type="text" v-model="inputTitle">
          <button @click="addItem">追加</button>
      </div>
  </div>
</template>
<script>
import Item from './Item.vue';
export default {
    name: 'app',
    components: { Item },
    data () {
        return {
            inputTitle: "",
            items: [
                {is_do: false, title: 'タスク1'},
                {is_do: true, title: 'タスク2'},
                {is_do: false, title: 'タスク3'}
            ]
        }
    },
    methods: {
        addItem() {
            this.items.push({
                title: this.inputTitle,
                is_do: false
            });
        }
    }
}
</script>

src/Item.vue

<template>
    <li v-bind:class="{ 'is-do': item.is_do }" @click="done(item)">{{ item.title }}</li>
</template>
<script>
export default {
	props: ['item'],
	methods: {
		done(item) {
			item.is_do = !item.is_do;
		}
	}
}
</script>
<style lang="scss" scoped>
li {
	cursor: pointer;
}
li.is-do {
	text-decoration: line-through;
}
</style>

機能としては、liをクリックするとis-doというクラスが付与されチェックされあた状態となります。
追加ボタンをクリックすると新しいタスクが追加されます。
この部分は単純なVue.jsの作りなので説明は省略します。

このくらいならコンポーネント分ける必要ないのですが、一つだけだとVuexのサンプルとしてわかりにくいのかなと。

Storeファイルのstateのデータを表示する

まずは、Storeファイルにstateを作成して、その内容をコンポーネントで表示するという部分をだけを作成してみます。
src/storeディレクトリにindex.jsというファイル名で作成します。

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
    state: {
        items: [
            {is_do: false, title: 'タスク1'},
            {is_do: true, title: 'タスク2'},
            {is_do: false, title: 'タスク3'}
        ]
    }
});

Vue、Vuexをインポートしたら、Vue.use(Vuex)でVuexを使用できる状態にします。
いくつかVuexを使用するための記述がありますが、基本的にはsrc/App.vueにあったdataをそのままstateに書いているだけですね。

次にコンポーネントで設定したstateを表示してみましょう。
Storeのインポートはmain.jsで行います。

src/main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store';

new Vue({
    store,
    el: '#app',
    render: h => h(App)
})

src/App.vueitemsの中をthis.$store.state.itemsに変更します。

src/App.vue

import Item from './Item.vue'

export default {
    name: 'app',
    components: {
        Item
    },
    data () {
        return {
            items: this.$store.state.items,
            inputTitle: ""
        }
    }
}

これでストアからstateのデータを読み込み表示することができました。

statemapStateを使用することで記述を簡略化することができます。

src/App.vue

import { mapState } from 'vuex'
import Item from './Item.vue'

export default {
    name: 'app',
    components: {
        Item
    },
    data () {
        return {
            inputTitle: ""
        }
    },
    computed: {
        ...mapState(['items'])
    }
}

インポートでmapStateを読み込んだら。computed...mapState(['items'])を追記します。これでdetaitemsを設定する必要がなくなります。

「…」が奇妙な感じですが「オブジェクトスプレッド演算子」という機能です。

main.jsStoreをインポートしておけば、どのコンポーネントからもデータを取り出せるので便利ですね。

mutation-types.jsの作成

stateから取得する方法がわかったので次はActionsMutationsを作成して、stateを更新できるようにしてみましょう。

「TODOをチェックする(DONE_TASK)」と「TODOを追加する(ADD_TASK)」というアクションを作成していきます。
早速アクションを書き始めてもいいのですが、その前にmutation-types.jsを作成します。
別ファイルにすることでストアとコンポーネントで使えるようにします。

src/store/mutation-types.js

export const ADD_TASK = 'ADD_TASK';
export const DONE_TASK = 'DONE_TASK';

定数に文字列を代入しているだけですが、こうすることでlintやコードの補間ができたりと便利らしいです。

ストアで使えるようにインポートしておきましょう。

src/store/index.js

import * as types from './mutation-types'

Actionsの作成

今度こそアクションを作成していきましょう。
src/store/index.jsstateの下に下記を追記します。

src/store/index.js

actions: {
	[types.ADD_TASK] ({ commit }, title) {
		let newItem = {
			title: title,
			is_do: false
		}
		commit( types.ADD_TASK, {
			data: newItem
		})
	},
	[types.DONE_TASK] ({ commit }, item) {
		commit( types.DONE_TASK, {
			data: item
		})
	}
}

関数名の部分が配列みたいになってまた奇妙な感じになってますが、こうすることで定数を関数名として使用できるようになります。

commitを実行することでミューテーションが実行されます。
ここでは値を渡したかったので第2引数に設定しています。

今回のような値を渡すだけならアクションは必要ないと思いますが、今後サーバーとのやり取りをする場合はこのActionsに記述していくことになります。

commitの部分は次のような書き方もできます。

commit({
	type: types.DONE_TASK,
	data: item
})

Mutationsの作成

先ほど作成したActionsの下に下記を追記します。

src/store/index.js

mutations: {
	[types.ADD_TASK] (state, payload) {
		state.items.push(payload.data);
	},
	[types.DONE_TASK] (state, payload) {
		let index = state.items.indexOf(payload.data)
		state.items[index].is_do = !payload.data.is_do
	}
},

アクションで設定した引数はpayloadに入っています。
dataプロパティに設定したのでpayload.dataで取り出せます。
あとは普通にstateの配列を書き換えているだけですね。

Componentsで使う

最後にコンポーネントからアクションを実行してステートを更新してみましょう。

まずはTODOをチェックするアクションです。
これはItem.vueで設定してましたね。

src/Item.vue

<template>
    <li v-bind:class="{ 'is-do': item.is_do }" @click="DONE_TASK(item)">{{ item.title }}</li>
</template>

<script>
import { mapActions } from 'vuex'
import * as types from './store/mutation-types';

export default {
    props: ['item'],
    methods: {
        ...mapActions([
            types.DONE_TASK
        ])
    }
}
</script>

ステートと同じでアクションにはmapActionsというヘルパーが用意されています。
methodsではこれを使用して、使いたいアクションtypes.DONE_TASKを指定します。
あとはタグの部分の@clickDONE_TASK(item)に変更しましょう。

同じ要領でApp.vueも作成してみましょう。

src/App.vue

<template>
  <div id="app">
      <ul>
          <item v-for="item in items" :item="item"></item>
      </ul>
      <div>
          <input type="text" v-model="inputTitle">
          <button @click="ADD_TASK(inputTitle)">追加</button>
      </div>
  </div>
</template>

<script>
import { mapState,mapActions } from 'vuex'
import * as types from './store/mutation-types';
import Item from './Item.vue'

export default {
    name: 'app',
    components: {
        Item
    },
    data () {
        return {
            inputTitle: ""
        }
    },
    computed: {
        ...mapState(['items'])
    },
    methods: {
        ...mapActions([
            types.ADD_TASK
        ]),
    }
}
</script>

いろいろ間違っているところがあると思いますが、以上になります。
複雑なとこもありますが、一つ一つ見ていくことでなんとなくわかってきたような気がしないでもないです。

作ったものはGithubに置いてあります。
参考サイト
Vuex Introduction
Vue.js + Vuexで開発をしてみよう!