【nuxt.js vue.js】vue-infinite-loadingとRailsで作った自作APIをつなぎこむ【無限スクロール】

プログラミング

Nuxt.jsと自作のAPIとつなぎこんだ際に無限スクロールを実装したのでそのTIPSをまとめます

無限スクロールでググると静的なデータとかで使ってみたとかばかりの記事が多く、自作APIとのつなぎ込みの情報がなかったのでTIPSで残しておければと思います。

やっち
やっち

キャプ画DBの無限スクロール実装しようと思ったけどくっそ苦戦したので

※フロントエンド初心者なのでミスや誤表記がある可能性がありますのでご了承ください

vue-infinite-loading とは

簡単に無限スクロールを実装できるvueのプラグインです。

PeachScript/vue-infinite-loading
An infinite scroll plugin for Vue.js. Contribute to PeachScript/vue-infinite-loading development by creating an account on GitHub.

無限スクロールとは↓みたいなやつで、下までスクロールしたら新たに読み込むやつです。

導入は簡単です!以下のコマンドを実行

npm install vue-infinite-loading -S

`plugings/infiniteloading.js` を作成し、以下を記述

import Vue from 'vue'
import InfiniteLoading from 'vue-infinite-loading'

Vue.component('infinite-loading', InfiniteLoading)

`nuxt.config.js` のplugins: に以下を追記

plugins: [
    { src: '~/plugins/infiniteloading', ssr: false }
  ],

おおよそこんな手順を踏むことで、infinite-loadingコンポーネントを用いて無限スクロールの導入をすることが可能です。

<infinite-loading v-if="searchStatus()" ref="infiniteLoading" spinner="waveDots" @infinite="infiniteHandler">

ここまでは、おおよそvue / nuxt で無限スクロールしたいと思って調べたらたどり着けます。

そしておおよそのサイトでは静的なデータをfor文で生成して無限スクロールしてて今回自分がやりたいAPIとの繋ぎ込みに関しては情報が少なかった印象です。

作ったAPIについて

ここで、一旦つなぎこむAPIについて補足しておきます。

仕様的なやつを簡単にまとめました。

Ruby on Rails 5系のSerializerとkaminariを用いてjsonを返すAPI

同形式のリストを取得

一回の最大24件取得しリクエストのパラメータに `page=2` と指定して続きのデータを取得

続きのデータが無くなった際にはレスポンスは [] のように空リストが返ってくる

本来ならページ数や最大データ数とか返すAPIが良心的なのでしょうが、今回は実装がめんどかったので直にデータのみが返ってくるシンプルなAPIを使っています。

やっち
やっち

本職はこっち(Rails)なのですが、こだわりが無いあたりちょっとアレですねごめんなさい。シンプルなAPIということでゆるして

躓いたところ

個人的にやりたかったこととしては、

  • 検索ボタンを押すとAPIを叩いて取得したデータを表示
  • 下までスクロールすると続きのページでAPIを叩いて更に表示
  • データの末尾まできたら1回以上APIを叩かない
  • 新たに検索したらもう一度無限スクロールが有効になる

ということがやりたかったのですが、適当にぐぐって実装したら死にまくりました。

死因は、 無限スクロールのコールバック関数が無限に呼ばれ続ける というものでした。

原因は簡単で、 検索文字列の下部に検索結果を表示させる際のv-forの外側においた vue-infinite-loadingコンポーネントちゃんが、検索される前から無限に呼ばれてました。

やっち
やっち

最初気づかなくてブラウザがクラッシュしたりPCが再起動しました(涙目)

vue-infinite-loading の仕様を確認ダァ

次に上記の問題を解決する際の仕様をまとめていきます。

勘違いあったらすまんそん

やっち
やっち

おおよそstack over flowのおかげで気づけました(ドキュメントを嫁)

infinite-loadingコンポーネントは、無設定だとその箇所に到達したらコールバック関数を呼び続ける(今回の死因、別のパラメータを使って制御する必要あり)

