記事の目的
Vuexの書き方を学び、動かす。どうせなら動くものがつくりたい!!!!ってことで、ToDoアプリケーションを作成し、その中で学んでいきます。アプリの雛形の作り方はこれまでの連載を参考にしていただきたいです。Githubレポジトリにソースコードがありますので、ご参照ください。特殊なライブラリを使っているわけではないため、この手順でやれば必ず動きます!!。
前回までの
一連の連載
(New!) Vuexを意地でも動かす~ToDoアプリを作ろう~コード付き
Vuexの導入
Vuexとは?
Vuex は、コンポーネント志向のWebアプリにおいて、各コンポーネントからアクセス可能な共通のデータプールを提供してくれます。本当の初学者はバックエンドのデータベースとこんがらがるかもしれません。あくまでフロントエンドのデータ保管場所です。
Fluxという概念を構築するために作られたフレームワークとも言えます。Fluxとは?
*参照元 https://raw.githubusercontent.com/facebook/flux/master/examples/flux-concepts/flux-simple-f8-diagram-with-client-action-1300w.png
- (左) Action...初期描写
- Dispatcher...ストアへの接続とリクエスト投げ
- Store...先ほどのクライアント側データプール
- View...表示
- (上) Action...Web画面でのユーザアクション(Click, Enterキー押)
といった感じでしょうか。あくまで概念です。
実際のVuexは以下から構成されます。各場所にどんな処理を書くべき関数なのかを書きます。
- State 初期データの宣言をする
- Getters Stateのデータに、ある決まった演算をして値を返す。データの上書きはしない。 (例. 商品データに、特定ユーザ向けで5%オフの演算をする場合)
- Mutations 初期データを実際に更新する
- Actions 初期データの更新をする演算のリクエストを受けて、下準備をした後、Mutationsを起動する。
Vuexをインストールし、使えるように
プラグインの追加を行います。プロジェクトのホーム階層で
yarn add vuex
を実行します。またコンポーネント中で呼び出して使えるようにmain.jsで使用宣言をします。
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import Vuex from 'vuex' /* 読み込み */
import App from './App'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'
import '@mdi/font/css/materialdesignicons.css'
import { store } from './store/store'
Vue.use(Vuetify)
Vue.use(Vuex) /* Vuexの使用宣言 */
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
store: store, /* store宣言 */
el: '#app',
components: { App },
template: '<App/>'
})
次に、src/store/store.jsを作成します。上述したState/Mutations/Actionsと対応している内容になってます。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
// 初期データを宣言
tasks: [
{
name: 'No1',
flag: false
},
{
name: 'No2',
flag: true
},
{
name: 'No3',
flag: false
}
]
},
mutations: { // 実際にStateのデータを更新する処理を記述する。
TaskFinished: (state, payload) => {
state.tasks.forEach(value => {
if (value.name === payload) {
if (value.flag === true) {
value.flag = false
} else if (value.flag === false) {
value.flag = true
}
}
})
},
TaskAdded: (state, payload) => {
console.log(payload)
state.tasks.push({
name: payload,
flag: false
})
}
},
actions: { // コンポーネントからリクエストを受けて、処理する下準備をするための関数
TaskFinished: (context, payload) => {
// 例えばバックエンドDBからのデータ取得などはここで済ませること
// commit関数で、Mutationsで定義した関数を指定して呼び出す
context.commit('TaskFinished', payload)
},
TaskAdded: (context, payload) => {
context.commit('TaskAdded', payload)
}
}
})
前回までの記事を見ていただいた方はわかると思うが、今までデータはApp.vue(親コンポーネント)で定義し、App.vueの親コンポーネント内のメソッドでデータ更新関数を用意していました。また、更新する実際のデータはEVENTトリガーで子コンポーネントから親コンポーネントに渡していました。
Vuexを使う場合には、コンポーネントから参照/変更するデータの管理はStateに行い、データ処理部分は、該当するMutationsに記述します。MutationsはVuexのActionsから呼び出される決まりなので、子コンポーネントから直接呼び出すのはActionsです。
ではコンポーネントを見てみましょう。
<template>
<div class='InputTask'>
<v-layout row wrap>
<v-flex xs6>
<v-form ref='form' v-on:submit.prevent='addtask(taskname)'>
<v-text-field v-model='taskname' placeholder='input the task'></v-text-field>
<v-btn v-on:click.prevent='addtask(taskname)' color='success' dark>Submit</v-btn>
</v-form>
</v-flex>
</v-layout>
</div>
</template>
<script>
export default {
name: 'InputTask',
data () {
return {
taskname: ''
}
},
methods: {
addtask: function (msg) { // 親コンポーネントに渡していたのでEvent渡しをしていましたが、Vuexではdispatch関数でActionsに記述した関数を呼びます。第二引数は渡したいデータ。
this.$store.dispatch('TaskAdded', msg)
this.taskname = ''
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.input{
width: 130pt;
height:30pt;
}
.button {
display: block;
position: relative;
margin: 0 auto;
width: 70pt;
border: solid 1px silver;
border-radius: 0.5rem 0.5rem;
padding: 0.5rem 1.5rem;
margin-top: 1rem;
text-decoration: none;
}
</style>
<template>
<div class='task'>
<v-data-table :headers="headers" :items='tasks' class="elevation-1">
<template v-slot:items="props">
<td>{{ props.item.name }}</td>
<td v-if=props.item.flag class="text-xs-left"><v-btn v-on:click='TaskFinished(props.item.name)' color='info'> Done </v-btn></td>
<td v-else class="text-xs-left"><v-btn v-on:click='TaskFinished(props.item.name)' color='warning'>Not Yet</v-btn></td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
name: 'TaskView',
computed: {
tasks () {
return this.$store.state.tasks // これまでpropsで親から受け取っていましたが、今回はstoreを直接参照してデータをいただきます。
}
},
data () {
return {
headers: [
{
text: 'Task Name',
align: 'left',
sortable: false,
value: 'name'
},
{
text: 'Status',
sortable: false,
value: 'status'
}
]
}
},
methods: {
TaskFinished: function (msg) { // 親コンポーネントに渡していたのでEvent渡しをしていましたが、Vuexではdispatch関数でActionsに記述した関数を呼びます。第二引数は渡したいデータ。
this.$store.dispatch('TaskFinished', msg)
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
App.vueについては今まで定義していたデータ宣言やメソッドの部分が消えてすっきりします。
<template>
<div id="app">
<v-app>
<AppBar></AppBar>
<InputTask />
<taskView />
</v-app>
</div>
</template>
<script>
import TaskView from './components/TaskView'
import InputTask from './components/InputTask'
import AppBar from './components/AppBar'
export default { // この変がスッキリしました。
name: 'App',
components: {
TaskView,
InputTask,
AppBar
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
エラーもなく、Vuex化する前と変わらずにぬるぬる動いています。
Actionsの補足
今回はあまり出番がありませんでしたが、Actions内部で下準備をすると言いました。
Actionsの役割で一番よくあるパターンが非同期処理を記述することです。
- データの更新や表示にサーバーサイドやバックエンドデータベースのデータを利用したい。
- しかもこれらの処理はユーザのアクション(ボタンクリックやエンター)に紐づけて行いたい
- ページ更新したくない
となればActionsに非同期処理を実装して、バックエンドのデータの取得や計算処理などを行い、そのデータや計算処理をMutationsに渡してStateデータの更新を行えば良いです。WebAPIなどへの接続なども良いと思います。その分エラーハンドリングも難しくなるので注意が必要です。
終わりに
今回はVuexを理解し、ToDoアプリに導入しました。次回はActionsに非同期処理を記述してバックエンドとのデータやりとりを実装する予定です。