はじめに
Vue と Ajax を組み合わせて非同期で Json を取得するサンプルは意外と見付かりません。今回の実装もものすごく初歩的な方法ですが、自分的には丸1日悩んだ結果なので今後のためにメモを残しておきます。
むしろエレガントな方法を教えてください。
やりたいこと
- Vue インスタンス作成前ではなく、作成後にコンポーネント側で Json を取得したい。
- データは専用のストアを用意して複数のコンポーネントで使い回したい。
- しかし Vuex は制約がキツいので使いたくない。
- Json を取得したらコンポーネントに反映したい。
- コンポーネントは単一ファイルコンポーネント(.vue)を使いたい。
jQuery ではなく axios を使う
axios とは Vue のドキュメントで推奨されている Ajax通信ライブラリです。jQuery を使ってもいいのですが、せっかくなので試してみました。
axiosについては下記のページが大変詳しいです。
≫axiosを乗りこなす機能についての知見集
まずは npm をインストールします。
$ npm install --save-dev axios
ストア専用 Vue インスタンスを作る
大元の Vue インスタンスでデータを管理する方法ではコンポーネントが増えるとデータ受け渡し地獄があります。規模が大きくなるなら Vuex がおすすめのようですが、制約が厳しく、小さいアプリを作るには向いていません。
かといって Vue のドキュメントにあるシンプルな Object のストアではイベントハンドリングがちょっと面倒になります(たぶん)。
この記事では Vue インスタンスを使い、イベントハンドリングの管理も含めた専用ストアを作ります。
import Vue from 'vue';
// Ajax通信ライブラリ
import axios from 'axios';
// Json取得のベースURL
const URL_BASE = '/api/search/';
// Vue.js のインスタンス
module.exports = new Vue({
data: {
// Jsonデータ格納用
search_list: []
},
methods: {
// Ajax通信でJsonを取得し、特定のプロパティに格納する
// 取得したら GET_AJAX_COMPLETE で通知する
get_ajax(url, name) {
return axios.get(URL_BASE + url)
.then((res) => {
Vue.set(this, name, res.data);
this.$emit('GET_AJAX_COMPLETE');
});
},
// プロパティ名を指定してデータを取得
get_data(name) {
return this.$data[name];
}
}
});
解説
// Vue.js のインスタンス
module.exports = new Vue({
data: {
// Jsonデータ格納用
search_list: []
},
普通に使うなら el
パラメータで HTML上の id を指定しますが、データ管理専用なので使いません。
Jsonデータ格納用のプロパティはあらかじめ用意しておきます。本当は動的に追加させたかったのですが、上手くいきませんでした(後述)。
// Ajax通信でJsonを取得し、特定のプロパティに格納する
// 取得したら GET_AJAX_COMPLETE で通知する
get_ajax(url, name) {
return axios.get(URL_BASE + url)
.then((res) => {
Vue.set(this, name, res.data);
this.$emit('GET_AJAX_COMPLETE');
});
},
axios は Promise オブジェクトを返すので .done()
、.catch()
、.then()
などで結果を受け取ります。
引数 res
の中にステータス、ステータステキスト、データが格納されています。res.status
、res.statusText
、res.data
で取得します。
取得したデータは Vue.set()
で登録します。this.$data[name] = res.data;
と書かないのはデータが配列やオブジェクトを含んでいるため、そのまま代入するとリアクティブにならないからです(データ更新しても画面に反映されない)。
データの登録が完了したら this.$emit()
でイベント通知します。$.emit()
、$.on()
は jQuery の $.trigger()
、$.on()
と考えれば理解が早いと思います(よくわかってません)。
// プロパティ名を指定してデータを取得
get_data(name) {
return this.$data[name];
}
Vue インスタンスの $data
を直接使わせるのではなく、必ずメソッド経由にします。将来的にはプロパティ名に応じて加工したデータを返すなどが出来ますね。
追記:2017.11.3
up9cloud さんがコメントで await / async 版を投稿してくださいました。
$on()、$emit()を使うよりシンプルになります。
メインのコンポーネントから Json 取得を呼び出す
<template>
<main id="app">
<ul>
<list-item v-for="item in search_list"></list-item>
</ul>
</main>
</template>
<script>
import Vue from 'vue';
// データ管理ストア
import store from './store';
// 子コンポーネント
import ListItem from './list-item.vue';
export default {
data() {
return {
// Jsonのデータを格納
search_list: []
};
},
components: {
// 子コンポーネント登録
'list-item': ListItem
},
created() {
// Json取得
store.get_ajax('', 'search_list');
// Json取得後に呼び出される
store.$on('GET_AJAX_COMPLETE', () => {
this.search_list = store.get_data('search_list');
});
}
};
</script>
解説
// データ管理ストア
import store from './store';
先ほど作ったデータ管理スクリプトを呼び出しています。今後増える各コンポーネントでこのように呼び出せばデータ共通化が楽になります。
data() {
return {
// Jsonのデータを格納
search_list: []
};
},
データの受け入れ先を用意しておきます。
created() {
コンポーネントの初期化が完了したら Json を取得するようにします。created
の実行タイミングについては公式ドキュメントのライフサイクルが参考になります。
// Json取得
store.get_ajax('', 'search_list');
Json を取得して search_list
というプロパティに格納します。URL の部分が空なのは store.js
でベースURLを指定してあるためです。
// Json取得後に呼び出される
store.$on('GET_AJAX_COMPLETE', () => {
this.search_list = store.get_data('search_list');
});
axios による Json 取得が完了すると GET_AJAX_COMPLETE
のイベントが発行されます。$on('GET_AJAX_COMPLETE', function(){})
によって受け取り、データに格納します。
ハマりポイント
①コンポーネントの data プロパティについて勘違いしていた
公式ドキュメントに「data は関数でなければならない」と書かれていたので**「コンポーネントってデータ格納できないの?」**と思っていました。
しかし下記のスクリプトからわかるように、data
に格納はできるようです。
this.search_list = store.get_data('search_list');
②コンポーネントの props プロパティにデータを直接入れると警告が出る
上記①が判明する前は props
にデータを格納するのかと考えていました。
export default {
props: {
search_list: Array
},
components: {
// 子コンポーネント登録
'list-item': ListItem
},
〜略〜
// Json取得後に呼び出される
store.$on('GET_AJAX_COMPLETE', () => {
// props のプロパティにデータを格納
this.search_list = store.get_data('search_list');
});
しかし本来 props
は親から受け取ったデータを格納する場所なので下記のような警告が出ました。
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "search_list"
③Vue.set() を使わないと反映されない
当初のストアは Vue インスタンスではなくただのオブジェクトでした。
new Vue({
el: '#app',
data: store.data // 親 Vue インスタンスのデータとして登録
〜略〜
})
module.exports = {
data: [],
get_ajax(url, name) {
// 〜 中略:axios によるデータ取得 〜
// ストアデータは更新されるが、Vueには反映されない
this.data = jsondata;
}
}
データを格納するのに Vue のメソッドを使わず直接代入したら、ストアデータは更新されているのに、画面に反映されていませんでした。
これは、Vue.set(Vueインスタンス, プロパティ名, データ)
を使うことで解消されました。
④データ格納先プロパティはあらかじめ用意する必要がある
あとから自由にデータが追加できるように、data
プロパティに中身は記載していませんでした。
// Vue.js のインスタンス
module.exports = new Vue({
data: {},
しかしこの状態で Vue.set()
を使っても下記のようなエラーが出るだけです。
[Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.
これはあらかじめ登録先を用意することで解消されました。無計画はいけないってことですねw
// Vue.js のインスタンス
module.exports = new Vue({
data: {
// Jsonデータ格納用
search_list: []
},
おわりに
Vue.js は便利ですが、一歩先に進むとまだまだわからないことだらけです。日本語の情報もまだまだ少ないです。皆さんの知見が共有されていくことを願います。