LoginSignup
4
4

More than 3 years have passed since last update.

Table上のリソースを定期ポーリングして入れ替えていくための自分なりの実装案 on Vue.js

Last updated at Posted at 2019-05-21

前提

前回こんなことを書きました。

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.vuetr に相当する部分です。
このコンポーネントを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つの状態です。

つまりどういう動きになるの?

  1. 親コンポーネントはリソースの一覧をaxios経由で取得します
  2. これをv-forを使って、Table上に一覧化します。一覧のtrは子コンポーネントです
  3. この子コンポーネントの1つのv-forループには、イベントリスナを登録しています。このリスナ経由で、子要素からの要求に基づき関数が実行されます。
  4. その関数というのは、特定のリソースを取得するためのaxiosを利用した関数です
  5. axios経由でリソースが取得できたら、上記1の時点でのリソースの一部を置き換えます
  6. で、そもそも3で登録した関数というのはどうやると発火されるのかというと、子要素のmountedにて定義したsetIntervalでタイマーされてます
  7. 上記の6は、リソースのstatusが特定の状態である間継続されます

とりあえずこんな感じで実装してうまく動いてます。

あとはstatusの状態に応じてtableのcssとかを切り替えたりして、それっぽく見せればいいのですが、それは見栄えの話なのでここでは省略します。

以上です。

自分の中でvue.jsでこの手のロジック実装する際どうするのがいいのかなぁ・・・と当初からの悩みだったので、比較的きれいに実装できてよかったかなと。(・・・と僕は勝手ながら思っている)

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4