素人Web屋の備忘録

素人Web屋の備忘録。たまに雑記。

Vuexのモジュール分割について

Vuexでストアを管理していると、アプリケーションが大きくなるにつれてストアが肥大化し、見通しが悪くなる問題が起こります。
それを回避するためにストアを分割できるようになっています。例えばアプリケーションの機能ごとやページごとにストアを分割していれば、コードの見通しがよくなりますし、プロジェクトメンバーで共同で作業しやすくなります。

以下にストアを分割する方法を記載します。

// ストア側

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);

const moduleA = {
  namespaced: true,
  state: {...},
  getters: {...},
  mutations: {...},
  actions: {...}
}

const moduleB = {
  namespaced: true,
  state: {...},
  getters: {...},
  mutations: {...},
  actions: {...}
}

export default new Vuex.Store({
  modules: {
    aaa: moduleA,
    bbb: moduleB
  }
});

これでストア分割完了です。
ポイントは各モジュールを定義するときに、namespacedオプションをtrueに設定することです。
モジュール分割をすると、同一のミューテーションタイプやアクションタイプが指定できることになります。そのため、どちらか一方を呼び出したときに、同一のタイプはすべて呼び出されることになります。それを避けるために、namespacedtrueにすることで影響範囲を限定することができます。

namespaceを与えたときは以下のように、各モジュールのミューテーションタイプ、アクションタイプを指定して呼び出します。

// コンポーネント側
this.$store.commit('aaa/設定したミューテーションタイプ')

this.$store.dispatch('bbb/設定したアクションタイプ')

this.$store.getters['ccc/設定したゲッター'] // これはゲッターの例

このようにnamespaceを設定することでモジュールごとのミューテーション、アクション、ゲッターを呼び出せます。
マップヘルパーにnamespaceを登録すればモジュール名を省略して呼び出せます。その場合はヘルパーの第一引数にnamespaceを指定します。

computed: {
  ...mapMutations("aaa",["設定したミューテーションタイプ"]),
  this.[設定したミューテーションタイプ]

}

ちなみにモジュール分割はネストもできるので、以下のように書くことも可能です。

const moduleA = {
  namespaced: true,
  state: {...},
  getters: {
    parentGet: ...
  },
  mutations: {...},
  actions: {...},

  modules: {
    page01: { // 親のnamespaceを継承するので、例えばゲッターを指定したいときは親モジュールのゲッターの呼び方と同じでOK(下記★参照)
      state: {...},
      getters: {
        get: // ゲッターの処理書く
      },
      mutations: {...},
      actions: {...},

      modules: {
        namespaced = true
        layer01: { // namespacedを指定しているので、例えばゲッターを指定したいときは親モジュールから相対パスで指定して呼び出す(下記♦参照)
          namespaced: true,
          state: {...},
          getters: {
            get: // ゲッターの処理書く
          },
          mutations: {...},
          actions: {...},
        }
      }
    }
  }
}

★の呼び出し方

...mapGetters("aaa", ["parentGet", "get"]), // マップヘルパー使ったとき

this.get // これで子のget呼べる
this.$store.getters.parentGet // マップヘルパー使わない場合

♦の呼び出し方

...mapGetters("aaa", ["parentGet", "get", "layer01/get"]), // マップヘルパー使ったとき

this['layer01/get'] // これで孫のget呼べる
this.$store.getters['form/layerPage/profile'] // マップヘルパー使わない場合は親からの相対パスで呼ぶ

モジュール分割は他にも例えば別モジュールに登録された処理を呼び出したり、ルートに登録されたモジュールを呼べたりするので、公式ドキュメント載せておきます。

参考資料
モジュール | Vuex

ここまで読んでいただいてありがとうございました!!!

Vuexのアクションについて

前回、Vuexのミューテーションについて説明しましたがミューテーションは以下のルールがあります。

ミューテーションハンドラ関数は同期的でなければならない

つまりミューテーションでできるのは同期処理のみで、非同期処理はできません。そのため実際に使うとなるとミューテーションだけでは辛くなります。
そのためにアクションがあり、アクションは非同期処理を行うことができます。

