###はじめに
vue.jsを使ったコンポーネント設計はとても快適ですね。規模が大きくなるにつれ肥大化するJavaScirpt、HTML、CSSの見通しが格段によくなります。
その半面、深く入れ子になったコンポーネント間のデータの受け渡しのコードも肥大化して、いわゆる「バケツリレー」に疲弊します。
Vuexにより、データの保管場所の特定やアクセス関数を統一することで、データフローがコンポーネントから独立され、コードの見通し、メンテナンス性が格段に向上します。
この記事は、Vuexの実装例を見ることでVuexの効果を伝えることが目的です。
Vuexとは?...からちゃんと理解したい人は、 Vuex公式サイトへ。
「Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 ....」
Qiita投稿一覧を Vue.js + Vuex (+Nuxt) で作ってみた
以下の画面は、QiitaAPIを使って、投稿一覧を表示するウェブサイトの画面です。
ここでの留意点は
・Qiita投稿は、AxiosでQiitaAPIから取得する。
・タブで他のページに遷移後、このページに戻ったときに、画面の再構築(QiitaAPI呼び出し)はしたくない。
・そのため、投稿一覧をClient側のJavaScriptの変数として保持しておかなければならない。
そこで、Vuexをつかった以下のコードをつくりました。
qiita投稿一覧のためのstoreコード
store/qiita.js
// ----------------------------------------------------------------------------
// State
// ----------------------------------------------------------------------------
export const state = () => ({
items: [], // 投稿の配列
})
// ----------------------------------------------------------------------------
// Mutation
// ----------------------------------------------------------------------------
export const mutations = {
// 投稿を一括登録
setItems (state, {items}) {
state.items = items
},
// 投稿配列をクリアー
clearItems(state) {
state.items = []
}
}
// ----------------------------------------------------------------------------
// Getter
// ----------------------------------------------------------------------------
export const getters = {
// 投稿記事の配列
items(state) {
return state.items
},
// itemsが空のとき trueを返す
is_empty_items(state) {
return (state.items.length < 1)
}
}
// ----------------------------------------------------------------------------
// Actions
// ----------------------------------------------------------------------------
import axios from 'axios'
export const actions = {
// Qiita API呼び出し
async fetchItems({commit, getters}) {
console.log(" --- getters.is_empty_items items[]が空なら true -> ", getters.is_empty_items)
if(getters.is_empty_items) {
// axiosで Qiita APIの呼び出し
// async/awaitにより非同期処理
// axios.$get(.....).then(res => ....)と書かなくてもよい
const items = await this.$axios.$get('/items?query=tag:nuxt.js')
// nuxt.config.jsにて baseURL設定
//const items = await this.$axios.$get('https://qiita.com/api/v2/items?query=tag:nuxt.js')
commit('setItems', {items})
console.log(" --- items の commit 完了 size -> ", items.length)
}
}
}
このコードは、以下のstoreの定義にしたがっています。
storeのデータフロー図
以下はVuex公式サイトにあるデータフロー図です。
-
state
- データの保管場所。直接参照可
-
mutations
- stateの更新はここでおこなう。
-
getters
- gettersに登録された関数の結果はキャッシュされ、その関数が依存しているstateの値が変更されたときにのみ再評価される
-
actions
- 非同期にstateの更新をおこなうときは、ここからmutationを呼び出す
store/qiita.jsのデータフロー図
上記の定義を store/qiita.jsと重ねると以下のデータフローになります。
Actions
- fatchItems 関数
getterである is_empty_itemsで配列が空であることを確認。空でなければ何もしない
axios.get により QiitaAPIの呼び出し。async/awaitはここで使う。
得られた投稿一覧をmutation setItemsに渡す。
Mutations
-
setItems 関数
投稿をstoreに格納する -
clearItems 関数
投稿配列を空にする
Getters
-
items 関数
投稿一覧を得る -
is_empty_items 関数
投稿配列のstoreが空のときtureを返す
Qiita投稿一覧画面のコンポーネント
上記の store/qiita.js を使用して、"Nuxt.js"のダグがついたQiita投稿を表示するコンポーネントのコードです。
...mapActions(,)
の先頭の ...
は スプレッド演算子です。ネット上のVuexの記事上でとてもよく使われているので、この際覚えましょう。
(ヒントは fMap = {a: ()=>{..}, b: ()=>{..}}
だと ...fMap
は何を返す?)
<template>
<v-content>
<v-container>
<v-layout row justify-center>
<h2>Nuxt.js のタグが付けられた投稿の一覧</h2>
<v-btn class="ml-5" @click="fetchItems">更新</v-btn>
<v-btn @click="clearItems">消去</v-btn>
</v-layout>
<v-layout wrap>
<v-row>
<v-col cols="12">
<ul>
<li v-for="item in items" :key="item.id">
<h3>
<span>{{item.title}}</span>
<small> by {{item.user.id}}</small>
</h3>
<h4>
<div>{{item.body.slice(0, 130)}}.....</div>
<p><a :href="item.url">{{item.url}}</a></p>
</h4>
</li>
</ul>
</v-col>
</v-row>
</v-layout>
</v-container>
</v-content>
</template>
<script>
import {mapGetters, mapActions, mapMutations} from "vuex"
export default {
layout: 'vuetify', // Layoutファイルの指定
head() { return { title: "Qiita" } },
// componetの生成前に呼び出す
fetch({store}) {
store.dispatch('qiita/fetchItems')
},
computed: {
...mapGetters('qiita', ['items'])
// 以下と同じ
//items: {
// get() { return this.$store.state['qiita'].items }
//},
},
methods: {
...mapActions('qiita', ['fetchItems']),
// 以下と同じ
//fetchItems() { this.$store.dispatch('qiita/fetchItems') },
...mapMutations('qiita', ['clearItems']),
// 以下と同じ
//clearItems() { this.$store.commit('qiita/clearItems') }
}
}
</script>
結局、Vuexの何がうれしいの?
- バケツリレーの撤廃
- Vue.jsでは、共通データをコンポーネントに受け渡す場合、親から子へはhtmlの属性でわたして、Propsで受け取ります。子から親へはイベントの引数として渡します。極端なたとえですが、Aの子がB、Bの子がC..と延々にZまで子孫がいたとします。 ここでもし、AとZ間のみ共通で使いたいデータがあるとすると、BからYは自分に必要のないデータを自分の親または子に渡していかなければなりません。このことをVue.jsのバケツリレーといわれています。 Vuexは共通データをどこからでも更新、参照する共通関数群を規定にしたがって定義します。この規定にしたがってが重要な部分です。あとから誰が見てもわかりやすくなります。
- グローバル変数定義の局所化、定形化
- SPAでは、一度サーバから得たデータは極力キャッシュして、通信負荷を削減したい。という動機から無秩序にグローバル変数を使ってしまうことがあります。 Vuexはグローバル定義を規則化することで、共同開発におけるコードの定形化がはかれます。
- WebAPI呼び出しの局所化、定形化
- WebAPI呼び出しをActionとして局所化、定形化することは、メンテナンス性がとても向上することでしょう。そして非同期処理はほぼ、Action内にまとめられることも、とても見通しの良いコードになるでしょう。リアクティブなSPAにおいては、とてもありがたいことです。
- なによりデータフロー設計ファーストになる
- 多くのコンポーネントを作ることは、副作用としてコンポーネント間でのデータの重複や隠蔽がうまれやすくなります。Vuexを習得すれば、開発の初動時にデータフロー設計(store定義)をおこなうこで、それらの問題が解決することはすぐに実感できるはずです。
ところでNuxt.jsはおすすめです
- この投稿に使っているコードはNuxt.jsで稼働しているものです。(nuxt上でないと動かないかも)
- Nuxt.jsをひとことで言うと、Vue.js にVuex(+その他)を組み込んだフレームワークです。
- Vue.jsやVuexの初期定義などの定型的かつややこしい部分をごっそり隠してくれています。
- そもそもVue.jsは、JavaScriptでWEBを作るためのフレームワークなので、Nuxt.jsはフレームワークのフレームワークです。
- SSR(サーバーサイドレンダリング)やPWA(Progressive Web Apps)用のフレームワークとて着目されていますが、従来のvue.jsでの開発にも十分な恩恵を感じられることは、多くの識者がWEB上で語っています。
- また、Railsの思想であるCoC(Convension over Configuration 設定より規定)をとても感じます。(他にもRailsに似てるところがたくさんある)
- Vue.jsをある程度習得した開発者でVuexの必要性を感じるひとは、まずはnuxt.jsに踏み込むほうが、遠回りのようで、近道ではないかと思います。(既存のVue.jsプロジェクトをNuxt化するのは容易だとおもいます)