Holmesでエンジニアをしているid:w-miuchiです。
Holmesのアプリケーションは、フロントエンドを主にThymeleaf + JQueryで構築しています。
今後はSPA(Single Page Application)に切り替えます。それにともないNuxtJS + TypeScriptを採用することになりました。
現在、NuxtJSおよびVue.jsを鋭意勉強中です。
まずはNuxtJSの公式ドキュメントを読むところから始めました。
読む→作って試す、と繰り返していたのですが、いまいちうまく試せなかったのがVuexでした。
公式通りであれば問題なかったのですが、実現したいのは以下の条件でした。
- Actions, Mutations, Stateを定義
- TypeScript(Interface)
- 非同期通信(GET/POST)
この条件で公開されているコードを調べました。
何かの条件が欠けていることが多かったのですが、見つけたのがGoogle-Books-APIです。
こちらのコードを元にしてサンプルを作ってみました。
Vuexの大まかな流れを理解できるかと思います。
Vuexとは
詳しくは下記のVuexの公式ドキュメントを見て頂ければと思います。
Vuexはコンポーネントが共通で持てるStoreであり、コントローラーになります。
ただし「共通の状態を共有する複数のコンポーネントを持ったときに、すぐに破綻します」
とあるように使い方は難しいです。
今回はこの点は置いておいて全体像を掴むことにしました。
Vuexの構成
Stateを扱うのはMutations、そのMutationsを操作するのはActionsとなります。
作成したサンプルにおいては以下の役割としました。
Actions
非同期処理(GETやPOST)を記述する。
またComponentから呼ぶ場合、受け渡すパラメータはインターフェースを用意する。
Mutations
非同期処理(Actions)の処理結果をStateに保存する。
Componentからは呼ばない。
State
Componentから呼ばれる。
Stateにもインターフェースを用意する。
サンプル
作ったサンプルは、以下の画面2つです。
今回はアカウント一覧画面を紹介します。
アカウント一覧画面
- 非同期でアカウント一覧情報を取得すること
- 取得時はローディングすること
- 検索機能があること
アカウント編集画面
- URIにidを含むこと
- idを元に非同期でアカウント情報を取得すること
- 名前を編集すること
- 非同期でアカウント情報を更新すること
Store
ファイル構成
store ├── account │ ├── edit.ts │ ├── search.ts │ └── types.ts ├── dummy.ts ├── store.ts └── types.ts
インターフェースを設定
store/account/types.ts
// Account export interface Account { id: string name: string } // Account検索結果 export interface SearchAccountState { keywords: string accounts: Account[] total: number page: number isLoading: boolean isThere: boolean isError: boolean } // Account検索用パラメータ export interface SearchAccountsPayloadObj { keyword: string page: number }
インタフェースの定義はわかりやすくできました。
HolmesのようにサーバーサイドにJavaの使っている場合、インターフェースは理解しやすいと思います。またサーバーサイドと定義を合わせられるのもメリットです。
またローディングやページングのような共通化できるものはインターフェースにした方がいいでしょう。
RootStateにModuleとして設定
store/types.ts
import { SearchAccountState, EditAccountState } from './account/types'; export interface RootState { // アカウント一覧 SearchAccountModule: SearchAccountState, // アカウント編集 EditAccountModule: EditAccountState }
検索結果と考えるとRootStateに設定することは考えどころです。
同じComponentを複数設置すると、検索結果まで同期してしまいます。
Vuex.Storeを設定
store/store.ts
import Vue from 'vue'; import Vuex from 'vuex'; import { RootState } from './types'; Vue.use(Vuex); export default new Vuex.Store<RootState>({});
Mutaions, Actionsの処理
store/account/search.ts
import { Module, VuexModule, Mutation, Action, getModule, } from 'vuex-module-decorators' import { Account, SetSearchAccountsObj, SearchAccountsPayloadObj } from './types'; import store from '../store'; import dummy from '../dummy'; @Module({dynamic: true, store, name: 'SearchAccountModule', namespaced: true}) class SearchAccountModule extends VuexModule { // Store public keyword: string = ''; public accounts: Account[] = []; public total: number = 0; public page: number = 0; public isLoading: boolean = false; public isThere: boolean = true; public isError: boolean = false; // 検索結果の保存 @Mutation public setSearchAccounts(payload: SetSearchAccountsObj): void { this.accounts = payload.accounts; this.page += 1; this.total = payload.total; } // 検索キーワードの保存 @Mutation public setSearchKeywords(payload: string): void { this.keyword = payload } // 検索結果の初期化 @Mutation public resetSearchAccounts(): void { this.keyword = ''; this.accounts = []; this.total = 0; this.page = 0; } // 検索結果取得中のローディング @Mutation public setLoading(payload: boolean): void { this.isLoading = payload } // 検索結果の有無 @Mutation public setThere(payload: boolean): void { this.isThere = payload } // 検索結果取得のエラー有無 @Mutation public setError(payload: boolean): void { this.isError = payload } // 検索結果取得 @Action({}) public async getAccounts(payload: SearchAccountsPayloadObj): Promise<void> { const { data, error }: any = await new Promise(res => // 非同期処理 setTimeout( () => { const items: Account[] = dummy.accounts.filter((item: Account) => { return item.name.match(new RegExp(payload.keyword, 'ig')); }); res({ data: { total: items.length, items: items }, error: null } ) }, 1000)); this.setLoading(false); // 検索結果のStore保存 if (data) { const info: SetSearchAccountsObj = { total: 0, accounts: [], }; if (data.total !== 0) { info.total = data.total; info.accounts = data.items; this.setSearchAccounts(info); this.setThere(true) } else { this.setSearchAccounts(info); this.setThere(false) } this.setError(false) } else { this.resetSearchAccounts(); this.setError(true) } } } export default getModule(SearchAccountModule)
こちらの@Mutationや@Actionというアノテーションの書き方はvuex-module-decoratorsを利用しています。
@MutationはStateの設定処理になります。
@Actionはダミーの情報をsetTimeoutで取得していますが、axiosを利用した通信に変わることを想定しています。
ダミーの情報は以下になります。
store/dummy.ts
export default { accounts: [ {id: '1', name: 'Giorno Giovanna'}, {id: '2', name: 'Bruno Bucciarati'}, {id: '3', name: 'Guido Mista'}, {id: '4', name: 'Leone Abbacchio'}, {id: '5', name: 'Narancia Ghirga'}, {id: '6', name: 'Pannacotta Fugo'}, ] };
Component
<template> <div class="AccountBlock"> <div class="AccountBlock--header"> <div class="AccountBlock--title">Account List</div> <a class="AccountBlock--refresh" @click="fetchAccounts">Refresh</a> <form @submit="search"><input class="AccountBlock--inputKeyword" type="text" placeholder="Keyword" v-model="keyword"></form> </div> <div class="AccountBlock--item" v-for="(account, index) in accountItems"> <div class="AccountBlock--itemName">{{ account.name }}</div> <div class="AccountBlock--itemEdit"><nuxt-link v-bind:to="{name:'account-edit-id',params:{id:account.id}}">Edit</nuxt-link></div> <div class="AccountBlock--itemRemove"><a href="/remove">Remove</a></div> </div> <div class="AccountBlock--Loading" v-show="isLoading">Loading...</div> <div class="AccountBlock--noData" v-show="!isThere">No data.</div> </div> </template> <script lang="ts"> import { Vue, Component } from 'vue-property-decorator'; import { Account, SearchAccountsPayloadObj } from '../../store/account/types'; import SearchAccountModule from '../../store/account/search'; @Component export default class SearchAccounts extends Vue { protected keyword: string = ''; // アカウント一覧の取得 protected get accountItems(): Account[] { return SearchAccountModule.accounts; } // ローディングの取得 protected get isLoading(): boolean { return SearchAccountModule.isLoading; } // アカウント一覧有無の取得 protected get isThere(): boolean { return SearchAccountModule.isThere; } // アカウント取得エラーの有無 protected get isError(): boolean { return SearchAccountModule.isError; } // アカウント取得 protected async fetchAccounts(): Promise<void> { await SearchAccountModule.resetSearchAccounts(); SearchAccountModule.setLoading(true); SearchAccountModule.setThere(true); const data: SearchAccountsPayloadObj = { keyword: this.keyword, page: 0, }; await SearchAccountModule.getAccounts(data); SearchAccountModule.setSearchKeywords(data.keyword) } created() { this.fetchAccounts(); } search(e: any) { e.preventDefault(); this.fetchAccounts(); } } </script>
レンダリングはStateを利用し、
非同期処理のアカウント情報取得はActionsを使用できるようになりました。
所感
Vuexの処理の流れや全体像を掴むことができました。 ただし、Storeにどの情報を持たせるべきかを考えて設計する必要があることがわかりました。
またサンプル作成のために他にもコードを調べましたが、
Vuexは作りたいものによって構造や書き方が様々でした。
非同期通信の有無やJavaScript OR TypeScriptでも大きく異なります。
ここからは、アプリケーションの構造に合わせた設計で考えて行きたいと思います。
最後に、
Holmesはエンジニア・デザイナーを募集しています!
興味がある方はこちらからご連絡ください!