アクションはアクションハンドラをアクションに登録してそれを呼び出すことで使います。
実際にアクションハンドラを登録して、それを呼び出す方法を記載します。
やってることはただのカウントアップです。

// ストア側
state: {
  count: 0
}
mutations: {
  [INCREMENT](state) {
    state.count += 1;
  }
},
actions: {
  increment({ commit, state }) { // 例としてプロミスで非同期処理しています
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("success");
      }, 1000);

      if (state.count === null) {
        reject("err");
      }
    })
      .then(value => {
        console.log(value);
        commit("INCREMENT");
      })
      .catch(err => {
        console.log(err);
        return err;
      });
  }
}
// コンポーネント側
methods: {
  aaa() {
    this.$store.dispatch('increment')
  }
}

やってることは以下です。(ミューテーションには定数を使っている前提です)

  • ストア側に状態変更に使うミューテーション登録
  • ストア側にそのミューテーションを使うためのアクション登録(登録したアクションハンドラの引数にcommit渡してるのは後ほど説明します)
  • コンポーネント側のメソッドオプションにそのアクションを使うためのメソッドaaaを登録

aaaを使うとアクションハンドラ -> ミューテンションハンドラ -> ステート変更 の流れで処理されます。

ポイントになるところは、登録したアクションハンドラの引数にcommitを渡しているところです。
アクションハンドラは引数にcontextを取ります。contextとはストアインスタンスで持っているステート、ゲッター、ミューテーション、アクションなどと同じものを呼び出せるオブジェクトです。contextごと渡してもOKです。
よりシンプルに書くために、分割代入を使って使いたいコンストラクタオプションを呼び出して使います。(上の例ではcommitstateを分割代入して使っています)
また、アクションハンドラはdispatchされることでトリガーされます。

ミューテーションハンドラはcommit
アクションハンドラはdispatch

アクションの使い方は以上になりますが、最後にコンポーネント側でアクションを呼び出すときの短縮記法を以下に記載しておきます。

// コンポーネント側
methods: {
  ...mapActions(["increment"]), // まずマップヘルパーに登録
  aaa() {
    // this.$store.dispatch('increment') // これ冗長です
    this.increment() // これで上と同じ意味です
  }
}

補足として説明してきたVueとVuex周りの用語で混乱しがちなものを以下にまとめました。

Vueのオプション
・data
・computed
・methods

Vuexのオプション
・state
・getters
・mutations
・actions

mutationsオプションに登録するもの
・mutationハンドラ

mutationsハンドラを呼び出すとき
・mutationの名前を指定してコミットする

actionsオプションに登録するもの
・actionハンドラ

actionsハンドラを呼び出すとき
・actionsの名前を指定してディスパッチする

次はモジュール分割について書きたいと思います。
ここまで読んでいただいてありがとうございました!!!

参考資料
アクション | Vuex

Vuexのミューテーションについて

ミューテーションはVuexのストアの状態を変更したいときに使うもので、分かりやすくいうとハンドラに近いものです。
使い方と特徴がいくつかあるので以下にまとめます。

  • ミューテーションはタイプとハンドラを持っています
  • ハンドラみたいなものなので直接使うことができません(公式ではハンドラに近い概念といっています)
  • タイプを指定してコミットすることでハンドラを使えます
  • ミューテーションのハンドラは第一引数にstateを渡して実際にstateの変更を行います
  • ミューテーションのハンドラは第二引数に追加の引数(payloadといいます)を渡すこともできます※payloadはオブジェクトで渡します
  • コンポーネント側でタイプを指定してコミットします
  • 上に書いた第一引数と第二引数を無視して、第一引数にtypeプロパティを持つオブジェクトを渡してコミットすることもできます
  • ミューテーションハンドラでは非同期処理ができない

具体的に設定すると以下のような感じになります。

// ストア側
state: {
  hoge: 10
}
mutations: {
  aaa(state) {
    state.hoge + 100
  },
  bbb(state, payload) {
    state.hoge + payload.num // ここではnumはpayloadオブジェクトにセットした値のキーと仮定します
  }
}

