この記事は オープンストリーム Advent Calendar 2018 の11日目の記事です。
うっかり期限を勘違いしていて前日に慌てて記事を書いています!間に合え!!
やりたいこと
- 最近やっと
TypeScript
とVue.js
を使い始めたのでVuex
も一緒に使ってみたい。 - とりあえず
vue-cli
でプロジェクトを作成すると、store.ts
ってファイルがぺろっと一つ追加される。けど、できれば1ファイルにべた書きするんじゃなくて、いいかんじに分けて書きたい。けど何をどう書けばいいのかよくわからない- あとたぶん別の言語やって1ヶ月もしたら内容をすっぱり忘れるので、備忘録もほしい
というわけで、調べてみた。
結論
ここにぜんぶ載ってた。すてき。
https://codeburst.io/vuex-and-typescript-3427ba78cfa8
……なのですが。これだけだと、ちょっと弊社の偉い人に怒られてしm記事としてアレなので、もうちょい噛み砕いた内容で書いていきます。
注意
2018/12/10 21:00 現在、Vue.jsの最新版にバグがあるようで、掲載した内容について動作確認まで行えていません(一応バグのある箇所以外はコンパイルエラーにならないことだけは確認してますが)。動作確認できてから改めてソースコードなど手直しを行います…
https://github.com/vuejs/vue/pull/9173
本題
ファイル構成
vue-cli create hoge
で作成されたテンプレからHelloWorld.vue
をひっこぬいて Todo.vue
と store
以下を足したような状態です。(src
以下の関係あるもののみ抜粋)
src
├── App.vue
├── components
│ └── Todo.vue
├── main.ts
└── store
├── Todo
│ ├── actions.ts
│ ├── getters.ts
│ ├── index.ts
│ └── mutations.ts
├── index.ts
└── types.ts
src/store/Todo
が Vuex
のモジュールで、 src/store
がルートステートになります。
モジュールについては公式のドキュメントをどうぞ。 https://vuex.vuejs.org/ja/guide/modules.html
TodoModule
TODOのリストを管理するやつをイメージしています。
実際はもっとたくさんあれこれ処理があるはずですが、サンプルなので以下省略。
types.ts
モジュール定義の前に、そちらで参照する interface
の定義です。
export interface RootState {
version: string;
}
export interface TodosState {
todos: Todo[];
}
export interface Todo {
// 一意になるかんじのID
id: string;
// チェックボックスON/OFF
done: boolean;
// やること
text: string;
}
特になんの変哲もない interface
ですね。
RootState
と TodosState
が今回の state
の定義になります。
Module(index.ts)
import { Module } from 'vuex';
import { TodosState, RootState } from '@/store/types';
import getters from './getters';
import actions from './actions';
import mutations from './mutations';
const state: TodosState = {
todos: [],
};
export const todos: Module<TodosState, RootState> = {
namespaced: true,
state,
getters,
actions,
mutations,
};
Module
のオブジェクトを用意しています。以上。
getters
, actions
, mutations
の定義は続けて以下に記載します。namespaced: true
にしておくと、このあとコンポーネントから参照するときにらくちんです。
Actions
import { ActionTree } from 'vuex';
import { TodosState, RootState, Todo } from '@/store/types';
const actions: ActionTree<TodosState, RootState> = {
add: async ({ commit }, todo: Todo) => {
if (await someAsyncAddMethod(todo)) {
commit('add', todo);
// 成功
return true;
}
// 失敗
return false;
},
remove: async ({ commit }, id: string ) => {
if (await someAsyncRemoveMethod(id)) {
commit('remove', id);
return true;
}
return false;
},
};
export default actions;
ActionTree
を実装したオブジェクトを用意します。それだけ。
総称型の指定は ActionTree<モジュールのState, ルートのState>
です。
引数の { commit }
については、他にも定義がありますが、このへんは公式ドキュメントのままですのでそちらを参照してください。
mutation
を直接呼び出すか、必ず action
を介するかは好みでいいんじゃないかなーと思いますが、サーバのAPIを叩くなど非同期処理を伴う場合は必ず action
経由で行いましょう。mutation
で非同期処理はダメ、絶対。
Getters
import { GetterTree } from 'vuex';
import { TodosState, RootState } from '@/store/types';
const getters: GetterTree<TodosState, RootState> = {
size: (state: TodosState) => {
return state.todos.length;
},
};
export default getters;
とりあえず感がすごい。よい例が思いつかずすみません。
ここではGetterTree
のオブジェクトを用意します。引数については action
と同様です。
state
の内容を加工して参照する必要があるものはgetter
を介するのかなーと思いますが、自分の場合、単純にそのまま参照して問題ないなら state
の内容を直接参照しちゃっても問題ないんじゃないかな派です。
Mutations
import { MutationTree } from 'vuex';
import { TodosState, Todo } from '@/store/types';
const mutations: MutationTree<TodosState> = {
add: (state, todo: Todo) => {
state.todos.push(todo);
},
remove: (state, id: string) => {
state.todos = state.todos.filter((e: Todo) => e.id !== id);
},
};
export default mutations;
state
を編集する役割を担うもの。これはあんまり迷わなくていいですね。
1つ気になってるのは state.todos.push(todo);
の行。オブジェクトに対する編集になるのか、配列に対する編集扱いになるのかがまだちょっとはっきりしません。(このへんは動作確認したら追記します)
RootState
ルートの定義はこんなかんじ
import Vuex, { StoreOptions } from 'vuex';
import { RootState } from './types';
import { todos } from './Todo';
const store: StoreOptions<RootState> = {
state: {
version: '1.0.0',
},
modules: {
todos,
},
};
export default new Vuex.Store<RootState>(store);
import { todos } from './Todo';
で、上記でつくった TodosState
をインポートしています。
さらにそこからルートステートの modules
に設定しています。
コンポーネントからの参照
// @Component
export default class TodoComponent extends Vue {
@TodoModule.State('todos')
private todos!: Todo[];
@TodoModule.Getter('size')
private size!: number;
@TodoModule.Action('add')
private add!: (todo: Todo) => Promise<boolean>;
private inputText: string = '';
private addTodo(): void {
// validate
if (!this.inputText) {
window.alert('なにか入力してね!');
return;
}
this.add({
id: new Date().getMilliseconds().toString(),
// チェックボックスON/OFF
done: false,
// やること
text: this.inputText,
});
}
}
コンポーネントなどから呼び出すときは、最初の記事にあったように vue-class
を使うのが良さげです。
ただ、現在は最初の記事の内容から比べるといくらか書き方が変わっているようですので、詳細は公式のドキュメントを確認してください。
コードを全部書くと記事が長くなるのでだいぶ端折っていますが、基本的には@Action
, @Mutation
, @Getter
のアノテーションでプロパティのようにぽんぽん定義していくだけ。モジュール側を参照するときは namespace('moduleName')
を経由するだけ。簡単ですね!
まとめ
最初の記事のおかげでTypeScriptでのVuexの使い方がなんとなくつかめたので、助かりました。
あと、迷ったらとりあえず 型定義ファイル を見るというくせもつきました。以前と比べるとだいぶさくっとVue.jsアプリが作れるようになってきたので、あとは忘れないようにしないと…