Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

vuex2.xその0003 vuex入門的な感じでToDo管理アプリケーションを作る

参考にさせて頂いたページ

はじめに

APIにリクエストする非同期な処理を想定して、actionsからcommitしています。 実際にAPIとか作ったらもう少し違う実装が適切かもしれないです。

ES2015が使えない環境下でのことを考慮するなど個人的事情からlodashを使用していたり、とはいえ全体的にはES2015で書いていますがそこはそっとスルーでお願いします。

今回のバージョン

  • Vue.js v2.1.10
  • vuex v2.3.0

サンプルソースコード

例として以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<style>
.is-done {
  color: #999999;
  text-decoration: line-through;
}
</style>

<h1>タスク管理アプリケーション</h1>
<div id="app">
</div>

<script src="js/lodash.js"></script>
<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
/**
 * store
 */
const store = new Vuex.Store({
  strict: true,
  state: {
    latestItemId: null,
    items: []
  },
  getters: {

  },
  actions: {
    addItem({commit, rootState}, content) {
      // 実際はここでAPIにリクエストしてDBのレコード追加が成功したらcommit
      const newItemId = rootState.latestItemId + 1;
      commit('addItem', {id: newItemId, content: content, isDone: false});
    },
    toggleItem({commit, rootState}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの更新が成功したらcommit
      const index = _.findIndex(rootState.items, function(o) { return o.id == id; });
      commit('toggleItem', index);
    },
    deleteItem({commit}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの削除が成功したらcommit
      commit('deleteItem', id);
    },
    getItems({commit}) {
      // 実際はここでAPIにリクエストしてDBからタスクリストを取得します
      const items = [
        {id: 0, content: '起きる', isDone: false},
        {id: 1, content: '朝食を食う', isDone: false},
        {id: 2, content: '昼食を食う', isDone: true},
        {id: 4, content: '夕食を食う', isDone: true},
        {id: 5, content: '風呂に入る', isDone: false},
        {id: 10, content: '寝る', isDone: false},
      ];
      const latestItemId = items[items.length - 1].id;
      commit('getItems', {latestItemId: latestItemId, items: items});
    }
  },
  mutations: {
    addItem(state, newItem) {
      state.latestItemId = newItem.id;
      state.items.push(newItem);
    },
    toggleItem(state, index) {
      state.items[index].isDone = !state.items[index].isDone;
    },
    deleteItem(state, id) {
      state.items = _.filter(state.items, function(item) {
        return item.id !== id;
      });
    },
    getItems(state, newState) {
      _.assign(state, newState);
    }
  }
});

/**
 * addItem
 * タスク新規追加フィールドのコンポーネント
 */
const addItem = {
  data() {
    return {
      content: '',
    }
  },
  template: `
    <section>
      <h2>タスクを新しく追加</h2>
      <div>
        <input type="text" v-model="content">
        <input type="submit" value="追加" @click="addItem">
      </div>
    </section>
  `,
  methods: {
    addItem() {
      if (!this.content) return;
      this.$store.dispatch('addItem', this.content);
      this.content = '';
    }
  }
};

/**
 * item
 * タスク一つ分のliタグのコンポーネント
 */
const item = {
  props: ['item'],
  template: `
    <li>
      <span :class="{'is-done': isDone}">id {{item.id}} : {{item.content}}</span>
      <button @click="toggleItem" v-text="buttonLabel"></button>
      <button @click="deleteItem">削除する</button>
    </li>
  `,
  computed: {
    id() {
      return this.item.id;
    },
    isDone() {
      return this.item.isDone;
    },
    buttonLabel() {
      return this.item.isDone ? '未完了に戻す' : '完了にする';
    },
  },
  methods: {
    toggleItem() {
      this.$store.dispatch('toggleItem', this.id);
    },
    deleteItem() {
      if (!confirm("削除しますか?")) return;
      this.$store.dispatch('deleteItem', this.id);
    },
  }
};

/**
 * itemList
 * タスク一覧のulタグのコンポーネント
 */
const itemList = {
  components: {item},
  template: `
    <section>
      <h2>登録されているタスク</h2>
      <ul>
        <item v-for="item in items" v-bind:item="item"></item>
      </ul>
    </section>
  `,
  computed: {
    items() {
      return this.$store.state.items;
    }
  },
  created() {
    this.$store.dispatch('getItems');
  }
};

/**
 * app
 * rootのコンポーネント
 */
const app = new Vue({
  el: '#app',
  store,
  components: {
    addItem,
    itemList,
  },
  template: `
    <div class="app">
      <add-item />
      <item-list />
    </div>
  `
});
</script>

</body>
</html>

Vuexとは

Storeを提供し、以下のようなルールに則った開発をすることでコードの見通しがよくなります。

  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う

正確な情報は公式ドキュメントを参照してください。

個人的まとめ

あくまでも個人的なもので、ES2015などを使用しない環境下のせいもあると思いますが、参考になればと思います。

  • VuexはStoreを提供するもので、コンポーネントは普通にVue.js単体で使っているときと同じ要領で書く
  • rootコンポーネントにstoreオプションを設定すると、子コンポーネントthis.$storeが参照できるようになる
  • コンポーネント内でstoreを参照するときはthis.$storeを使う
  • Storeはstrict: true,を設定した方がいい
  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う
  • mutationsが持っているメソッドは第一引数にstateを受け取る
  • actionsが持っているメソッドは第一引数にcontextまたは分割で{state, rootState, commit, dispatch, getters}を受け取る
  • mutaionを呼ぶときはcommitで、actionを呼ぶときはdispatch
  • getters, modules, plugins というキーもあるけど今回は使っておらず、いずれも大きい規模になるほど有用そう
  • actionのdispatchとpromiseについては「アクション · GitBook

Vuexに限らずVue.jsのコンポーネントのこと

  • コンポーネント内において、Storeが持っているstateの更新が反映されてほしい箇所はcomputedでreturnする
  • コンポーネント内において、親からもらったpropsの更新が反映されてほしい箇所は直接templateに書くか、computedでreturnする
  • アロー関数を使うとthisがコンポーネント自身を参照しないので理解して使う