コンポーネント側でコミットするには以下のように書きます。

// コンポーネント側
this.$store.commit('aaa') // これがミューテーションタイプを指定した普通のコミット

this.$store.commit('bbb', { // これがpayloadも渡す場合のコミット
  num: 100
})

this.$store.commit({ // これがtypeプロパティを持つオブジェクトを渡した場合のコミット
  type: 'bbb',
  num: 100
})

また、ステートとゲッターと同じく、コンポーネント側で上のように書いていても冗長なのでもっと短く書ける方法があります。
mapMutationsを使います。

設定方法は以下のようにします。

// コンポーネント側
import { mapMutations } from "vuex";

export default {
  methods: {
    ...mapMutations(["aaa"])
  }
}

スプレッド演算子を使うことでオブジェクトをマージできるので、他に設定したいmethodsがあったとしても問題ありません。
呼び出すときは以下の記述で書けます。実際仕事ではこっちを使うほうが多いと思います

this.$store.commit('aaa') // マップミューテーション使う前
this.aaa() // 使った後

また、仕事ではミューテーションに定数を使用していることも多いと思うので、以下の方法に慣れておくといいかもしれません。

  • 定数を宣言するファイルを作成します
  • ファイルをstore側でインポートしてミューテーションのタイプに定数を使います
  • コンポーネント側ではタイプとして定数を指定してコミットします

例を出すとこんな感じです。

// mutation-type.js
export const AAA = 'AAA'
// ストア側
import { AAA } from './mutation-types' // 分割代入で必要な定数をインポートします

state: {
  hoge: 10
}
mutations: {
  [AAA](state) { // 定数はブラケット記法で書きます。この書き方はES2015のcomputed property nameです
    state.hoge + 100
  }
}
// コンポーネント側
import { mapMutations } from "vuex";

export default {
  methods: {
    ...mapMutations([AAA])
  }
}
this.AAA() // 使うとき

基本の使い方はこんな感じになります。
次回はアクションについて書いていきます。

ここまで読んでいただいてありがとうございました!!!

参考資料
ミューテーション | Vuex

Vuexのゲッターについて

Vue.jsには算出プロパティがありますが、Vuexではそれと同じことをgettersを用いて行います。
設定方法は以下のようにgettersの中に好きなgetterを追加していきます。

state: {
  hoge: 10
}
getters: {
  aaa: aaa(state) {
    return state.hoge
  }
}

getterは必ず第一引数がstateになります。
コンポーネント側でhogeの値10を使いたい場合、コンポーネント側でgettersを呼び出せば使えます。
呼び出し方は冗長ですが、以下のように書きます。

this.$store.getters.aaa // 短く書く方法もあるのでそれは後ほど説明します

ちなみにgetterはアロー関数を使えば以下のように短く書けるのでこっちのほうが良いです。

state: {
  hoge: 10
}
getters: {
  aaa: state => state.hoge
}

getterの中で他のgetterを呼び出したいときもあると思いますので、その場合は第二引数にgettersを渡して、以下のように使います。

state: {
  hoge: 10,
  huga: 100,
}
getters: {
  aaa: state => state.hoge,
  bbb: (state, getters) => getters.aaa + state.huga // getters.aaaでhogeの値を取得 -> hugaの値と足す -> getters.bbbは110になる
}

繰り返しになりますが呼び出し方は以下です。

this.$store.getters.bbb

ここまで、getterの第一引数にstate、第二引数にgettersを渡すことでストアオブジェクトの状態(stateオブジェクトの中身のこと)を参照できました。
この方法をプロパティアクセススタイルといいます。
さらに、ストアオブジェクトの状態を参照するもうひとつの方法があります。
その方法をメソッドアクセススタイルといいます。

メソッドアクセススタイルでは引数を渡すことができます。これはプロパティアクセススタイルではできません。
以下に両者の違いを書きます。

aaa: state => state.hoge, // プロパティアクセススタイル: getters.aaaでhogeの値を取得
zzz: (state, getters) => num => getters.aaa + num // メソッドアクセススタイル: getters.aaaでhogeの値を取得 + 引数numを足す

