はじめに
この記事は、Vue.js Advent Calendar 2019 #20日目 の記事です。
担当は @is_ryo でお送りします。
Vuexの治安悪すぎ問題
みなさん Vuex の設計ってどうされていますか?
規約を設けずに自由な感じで Vuex を使っているとこんなことありませんか?
- 巨大化する State…
- 直接呼ばれるmutation…
- getters?actions?なにそれおいしいの?
- modulesが分けられてない…
- とりあえず Vuex に突っ込んでおくかーみたいな風潮
といった悩みを抱えてませんか?私は日々悩まされています…
Vuex はかなり便利でいろんな使い方ができるので治安が悪くなりがちなのかなと思います。
実際に私のチームでも Vuex 周りの治安がかなり悪くなってしまいました…
規約を作ろう
やっぱり治安を良くするためにはまず規約を作らないといけませんよね。
でもあんまりがちがちな規約を作ってしまうと守ってもらえなくなってしまう…
なので下記3点を解決できるような、かつあんまり頑張らない規約を作りました。
- Storeの中身を整理したい
- 整理した上でComponentがどのStoreを使っているのかわかりやすくしたい
- Storeに
Store.hoge
でアクセスしたくない
どんなの作った?
ざっと下記の5つ
-
stores/
以下にVuex関係のソースを置く - namespaceを切って、それぞれのnamespace(Module)ファイルを生成する
- Moduleの構成要素は、
namespaced / state / mutations / actions (/ getters)
- 直接
mutations
を使わない - createNamespacedHelpersを使って、Storeとやり取りする
stores/
以下にVuex関係のソースを置く
ディレクトリ構成はこんな感じ
src/stores/
├── flags.ts
├── store.ts
└── types.d.ts
Storeの親ファイル。modules
内にModuleファイルをimportしていく。
modules内にModuleを入れていく。
import Vue from "vue"
import Vuex, { StoreOptions } from "vuex"
import { RootState } from "./types"
import { flags } from "./flags"
Vue.use(Vuex)
const store: StoreOptions<RootState> = {
state: {
version: ""
},
modules: {
flags // ←ここにModuleを突っ込む。この場合のnamespace名はflags
}
}
export default new Vuex.Store<RootState>(store) // ←Storeを生成
flags.ts
StoreのModuleファイル。ここにModuleの内容を書いていく。
Moduleを変数に入れてexportする。そのときの変数名がnamespace名となる。
import { Module, ActionContext } from "vuex"
import { FlagsState, RootState } from "./types"
export const flagsNamespacedHelper = createNamespacedHelpers("flags")
export const flags: Module<FlagsState, RootState> = {
/* 省略 */
}
types.d.ts
Store関係の型ファイル。
namespaceを切って、それぞれのnamespace(Module)ファイルを生成する
すべてひとつのStoreとして扱うとStateがゴミ箱みたいになってくるので、namespaceを切ります。
namespaceを切ることでModuleとしてStoreを分割することができます。
Storeで扱いたいデータの種別ごとで切るといい感じになると思います。
例えばフラグ関係とか描画するデータ関係とか…
Moduleの構成要素は、namespaced / state / mutations / actions (/ getters)
Moduleの構成要素は、namespaced / state / mutations / actions (/ getters)
になります。(gettersは必要ないかも)
例: stores/flags.ts
import { Module, ActionContext, createNamespacedHelpers } from "vuex"
import { FlagsState, RootState } from "./types"
export const flagsNamespacedHelper = createNamespacedHelpers("flags")
export const flags: Module<FlagsState, RootState> = {
namespaced: true,
state: {
hiddenToolbarItems: false,
overlay: false
},
mutations: {
setHiddenToolbarItems(state: FlagsState, value: boolean): void {
state.hiddenToolbarItems = value
},
setOverlay(state: FlagsState, value: boolean): void {
state.overlay = value
}
},
getters: {
hiddenToolbarItems(state: FlagsState): boolean {
return state.hiddenToolbarItems
},
overlay(state: FlagsState): boolean {
return state.overlay
}
},
actions: {
setHiddenToolbarItems(
{ commit }: ActionContext<FlagsState, RootState>,
value: boolean
) {
commit("setHiddenToolbarItems", value)
},
setOverlay(
{ commit }: ActionContext<FlagsState, RootState>,
value: boolean
) {
commit("setOverlay", value)
}
}
}
直接 mutations
を使わない
直接 mutations
を使わないようにします。要するに Store.commit("hoge")
って書かないようにします。
あくまで個人の感覚なんですが mutations
はprivateな関数として扱った方がいいのかなって思っています。
逆に actions
はpublicな関数として扱うという形で mutations
と actions
の棲み分けをします。
actions
経由で mutations
を使いましょう。書き方は下記のような感じです。
Store.dispatch("hogehoge")
NamespacedHelpersを使って、Storeとやり取りする
というかそもそも Store.hoge
って書くのをやめましょう。
毎回 import Store from "@/stores/store"
って書くのめんどくさすぎるし、どのModuleを使ってるのかわからないくて困るし、namespaceを切っているので、namespaceを間違ったり書き忘れたりするので…
そういうときに役立つのが NamespacedHelpers
です。
Moduleを定義しているファイルの中で createNamespacedHelpers()
を定義して export します。
import { createNamespacedHelpers } from "vuex"
export const flagsNamespacedHelper = createNamespacedHelpers("flags")
こうやってModuleに対してやり取りをできるようにできます。
Stateを購読する場合は、computedでmapStateを展開します。スプレッド構文で書く必要があるので注意。展開したあとは this
から参照できます。
例: App.vue
(抜粋)
import Vue from "vue"
import { createNamespacedHelpers } from "vuex"
import { flagsNamespacedHelper } from "@/stores/flags"
export default Vue.extend({
computed: {
...flagsNamespacedHelper.mapState({
hiddenToolbarItems: (state: any) => state.hiddenToolbarItems,
overlay: (state: any) => state.overlay
})
}
})
Actionを使う場合は、methodsにmapActionsを展開します。こちらもスプレッド構文で書く必要があります。また、一度変数に入れないと型推論をしてくれないので注意。展開したあとは this
から参照できます。
例: viwes/home/script.ts
(抜粋)
import Vue from "vue"
import { HomeComponentState } from "@/types"
import { flagsNamespacedHelper } from "@/stores/flags"
// Vuex store helpers
const flagsMapActions = flagsNamespacedHelper.mapActions([
"setHiddenToolbarItems",
"setOverlay"
])
export default Vue.extend({
data(): HomeComponentState {
return {
text: "hoge"
}
},
created() {
this.setHiddenToolbarItems(true)
this.setOverlay(false)
},
methods: {
...flagsMapActions,
getText(): string {
return this.text
}
}
})
まとめ
今回作った規約をまとめると、
- ちゃんとModule単位に分割することでStoreを整理する
- NamespacedHelperを使うことでComponentがどのModuleを使っているのかimportでわかりやすくする
-
mapState
とmapActions
を使ってStoreにアクセスする
といったところを重きに置いてます。
規約はそれぞれのチームでどのような規約が必要になってくるのか変わってくると思います。
これから規約を作らないと…という感じで困っている人の参考になれば嬉しいです!
ではまた!!!