infinit-loadingのコールバック関数には $state で状態を受け取れる(コールバック関数内での状態処理に使用)

コールバック関数内では $state.loaded() でロード完了、$state.complete()で全てのデータを取得完了という状態に遷移可能。(コールバック関数内での状態処理に使用)

今回のコンポーネントの定義だと、コールバック関数外からはinfinite-loadingの状態はthis.$refs.InfiniteLoading で取得 or 操作が可能(再検索で使用)

コンポーネント外からは this.$refs.infiniteLoading.stateChanger.reset() でinfinite-loadingの状態をリセットできる。(再検索で使用)

以上を踏まえて今回の目的を達成するためには以下のことをする必要がありそうです。

infinite-loadingのコールバック関数が無限に呼ばれないように制御するパラメータの適切な制御(search_status)

APIを叩くためのページネーションを管理する変数の適切な制御(page)

infinite-loadingの状態の適切な制御

以上を踏まえて生成されたもの

ということで以上を踏まえて生成されたコード`index.vue`です。今回の說明に関係ない箇所は省略しています。

コメントを入れたので良かったら参考にしてください。

<template>
  <section class="container">
    <div class="columns">
      <input type="text" placeholder="キーワード" v-model="query">
      <button v-on:click="searchImages()" class="button">検索</button>
    </div>
    <div class="columns is-multiline">
      <div v-for="image in images" :key="image.id" class="column is-4">
       // 表示部分
      </div>
    </div>

    <infinite-loading v-if="searchStatus()" ref="infiniteLoading" spinner="waveDots" @infinite="infiniteHandler">
      <span slot="no-more"/>
      <span slot="no-results"/>
    </infinite-loading>
  </section>
</template>

<script>
  export default {
    data: function() {
      return {
        query: "", // 検索文字列
        images: [], // 取得データ
        page: 1, // ページ
        search_status: false // infinite-loadingを叩くか制御するフラグ
      }
    },
    methods: {
      // 検索ボタンを押した後の処理(後のinfinite-logingと処理が重複しているため冗長感はある)
      searchImages: function() {
        if(this.query != "") { // 検索文字列が空だったらAPI叩かない
          this.page = 1; // 再検索用に初期化
          this.$axios.$get(`/api/images?q=${this.query}`)
            .then(json => {
              this.images = json; // 再検索時に前の検索結果を消すために代入
              this.search_status = true; // 結果が取得できたらinfinite-loading発動
            })
            .catch(e => ({ error: e }))
          if(this.$refs.InfiniteLoading){ // ← 再検索時にinfinite-loading残ってたら初期状態にする
            this.$refs.infiniteLoading.stateChanger.reset();
          }
        }
      },
      infiniteHandler: function($state) { // infinite-loadingコールバック関数
        this.page += 1; // この関数で叩くAPIは2ページ目以降
        this.$axios.$get(`/api/images?q=${this.query}&page=${this.page}`)
          .then(json => {
            if(json.length > 0) { // 結果が空じゃない場合
              this.images.push(...json); // 取得データを追記
              $state.loaded(); // infinite-loading状態遷移
            } else { // 結果が空(データの末尾に到達)
              $state.complete(); // infinite-loading状態遷移
              this.search_status = false; // infinite-loadingを止めておく(冗長説)
            }
          })
          .catch(e => ({ error: e }))
      },
      searchStatus: function() { // v-ifで管理されるinfinite-loadingの制御フラグ取得関数
        return this.search_status;
      }
    }
  }
</script>
やっち
やっち

見苦しいコードですんません。初心者なので許してヒヤシンス

できたものは以下の動画になります。

今回のものと別のこともやってますが多目に見てください。

所感

やっち
やっち

フロントエンドって難しいですけど、感覚をつかめればスイスイ進める印象です。あと、成果物が視覚的に見せられるので非常に楽しいです。

今後も週末だけでも手を動かしたいでしゅ

コメント

タイトルとURLをコピーしました