呼び出し方は以下です。

this.$store.getters.zzz(500) // 引数が渡せる

ここまでgetterを呼び出す2つの方法について説明しました。
最後にgetterを短い記述で呼び出す仕組みを説明します。

コンポーネント側で短い記述で呼び出す仕組みをmapGettersヘルパーといいます。
これを使うと以下のように省略して書くことができます。 コンポーネント側で、算出プロパティにmapStateヘルパーを設定します。

// コンポーネント側
computed: {
  ...mapGetters(["aaa", "bbb"]),
  computed01() {...},
  computed02() {...}
}

そうするとコンポーネント側でgetterを呼び出したいときはこれくらい短く書けます。

this.aaa // プロパティアクセススタイル
this.zzz(500) // メソッドアクセススタイル

次はミューテーションについてまとめたいと思います。
ここまで読んでいただいてありがとうございました!!

参考資料
ゲッター | Vuex

Vuexのステートについて

Vue.jsを使用するときに、アプリケーションの状態を管理するためにVuexを使うことがほぼだと思いますので、これから何回かに渡ってVuexについての基本的な使い方をまとめようと思います。

今回はVuexについて少し触れて、あとはステートについてまとめようと思います。

なぜVuexを使う必要があるのかというと、アプリケーション全体の状態を1つの場所で管理しておけば、現在のアプリケーションの状態のスナップショットが容易になるのでデバッグしやすくなります。

  • Vuexはsingle state tree(アプリケーション全体の状態が1つのオブジェクトツリーに格納される)
  • スナップショットが容易なのでデバッグしやすい

Vuexを使うときはVuex専用のファイルを作ってそれをルートコンポーネントでインポートする使い方が一般的です。

// store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex); // SFCを使う場合はこの記述を書く -> この記述で各コンポーネントでstoreを参照できるようにする機能が有効になります。
export default new Vuex.Store({
  state: {
    aaa // ステートプロパティを設定していく
    bbb
    hoge: 10
  }
})

store.jsをルートコンポーネントに設定する

import store from "./store";
new Vue({
  store, // SFCを使う場合はこの記述を書く -> この記述で各コンポーネントでstoreを参照できるようになりました。
  aaa: 1,
  bbb: 5,
  hoge: 10
})

これでVuexを使う準備と、SFCでstoreを参照できる設定が完了です。
コンポーネントでstoreを参照する方法は以下です。

this.$store.state.hoge // これで設定したステートプロパティにアクセスできます。(例でhogeプロパティを参照しています。)

ただこの書き方は冗長過ぎて面倒なので、短縮できる仕組みが用意されています。
その仕組みをmapStateヘルパーといいます。これを使うと以下のように省略して書くことができます。

コンポーネント側で、算出プロパティにmapStateヘルパーを設定します。

computed: {
  ...mapState(["aaa", "bbb", "hoge"]),
  computed01() {...},
  computed02() {...}
}

そうするとコンポーネント側でステートプロパティを参照したいときはこれくらい短く書けます。

this.hoge // mapStateヘルパーを使う前はthis.$store.state.hoge

mapStateヘルパーはcomputedにスプレッド演算子で設定します。
mapStateヘルパーは以下の特徴があるのでこのように設定できます。

  • mapState内ではthis.$store.stateと同じ意味としてstateが使える

なのでmapStateの正体はただ算出プロパティにステートプロパティのゲッターを設定しているだけです。

mapState({
  aaa: state => state.aaa,
  bbb: state => state.bbb,
  hoge: state => state.hoge
})

しかもmapStateには以下の特徴もあります。

  • 算出プロパティの名前(ここではaaaとかbbbとかhogeとか)とステートプロパティの名前(こっちもaaaとかbbbとかhogeとか)が同じならmapStateに文字列配列を渡すだけでいい

なのでここまでシンプルに書けます。

  mapState(["aaa", "bbb", "hoge"]),

最後に、なぜスプレッド演算子を使わないといけないか説明します。
mapState()はオブジェクトをリターンするので、mapState(["aaa", "bbb", "hoge"])を実行したときに返り値は以下になります。

