WIP

vuexの仕組み / 書きかけ

created at 2017.02.14
updated at 7ヶ月前 ago
vsanna / public vue.js

結論: 公式vuexドキュメント が最もわかりやすいのでそれを読むといい。 loading 公式vuexドキュメント loading

vuexとは

Fluxの仕組みをvue.jsに導入するツール。

Fluxとは

データの流れを一方向にする設計思想、設計そのもののことで、

  • ユーザーの操作
  • データの変更
  • 見た目への反映

という流れを固く守らせる。

基本的な流れ

  1. storeを作成するstore/index.jsを書く
  2. vueのroot インスタンスを画面にマウントする箇所(new Vue({}) する箇所)でそのstoreを読み込む
  3. あとはroot インスタンス配下にcomponentを配置して、rootインスタンスやcomponentからそのstoreを参照しつつ動くようにコードを書いていく
    • 具体的にはstoreのmutation / actionをrootインスタンスやcomponentから叩いてstoreを更新させる

vuexの用語確認

  • store
    • "single source of truth"
    • アプリケーション全体の「状況 = データ」を唯一保有するオブジェクト
      • ローディング中だとか、like済み/like前、とか。
    • 画面上の全てのコンポーネントのプロパティはこのストアが保有する「状況 = データ」を参照するにすぎないというのが理想
    • state, mutation, action, getterを司る
  • state
    • storeが保有する「状況 = データ」のこと
  • mutation
    • 「操作」の意。
    • storeのstateを変更する唯一の手段。stateのsetterみたいなもの。
    • actionと比較して絶対に同期的な処理だけを扱うことが特徴
    • 各コンポーネントからはthis.$store.commit('mutation-key') で呼び出す(が、多分あんまりかかないのでは)
  • action
    • mutationに入れられないような非同期な処理を保有する
    • 各コンポーネントからはthis.$store.dispatch('action-key')で呼び出す(こっちはよく書く)

simpleなstore

const store = new Vue.store({
  state: {
    count: 0,
  }, 
  getters: { // getter
    doubledCount () {
      return state.count * 2
    }
  },
  mutations: { // setterのようなもの
    increments(state){ // mutationはstateを第一引数に受け取る。後述
      state.count ++ // stateの更新
    }
  }
})

console.log(state.count) // 0
store.commit('increments') // commitで実行
console.log(state.count) // 1

ここから先はstate, getters, mutations, actions についてまとめる

state

  • store/index.jsでstateを初期値とともに設定し、vue store インスタンスを作る。
    • 大抵の場合、store/state.jsなどにかきだして、store/index.jsはそれらをimportしてまとめるだけの処理を行なう
    • gemっぽい
  • それをrootとなるvueインスタンスを定義する場所でvueインスタンスに結びつける
  • そのroot vueインスタンスの配下にあるcomponentにおいてstoreを参照する
/*
tree構造
- main.js ... root vue インスタンスを作成し、画面上にセットする
- components/
    - CounterComponent.vue
- store/
    - index.js ... storeを作成する
*/

// index.js ... storeを作成する
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.store({
  state: {
    count: 0,
    loading: false
  },
  getters: { // 後述
    loadingToString (state, _getters) {
      return state.loading ? 'loading...' : 'not loading now'
    }
  },
  mutations: { // 後述. 本来ここではkeyに定数を使うことが多い
    increments (state) { state.count++ },
    decrements (state) { state.count-- },
    setCount(state, payload) { state.count = payload.count }
    showLoading (state) { state.loading = true },
    hideLoading (state) { state.loading = false },
  },
  actions: { // 後述
    increments (context) {
      context.commit('increments')
    },
    waitAndIncrements (context, payload) {
      setTimeout(() => { context.commit('increments') }, payload.decay)
    }
  }
})

// main.js ... root vue インスタンスを作成し、画面上にセット
import Vue from 'vue'
import store from './store'
import Counter from './components/CounterComponent.vue'

new Vue({
  'el': '#app',
  store,
  components: { Counter } // { 'counter': Counter }の省略記法
})


// CounterComponent.vue ... vue componentを作成
import { mapState } from 'vuex'

export default {
  data: {
    localCount: 0 // storeでなく個別にデータを持っても別にいい(が、理想は全てstoreを参照している状況)
  },
  computed: {
    count: this.$store.state.count
   // これでthis.countがstoreのcountに紐づく
  }
  /*
  computed: {
    count: {
      get () { this.$store.state.count },
      set (num) { this.$store.commit('setCount', { count: num }) }
      // setterもcomputed内で実装できる
      // this.count = 10とすればstoreを直に更新することができる
    }
  }

  いちいちthis.$store.state.hogehogeと書くのがめんどくさい場合は、mapStateを使う
  computed: mapState({
    count: state => state.count,
    countAlias: 'count', // 文字列を使うとstate.countを呼び出す
    countPlusLocalCount (state) {
      return state.count + this.localCount
    }
  })

  // storeと同名の算出プロパティでよければ配列で渡すだけ
  computed: mapState(['count'])
    */
}

getters

  • storeのあるstateに何らかの処理を加えてcomputedに加えたい
  • storeのgettersにはやす
/*
tree構造
- main.js
- components/
    - CounterComponent.vue
- store/
    - index.js
    - getters.js ... storeのgettersプロパティを別ファイルに分割することが多い
*/

// getters.js
export const getters = {
  loadingToString (state, _getters) {
    return state.loading ? 'loading...' : 'not loading now'
  }
}

// index.js
import { getters } from './getters.js'
export default new Vuex.store({
  getters,
  /* 省略 */
})

// CounterComponent.vue ... vue componentを作成
import { mapGetters } from 'vuex'

