こんにちは。
エンジニアのNkawaKです。
本記事ではSPAとVue.jsについて前回の記事で書ききれなかった内容を解説していきます。
今回はVue.jsの文法とVuexに焦点を当てて解説します。
Vue.jsのテンプレート構文
テンプレート構文とは、その名の通りVueコンポーネントの<template>タグの中で使用できる構文のことです。前回のように、以下のサンプルアプリのソースコードを解説していきます。
<template>タグの中では様々な構文が使えますが、そのなかでも最も基本的な例がマスタッシュ構文でしょう。MemoForm.vueのソースコードをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
<template> <div> <h5>ようこそ、{{ name }}さん</h5> <form @submit.prevent="postMemo"> <label for="memo">メモ</label> <div> <textarea id="memo" cols="30" rows="10" v-model="memo.text"></textarea> </div> <span v-if="error.inValid">メモを入力してください</span> <div> <button type="submit">メモる</button> </div> </form> <MemoList :items="this.memoList" @delete="deleteMemo"/> <RouterLink to="/">トップに戻る</RouterLink> </div> </template> <script> import MemoList from "./MemoList"; export default { components: { MemoList }, computed: { name() { return this.$store.getters["name"]; } }, data() { return { memo: { id: null, text: "" }, error: { inValid: false }, memoList: { memos: [] } }; }, mounted: function() { this.$nextTick(function() { this.memoList.memos = JSON.parse(localStorage.getItem(this.name)) || []; }); }, methods: { postMemo() { if (this.memo.text === "") { this.error.inValid = true; return; } else { this.error.inValid = false; } const text = this.memo.text; const memosLength = this.memoList.memos.length; this.memoList.memos.push({ id: memosLength === 0 ? 0 : this.memoList.memos[memosLength - 1].id + 1, text: text }); this.memo.text = ""; }, deleteMemo(memoId) { const afterMemos = this.memoList.memos.filter(memo => { return memo.id !== memoId; }); this.memoList.memos = afterMemos; } }, watch: { memoList: { handler(newVal) { localStorage.setItem(this.name, JSON.stringify(newVal.memos)); }, deep: true } } }; </script> |
<template>の中でマスタッシュ構文{{ }}を書くと変数の中身や文字列、関数の戻り値などをコンポーネント内に描画することができます。
例えば、上のコンポーネントでは”<h5>ようこそ、{{ name }}さん</h5>”の部分に、トップページで入力された名前を表示しています。
また各コンポーネントは固有のデータを持つことができます。それが<script>内のdata()の部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
data() { return { memo: { id: null, text: "" }, error: { inValid: false }, memoList: { memos: [] } }; } |
memoはメモを識別するためidと入力されたメモがtextをパラメータとして持っていて、errorはメモが何も入力されずに送られたらtrueが入ります。memoListは今まで入力されたメモの一覧が配列に入れられています。
これらのデータを表示したり、条件分岐をしたり、メモ一覧を画面に展開しています。
次にv-modelという構文を紹介します。v-modelを使えばコンポーネントが持つデータとフォームをマッピングすることが簡単にできます。このコンポーネントではテキストエリアとmemo.textをマッピングしています。
1 |
<textarea id="memo" cols="30" rows="10" v-model="memo.text"></textarea> |
v-model=”memo.text”と書くことで、このテキストエリアに入力された内容がリアルタイムでdata()のmemo.textにも入力されます。素のJavaScriptやjQuery ではonChangeなどのイベントでテキストの内容を取得する関数を実行する必要がありますが、Vue.jsではこのように簡単にフォームの情報を取得して更新を行うことができます。
さらにv-ifという構文を使って、コンポーネント内で条件分岐をすることができます。
例えば、このアプリではトップページやメモ入力欄で何も入力しないでボタンを押すと入力を促すエラーメッセージが表示されます。このような特定の条件でだけ表示をする場合にv-ifを使います。
実際にエラーメッセージが表示される流れをソースコードを追いながら、見ていきましょう。
メモるボタンを押すとメモを追加するための関数(postMemo)が走ります。
1 |
<form @submit.prevent="postMemo"> |
postMemoは以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
postMemo() { if (this.memo.text === "") { this.error.inValid = true; return; } else { this.error.inValid = false; } const text = this.memo.text; const memosLength = this.memoList.memos.length; this.memoList.memos.push({ id: memosLength === 0 ? 0 : this.memoList.memos[memosLength - 1].id + 1, text: text }); this.memo.text = ""; } |
this.memo.textはdata()内のmemo.textです。テキストエリアとマッピングされているので、なにも入力されなかった場合、空文字(“”)のままです。空文字なら条件分岐のtrueとなり、error.inValidにtrueが代入されて、この関数をreturnで終了します。
error.inValidがtrueになったことでv-ifの条件もtrueになり、エラーメッセージが表示されます。
1 |
<span v-if="error.inValid">メモを入力してください</span> |
逆にメモが入力されていた場合は、error.inValidはfalseのままで、data()のmemoList.memosの配列に他のメモと被らないidとテキストの内容をオブジェクトにしてpushします。その後、this.memo.text = “”;で空文字を代入して、テキストエリアを空にします。
Vue.jsはpush、map、filterなどの配列操作を行う関数が実行されると、data()内の配列の変更を検出し、自動でコンポーネントに表示してくれます。今回の例ではmemoList.memosにpushが行われたので、新しく追加されたメモが画面に表示されれます。
最後にv-forという構文を解説します。v-forは<template>の中で反復処理を行うための構文です。
今回のアプリではメモの一覧を表示するために使っています。
1 |
<MemoList :items="this.memoList" @delete="deleteMemo"/> |
インポートしたMemoListというコンポーネントにdata()のmemoListとdeleteMemoという関数を渡しています。
コンポーネントの中で別のコンポーネントを使用するためには次のようにコンポーネントをインポートした後で、コンポーネントを使用することを宣言する必要があります。
1 2 3 4 5 |
import MemoList from "./MemoList"; export default { components: { MemoList } |
MemoList.vueの中身を見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<template> <div> <div v-for="item in items.memos" :key="item.id"> <span>{{ item.text }}</span> <button @click="emitDelete(item.id)">削除</button> </div> </div> </template> <style> div { white-space: pre; } </style> <script> export default { props: { items: { type: Object, required: false } }, methods: { emitDelete(memoId) { this.$emit("delete", memoId); } } }; </script> |
<template>内でv-forを使ってMemoForm.vueから渡されたメモ一覧を展開しています。
1 2 3 4 |
<div v-for="item in items.memos" :key="item.id"> <span>{{ item.text }}</span> <button @click="emitDelete(item.id)">削除</button> </div> |
v-forが書かれたdivの部分がメモの数だけ繰り返し表示されます。
コンポーネントから別のコンポーネントにデータを渡す場合にはpropsというプロパティを設定する必要があります。以下の部分でpropsを定義しています。
1 2 3 4 5 6 7 |
export default { props: { items: { type: Object, required: false } } |
propsのitemsというプロパティはオブジェクト型でrequiredはfalseに設定しています。
requiredというプロパティは必須項目かどうかを表しています。今回の例では、メモが何もない場合もあるので、falseにしています。
またもう一点注意すべきことがあります。MemoForm.vue内にMemoList.vueというコンポーネントを表示し、データを渡しているため、MemeForm.vueが親でMemoList.vueが子という親子関係が成立しています。
こうした親子関係が成り立っているため、基本的に親からpropsで渡されたデータは子のコンポーネント側で直接操作することができません。
子のコンポーネントでpropsのデータを操作する場合、$emitを使って親のコンポーネントに定義された関数を実行してデータを操作します。
1 2 3 4 5 6 7 |
<button @click="emitDelete(item.id)">削除</button> methods: { emitDelete(memoId) { this.$emit("delete", memoId); } } |
上記の削除ボタンが押されるとemitDeleteという関数が実行され、this.$emitで親のコンポーネントに
関数の実行を通知しています。this.$emit()の第1引数の文字列が実行するイベント、第2引数以下が関数に渡すデータとなっています。
propsとthis.$emitを解説したところで、もう一度親のコンポーネントを確認しておきましょう。
1 2 3 4 5 6 7 8 |
<MemoList :items="this.memoList" @delete="deleteMemo"/> deleteMemo(memoId) { const afterMemos = this.memoList.memos.filter(memo => { return memo.id !== memoId; }); this.memoList.memos = afterMemos; } |
MemoListの:itemsでは子のコンポーネントで定義したprops.itemsにメモ一覧を渡しています。
@delete=”deleteMemo”の部分で@deleteというイベントにdeleteMemoを登録しています。
子のコンポーネントではthis.$emit(“delete”, memoId)という形で関数を実行していましたが、第一引数の文字列は@の部分に登録されたdeleteと繋がっていることが確認できたと思います。
$emitを通じてdeleteMemoが実行されると、引数で渡されたidに該当するメモをfilterで除いて、新しくmemoList.memosの配列に代入しています。以上のようなプロセスを経ることで、子のコンポーネントから親のデータを操作することができました。
mounted、computed、watch
テンプレート構文の次に<script>の部分で使うメソッドについても解説しておきます。
今回のアプリで使用したmounted、computed、watchの3つを解説します。
mounted
mountedはコンポーネントがマウントされた直後に呼び出されるメソッドです。
1 2 3 4 5 |
mounted: function() { this.$nextTick(function() { this.memoList.memos = JSON.parse(localStorage.getItem(this.name)) || []; }); } |
コンポーネントがマウントされた直後にローカルストレージに保存しているメモ一覧を取得して、
data()のmemoListに代入しています。
ちなみに$nextTickは画面全体が読み込まれた後で、処理を実行するためのメソッドです。
computed
computedは算出プロパティとも呼ばれていて、計算や加工を行った値を返すために使われます。
1 2 3 4 5 6 7 |
<h5>ようこそ、{{ name }}さん</h5> computed: { name() { return this.$store.getters["name"]; } } |
今回のアプリではログインしたユーザーの名前を表示するためにしか使っていないですが、より複雑な処理を行うこともできます。
1 2 3 4 5 6 |
computed: { extension() { const nameArray = this.selectedFile[0].name.split('.'); return nameArray[nameArray.length - 1]; } } |
最近の業務では選択されたファイルの拡張子を返す、算出プロパティを書きました。
watch
watchはウォッチャと呼ばれる、特定のデータの監視を行うためのメソッドです。
1 2 3 4 5 6 7 8 |
watch: { memoList: { handler(newVal) { localStorage.setItem(this.name, JSON.stringify(newVal.memos)); }, deep: true } } |
今回のアプリでは、メモ一覧を監視するために使用しています。新しいメモが追加されたらローカルストレージにも保存する処理が非同期で実行されます。またdeep: trueというオプションを付けることでネストされたプロパティも監視することができます。
Vuex
最後にVuexというVue.jsのコアライブラリを解説します。
SPAのような画面のリロードを行わないアプリケーションでは、リロードの都度セッションから情報を取得することができません。またVue.jsでコンポーネント間のデータの受け渡しは、propsやemitのみで行うため、親子関係でないコンポーネントにデータを渡す事ができません。
そのうえコンポーネントの階層が深くなると、propsとemitのデータの受け渡しがバケツリレーのような煩雑な作業になってしまいます。これらの問題を解決するものがVuexです。
Vuexを使うことで、SPAの状態(ログインしているユーザー情報など)をコンポーネント全体で管理することが可能になります。
Vuexの概要について解説したところで、Vuexのソースコードを見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const state = { name: null }; const getters = { name: state => (state.name ? state.name : "") }; const mutations = { setName(state, name) { state.name = name; } }; const actions = { login(context, name) { context.commit("setName", name); } }; export default { state, getters, mutations, actions }; |
Vuexのコードはstate、getters、mutation、actionsの4つのオブジェクトから構成されています。
stateは管理されているデータの状態です。初期値はnullになっていますが、ログインされたユーザー名を設定して管理します。
gettersは名前の通り、stateの値を取り出すためのゲッターです。state.nameに値が入っていればそのまま名前を取得し、なければ空文字を返しています。
stateは直接値を代入することができません。値を設定するためにはmutationsとactionsを使って間接的に設定します。
mutationsはstateを同期処理でstateを更新するために使用します。state.nameに値をセットするためのsetNameを定義しています。
mutations内に定義する関数は、第一引数にstateを、第二引数にセットする値を設定します。
actionsもstateに値を設定するために使用しますが、actionsでは非同期処理を行えることがmutationsとの違いです。
今回のアプリはサーバーと通信してデータを取得していないので、同期処理で関数を定義しています。
非同期処理を行う場合は以下のように書くこともできます。
1 2 3 4 5 6 |
const actions = { async login(context, formData) { const response = axios.post("/api/login", formData); context.commit("setName", response.data); } }; |
ログインフォームに入力された値をサーバーに送信して、返信されてきたユーザー情報をmutationsのsetNameを使ってセットしています。
actionsに定義する関数は第一引数にcontextを設定します。第一引数のcontextからcommitというメソッドを実行することで
mutationsで定義した関数を実行することができます。commitの第一引数は実行したいmutationsの関数名を指定します。
Vuexのコードの定義を確認したので、次にコンポーネント側でVuexの関数を実行するための前準備を見ておきましょう。
user.jsで定義したVuexの4つのオブジェクトをコンポーネントで使うために、
state、getters、mutations、actionsを一つのオブジェクトにまとめてエクスポートします。
1 2 3 4 5 6 |
export default { state, getters, mutations, actions }; |
次にエクスポートしたオブジェクトをstore/index.jsでインポートします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import Vue from "vue"; import Vuex from "vuex"; import user from "./user"; Vue.use(Vuex); const store = new Vuex.Store({ modules: { user } }); export default store; |
Vue.use(Vuex)でVuexの使用を宣言し、new Vuex.storeでVuexのインスタンスを作成します。
Vuexのインスタンスのmodulesにエクスポートしたuser.jsのオブジェクトを含めることで、user.jsで定義した関数はインスタンスのメソッドとして登録されます。次にここで作成したインスタンスのstoreをエクスポートします。
最後にエクスポートしたインスタンスをmain.jsのVueインスタンスに含めることで、
全てのコンポーネントでVuexの機能を使用することができるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 |
import Vue from "vue"; import router from "./router"; import store from "./store"; import App from "./App.vue"; new Vue({ el: "#app", router, store, components: { App }, template: "<App/>" }); |
最後にコンポーネント側でVuexを使用している部分を見ておきましょう。
Top.vueでは入力されたユーザー名をVuexのstateに設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<template> <div> <h2>簡単メモ帳</h2> <form @submit.prevent="login"> <label for="name">お名前</label> <div> <input type="text" id="name" v-model="nameForm.name"> </div> <span v-if="error.inValid">お名前を入力してください</span> <div> <button type="submit">利用する</button> </div> </form> </div> </template> <script> export default { data() { return { nameForm: { name: "" }, error: { inValid: false } }; }, methods: { login() { if (this.nameForm.name === "") { this.error.inValid = true; return; } else { this.error.inValid = false; } this.$store.dispatch("login", this.nameForm.name); this.$router.push("/memo"); } } }; </script> |
トップ画面で名前が入力されて、利用するボタンが押されるとコンポーネントのlogin()が実行されます。
this.$store.dispatch(“login”, this.nameForm.name)の部分でuser.jsのactionsに定義したloginを実行し、入力された名前をstateに設定します。
actionsの関数を実行する場合はdispatchというメソッドを使用します。この時、第一引数はactionsに定義した関数名を文字列で指定します。
この部分ではactionsを使用していますが、非同期処理を使用していないので、mutationsでもstateを設定できます。
mutationsを使用する場合は$store.commit(“setName”, this.nameForm.name)と書きます。
第一引数でmutations内の実行したい関数名を文字列で指定するのは、mutationsと同じです。
MomeForm.vueではgettersを使って、stateを取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<template> <div> <h5>ようこそ、{{ name }}さん</h5> <form @submit.prevent="postMemo"> <label for="memo">メモ</label> <div> <textarea id="memo" cols="30" rows="10" v-model="memo.text"></textarea> </div> <span v-if="error.inValid">メモを入力してください</span> <div> <button type="submit">メモる</button> </div> </form> <MemoList :items="this.memoList" @delete="deleteMemo"/> <RouterLink to="/">トップに戻る</RouterLink> </div> </template> <script> import MemoList from "./MemoList"; export default { components: { MemoList }, computed: { name() { return this.$store.getters["name"]; } }, |
gettersを使って取得した名前を、算出プロパティとして返しています。
this.$store.getters[“name”];で、user.jsのgettersに定義したプロパティを文字列で指定し、stateを取得します。
まとめ
- コンポーネントの<template>の中ではテンプレート構文を使って、変数の表示や条件分岐、反復を行うことができる。
- mounted、computed、watchなどを使うことでデータの加工や監視をしたり、コンポーネントがマントされた直後の処理を書ける。
- Vuexを使うことで、アプリケーションの状態を管理し、全てのコンポーネントで状態を取得したり更新ができる。
Vue.js解説は以上になります。Vue.jsを学習する際の参考になれば幸いです。
前編と後編を通じて、ここまで長らくお読みいただきありがとうございました。
- 【React】フロントエンドのテストコードを書いてみよう【Vitest】 - 2024-04-30
- React Hooks入門ガイド - 2022-03-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【後編】 - 2020-02-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【前編】 - 2019-08-05