前提
前回こんなことを書きました。
vue.jsで、v-forしながらそのindexを利用してclassを切り替えたいような場合
ここでは、
- リソースの一覧をTable表示し
- その上で各行に存在するリソースの操作メニューを別コンポーネントに切り出した
ということをやりました。
ここまでやるなら、リソース1つ1つ(table.tbody.trに相当)もComponentに切り出しちゃうほうが良いですよね。
で、そうしておくと、リソースのポーリングなんてのもすごくやりやすくなるなぁと思ったので、ちょっと書いてみます。
今回の記事の位置づけ
- 前回に加えてTableの各行に相当するリソースも別コンポーネントに切り出す
- その上でリソースの状態に応じてリソースを定期的にポーリングする
- ・・・という機能の実装案としてのメモ
例えば、
- OpenStackでインスタンスを作ったとします
- けどインスタンスがACTIVEになるまで、一覧上のリソースはぐるぐるリソースの状態をポーリングし続けますよね
- ではこれを実際に作るとどうなるかな
というような記事です。
分割していくとこうなった
結果的に、以下のような3つのコンポーネントに分かれました。
なお前回も書きましたが、私はBulma.jsを使ってますので、要素もそれを踏まえて読んでいただければ。
なお、今回は操作メニューについてのコードはあまり関係がないので省略します。
以下実際のコードです。
ポイントはコードの下に書きます。
List.vue
Tableでリソースの一覧を表示するためのコンポーネントです。
Tableの定義をして、子コンポーネントを呼び出し、そこにAPIで取得したリソースを埋め込むのがお仕事です。
<template>
<span>
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{{ $t('ID') }}</th>
<th>{{ $t('Name') }}</th>
<th>{{ $t('Description') }}</th>
<th>{{ $t('Status') }}</th>
<th>{{ $t('') }}</th>
</tr>
</thead>
<tbody>
<list-element
v-for="r in resources"
:id="r.id"
:key="r.id"
:name="r.name"
:description="r.description"
:status="r.status"
@resource-status-update="updateResource(r.id)"
/> <!-- ポイント(1) -->
</tbody>
</table>
</span>
</template>
<script>
import client from '~/clients'
import ListElement from './ListElement'
export default {
components: {
ListElement,
},
data() {
return {
resources: null,
}
},
async mounted() {
try {
let res = await client.listResources()
this.resources = res.data
} catch (error) {
(省略)
}
},
methods: {
updateResource: async function(pollingTargetID) { // ポイント(2)
console.log(`Received polling request about ${pollingTargetID}`)
let res = await client.getResource(pollingTargetID)
let polledResource = res.data
let copiedResources = [...this.resources]
for (let i = 0; i < this.resources.length; i++) {
if (this.resources[i].id == pollingTargetID) {
copiedResources[i] = polledResource
}
}
this.resources = copiedResources
},
},
}
</script>
ListElement.vue
上の List.vue
の tr
に相当する部分です。
このコンポーネントをv-forしてリソースの一覧を記述します。
この子の仕事は、
- 親から与えられたリソース情報(プロパティ経由)を用いたtrタグのレンダ
- trに記載されるリソースがポーリングに値する状態であれば、ポーリング要求を親に(つまりList.vueに)飛ばす(※1)
です。
余談:直接子コンポーネントで書き換えちゃえばいいじゃない・・・と思いがち(自分も思ってた)
ここで※1についてですが、子コンポーネントでポーリングしてリソース情報を書き換えてしまえばいいのでは・・・と思いがちなのですが、親から与えられたプロパティを子コンポーネント側で書き込むのは非推奨なのですね。
以下のようなWarningが出てしまいます。
[Vue warn]: 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: "propsValue"
いろいろ記事はあると思いますが例えば https://stackoverflow.com/questions/40780730/vue-js-changing-props とか。
Vue.js自体のドキュメントにも記載があったはず。
ということで、親から変更を かけてもらう
必要があるわけです。
ということでコードコード。
<template>
<tr :class="{ 'is-status-update': isStatusUpdate }">
<td>{{ id }}</td>
<td>{{ name }}</td>
<td>{{ description }}</td>
<td>
{{ status }}
<progress v-if="isStatusUpdate" class="progress is-small is-link" max="100">
15%
</progress>
</td>
<td>
<list-operation :id="id" :status="status" />
</td>
</tr>
</template>
<script>
import ListOperation from './ListOperation' // 前回説明したところ
export default {
components: {
ListOperation, // 前回説明したところ
},
props: {
id: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
status: {
type: String,
default: '',
},
},
data() {
return {
pendingStatus: ['PENDING_CREATE', 'PENDING_DELETE'] // 作成中、削除中みたいな意味
}
},
computed: {
isStatusUpdate: function() {
return this.pendingStatus.includes(this.status)
},
},
mounted() {
const self = this
// start polling request to parent component
setInterval(function() { // ポイント(3)
if (settings.pendingStatus.includes(self.status)) {
console.log(`polling request has emitted about ${self.id}`)
self.$emit('resource-status-update')
}
}, 2500) // 2500 msでポーリング
},
}
</script>
コードのポイント
ポイント(1) @ 親コンポーネント
ここで子コンポーネントをv-forすることで要素をレンダしてます。
その際、リソースの情報をプロパティ経由で渡しています。
また、 resource-status-update
というイベントに対するリスナを登録し、その際 updateResource(r.id)
とすることで、該当のリソースのIDを利用して関数を呼び出しています。
ポイント(2) @ 親コンポーネント
ここで client.getResource(pollingTargetID)
を定義してます。
- この関数はリスナなので、子コンポーネントからの要求に基づき発火します
- pollingTargetIDはv-for内のkeyなので、リソースごとに異なります
-
client.getResoure
というのは、axios経由でリソースを取得するための関数です - これが取得できたら、親コンポーネントのリソース一覧である、
this.resources
の一部を入れ替えています
ポイント(3) @ 子コンポーネント
- mounted(DOMがマウントされた時点)で、setIntervalを仕込みます
- setIntervalの中では、単にイベントだけを送出します(to 親コンポーネント)。(逆にそれ以外は何もしません)
- ただし、リソースのstatusが特定の状態であるときのみです。その特定の状態が、
this.pendingStatus
であり、実際には['PENDING_CREATE', 'PENDING_DELETE']
という2つの状態です。
つまりどういう動きになるの?
- 親コンポーネントはリソースの一覧をaxios経由で取得します
- これをv-forを使って、Table上に一覧化します。一覧のtrは子コンポーネントです
- この子コンポーネントの1つのv-forループには、イベントリスナを登録しています。このリスナ経由で、子要素からの要求に基づき関数が実行されます。
- その関数というのは、特定のリソースを取得するためのaxiosを利用した関数です
- axios経由でリソースが取得できたら、上記1の時点でのリソースの一部を置き換えます
- で、そもそも3で登録した関数というのはどうやると発火されるのかというと、子要素のmountedにて定義したsetIntervalでタイマーされてます
- 上記の6は、リソースのstatusが特定の状態である間継続されます
とりあえずこんな感じで実装してうまく動いてます。
あとはstatusの状態に応じてtableのcssとかを切り替えたりして、それっぽく見せればいいのですが、それは見栄えの話なのでここでは省略します。
以上です。
自分の中でvue.jsでこの手のロジック実装する際どうするのがいいのかなぁ・・・と当初からの悩みだったので、比較的きれいに実装できてよかったかなと。(・・・と僕は勝手ながら思っている)