export default {
  data: {
    localCount: 0 // storeでなく個別にデータを持っても別にいい(が、理想は全てstoreを参照している状況)
  },
  computed: {
    // this.loadingMessageとthis.$store.getters.loadingTostringをむすびつける
    loadingMessage: this.$store.getters.loadingToString,
    // loadingMessage: mapGetters(['loadingToString'])でもOK.
    // mapStateと同じ使い方ができる
  }
}

mutations

  • storeのstateを変更する唯一の方法。stateのsetterみたいなもの
  • mutationはタイプとハンドラを持つ
    • 単にmutationの名前と処理内容
  • mutationの第一引数にはstateが渡される
  • 第二引数にはcommit時に渡した引数が入ってくる(下のargs)
  • mutationの実行(呼び出し)はstore.commit('mutationtype', args)を使う
  • mutationに渡す引数argsはオブジェクト(payloadとよく名付ける)として渡すのが作法らしい
    • 上の例で言えば args = { some: 'thing'}としてわたして、
    • hogeMutation (store, payload){'/* payload.someで呼び出し */}
  • 各componentのmethodsにmutationsを結びつける事ができる
    • mapMutationsをつかう
/*
tree構造
- main.js
- components/
    - CounterComponent.vue
- store/
    - index.js
    - getters.js
    - mutations.js new!
    - mutation-types.js new!
*/


// mutations.jsに書き出すことが多い
// また、mutation typeをmutation-types.jsに定数としてまとめることが主流。ただ好み。

// mutation-types.js
export const INCREMENTS = 'INCREMENTS'
export const DECREMENT = 'DECREMENTS'
export const SET_COUNT = 'SET_COUNT'
export const SHOW_LOADING = 'SHOW_LOADING'
export const HIDE_LOADING = 'HIDE_LOADING'


// mutations.js
// ここでのmutation typeとmutation handlerの例
// mutation type : 'INCREMENTS'
// mutation handler : (state) => { state.count++ }
import * as types from './mutation-types.js'
export const mutations = {
    [types.INCREMENTS]: (state) { state.count++ },
    [types.DECREMENTS]: (state) { state.count-- },
    // こうかいてもOK 
    [types.SET_COUNT](state, payload) { state.count = payload.count }
    [types.SHOW_LOADING] (state) { state.loading = true },
    [types.HIDE_LOADING] (state) { state.loading = false },
    // 別に定数使わなくてもいい
    someMutation (state) {/* do something */}
}

// index.js
import { mutations } from './mutations.js'

const store = {
  mutations // 'mutations': mutations の省略記法
}


// どこかのcomponentでmutationを叩くとき。
this.$store.commit('INCREMENTS') // countが1ふえる
this.$store.commit(types.INCREMENTS) // 定数使うなら呼び出し時も使った方がいい

// また、commitの呼び出し方には2通りある
this.$store.commit('INCREMENTS', { amount: 10 })
this.$store.commit({
  type 'INCREMENTS', // mutation typeもオブジェクトに含めて送る
  amount: 10
})

// componentへの結びつけ
import { mapMutations } from 'vuex'
export default {
  methods: mapMutation(['SET_COUNT']) 
  // this.$store.commit('SET_COMIT')を、this.SET_COMMITで呼び出せるようにした
  // aliasをつけたいのであれば、mapStateと同じように書く
}

action

  • mutationとの違いは非同期な処理を行う点. 直接stateを更新しない
  • 各コンポーネントからはthis.$store.dispatch('actionname')で呼び出す
  • actionの第一引数にはcontextが渡される
    • contextはほぼstoreと思って間違いないが、後述の理由によりstoreそのものではない
  • 第二引数にはdispatch時の引数が渡される
    • mutation同様オブジェクトで渡すのが望ましい
  • dispatchはaction内でpromiseをreturnするとそれを返してくれるので、それを組み合わせることで複数のactionを繋げられる
    • async / awaitを使えば記述もシンプルになる
  • mapActionsでactionをコンポーネントのmethodに紐付けできる
/*
tree構造
- main.js
- components/
    - CounterComponent.vue
- store/
    - index.js
    - getters.js
    - mutations.js
    - mutation-types.js
    - actions.js new!
*/


// actions.jsにかきだす
export const actions = {
    increments (context) {
      context.commit('increments')
    },
    // contextのうち、必要なものだけ受け取ることもできる。タイプ数節約。
    waitAndIncrements ({ commit }, payload) {
      // 非同期処理
      setTimeout(() => {commit('increments') }, payload.decay)
    },
    async getUsers(context, payload) {
      return new Promise((resolve) => {
        API.get('/get/users/path').done((data) => {
          context.commit('SET_USERS', { users: data.users })
          resolve(data.users)
        })
      })
    },
    async getFriend (context) {
      await users = context.dispatch('getUsers') // こうかける
      let friends = users.filter((user) => { return user.isFriend })
      context.commit('SET_FRIENDS', friends)
    }
}

// index.js
import { actions } from './actions.js'
const store = {
  actions
}

// 各componentでの呼び出し
this.$store.dispatch('waitAndIncrements', { decay: 1000 })

ここまででstoreのstate / getters / mutations / actionsを見てきた

modulesによるstoreの分割と、名前空間

もろもろなやんだこと

  • data, methodsにmapするのだるくないか
  • 書くcomponentで保有するべき情報はどういったものか ... 原理主義的に全てstoreにおくの?
  • root vue インスタンスにもこれまで通りdata, methodsを持たせられることを明記
  • actionの第三第よん引数

shareシェアする

forumコメント

まだコメントはありません!
ログインしてコメントを残す
{{comment.user.name}} on {{commentCreatedAt()}}

content_copy前後のイシュー

{{message}}