{
  aaa() {
    return this.$store.state.aaa
  },
  bbb() {
    return this.$store.state.bbb
  },
  hoge() {
    return this.$store.state.hoge
  },
}

そうすると、算出プロパティに他の関数をセットしたいときに以下のようになり問題が起こります。

computed: {
  {
    aaa() {
      return this.$store.state.aaa
    },
    bbb() {
      return this.$store.state.bbb
    },
    hoge() {
      return this.$store.state.hoge
    },
  },
  computed01() {...},
  computed02() {...}
}

そこでスプレッド演算子を使ってオブジェクト同士をマージします。(mapStateの返り値と、computed01() {...}とかcomputed02() {...}をマージする)

...mapState(["aaa", "bbb", "hoge"]),

そうすると算出プロパティの中が以下のようになります。

computed: {
  aaa() {
    return this.$store.state.aaa
  },
  bbb() {
    return this.$store.state.bbb
  },
  hoge() {
    return this.$store.state.hoge
  },
  computed01() {...},
  computed02() {...}
}

長くなりましたが、Vuexのステートについて基本的な使い方でした。
ここまで読んでいただいてありがとうございました!!!

参考資料
ステート | Vuex

配列操作でよく使うメソッドまとめ05

前回の配列操作でよく使うメソッドまとめ04つづき。
今回も前回までと同じJSONデータを使ってfindの使い方についてまとめます。
JSONデータの中身は配列操作でよく使うメソッドまとめ01のページ下部に載っています。

findを使えば引数で与えた関数に一致する要素を返してくれます。
今回は選手名で検索して、その選手がいるチームを返そうと思います。

特定の選手がいるチームを返す

import data from '../EasternConference.json'
return data.find(data => {
    for(let i = 0; i < data.members.length; i++) {
        if(data.members[i].name === "Quincy Acy") {
            return data
        }
    }
})

これで該当選手がいるチームを返すことができます。

参考資料
Array.prototype.find()

配列操作でよく使うメソッドまとめ04

前回の配列操作でよく使うメソッドまとめ03つづき。
今回も前回までと同じJSONデータを使ってObject.assignの使い方についてまとめます。
JSONデータの中身は配列操作でよく使うメソッドまとめ01のページ下部に載っています。

Object.assignを使えば簡単に対象のオブジェクトのシャローコピーを行うことができます。
ディープコピーをしたい場合はObject.assignではない方法で行うのでそれについても記載します。

各チームメンバーの配列をシャローコピーする

import data from '../EasternConference.json'
let menberArry = data.map(v => v["menbers"])
console.log(Object.assign({}, menberArry))
return Object.assign({}, menberArry)

Object.assignは第一引数に空のオブジェクト、第二引数にターゲットオブジェクトを設定すれば、ターゲットオブジェクトをシャローコピーしたオブジェクトを返してくれます。
第一引数に空オブジェクトではないオブジェクトを指定した場合は、オブジェクト同士がマージされますし、オブジェクト同士に同一のキーがあれば、第二引数側の値が使われます。(ただ上書きしてるだけ) 気を付けるポイントとしては、Object.assignはundefinedやnullを許容するので例外は投げられません。
ただ、実際にこのメソッドを使う場面では元のオブジェクト自体にundefinedやnullが含まれていることは少ないんじゃないかなぁと思います。(元のオブジェクト生成時点で例外処理や置換処理が行われていてほしい)

各チームメンバーの配列をディープコピーする

import data from '../EasternConference.json'
let menberArry = data.map(v => v["menbers"])
const deepCopyObj = JSON.parse(JSON.stringify(menberArry))
console.log(deepCopyObj)
return deepCopyObj

ディープコピーするには組み込みオブジェクトであるJSONオブジェクトを使用して以下の手順を踏めばディープコピーできます。
まずstringifyメソッドでJavaScriptのオブジェクトをJSON形式の文字列に変換します。(シリアライズ化)
つぎにparseメソッドでJavaScriptのオブジェクトに戻します。(デシリアライズ化)

参考資料
Object.assign()