LoginSignup
8
6

More than 3 years have passed since last update.

【Rails6】kaminariでページネーションしてVue.jsで無限スクロール(vue-infinite-loading)を導入する。

Last updated at Posted at 2020-08-23

事前準備

バックエンドはRails6を使いフロントはVueを使いますので、
Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門
こちらの方の記事を参考にVueを導入してあるのが前提とさせていただきます。
Vueの導入・基本的な書き方などは省かせていただきます。

またkaminariに関しては、
【rails】kaminariを使ってページネーションを作る
こちらの方の記事を参考にお願いいたします。

開発環境

  • Ruby on rails 6.0.3.2
  • vue 2.6.11
  • yarn 1.22.4
  • webpack 4.43.0

こちらの環境で動作確認しました。

詰まったポイント

Vue側でvue-infinite-loadingというプラグインを用いて実装していきます。
そこで詰まったところがrailsのモデル側でページネーションしておらず全データを渡してしまっていたため、
無限スクロールを行おうとしたら全データが表示されてしまい意味がない状態になってしまいました。
なのでrailsの便利Gemである「kaminari」でページネーションして少しずつデータをVueに送る訳です。

ルーティング

config/routes.rb
Rails.application.routes.draw do

#api用
  namespace :api, {format: 'json'} do
    namespace :v1 do
      resources :videos, only: [:index]
    end
  end
end

今回必要な部分のみ伐採しております。

kaminariをインストールする

gem 'kaminari'

kaminariをまずはGemfileに記述します。

$ bundle install

bundle installでインストールを実行する。

JSONをpagenationさせる

app/controllers/api/v1/videos_controller.rb
class Api::V1::VideosController < ApiController
  include Pagenation  #pagenation_controllerにて定義

  def index
    videos = Video.all.page(params[:page]).per(5)
    pagenation = resources_with_pagination(videos)  #pagenation_controllerにて定義
    @videos = videos.as_json
    object = { videos: @videos, kaminari: pagenation }  #{}でjsonを複数渡せる
    render json: object
  end

end

まずはコントローラーでJSON化する記述を書いていきます。
.page(params[:page]).per(5)で、URLにpage=1というオプションが与えられたら上から5つのデータを取り出すということになります。
また、page=2というオプションが与えられたら続きの5件のデータを取り出します etc...

pagenation = resources_with_pagination(videos)
こちらはkaminariの便利メソッドを呼び出すものでして次に説明します。

object = { videos: @videos, kaminari: pagenation }
{}に入れることによって@videosとkaminariの内容を同時にJSON化できます。

試しにhttp://localhost:3000/api/v1/videos?page=1 にアクセスしてみてください。

※初歩的なことですが僕がつまずいた注意点を書いておきます。
render json: @videosとした場合はVue側で

例1
  mounted () {
    axios
      .get('/api/v1/videos')
      .then(response => (this.videos = response.data))
    }

とやってaxiosからvueに@videosのデータを入れられます。

ただし、今回の場合は
object = { videos: @videos, kaminari: pagenation }
render json: object
オブジェクト{}の中に配列が2つ入っているので同じように@videosのデータを取り出す場合は

例2
  mounted () {
    axios
      .get('/api/v1/videos')
      .then(response => (this.videos = response.data.videos))
    }

response.data.videos と.videosを追加しなければならないことに注意してください。
逆にkaminariのデータが欲しい場合はresponse.data.kaminari とすればOKですね。

kaminariのメソッドを定義する

先程出てきたresources_with_pagination()を説明していきます。
無限スクロールをするときに
- 今は何ページなのか
- 今どこまで読み込んだのか
- 次のページは何ページか
- 全部で何ページなのか
などの情報がないと無限スクロール側が何を読み込めばいいのか、いつ止めればいいのかなどが分かりません。

それをkaminariがやってくれます。
まずは、app/controllers/pagenation.rbを作成します。

app/controllers/pagenation.rb
module Pagenation
  class Api::V1::VideosController < ApiController 
    def resources_with_pagination(resources)
      {
        pagenation: {
          current:  resources.current_page,
          previous: resources.prev_page,
          next:     resources.next_page,   
          limit_value: resources.limit_value,
          pages:    resources.total_pages,
          count:    resources.total_count
        }
      }
    end
  end
end

これでページネーションの情報をJSON化して渡すことができます。
RailsAPIでページネーションに対応したJSONを返す(kaminari使用)
こちらの記事が参考になりました。

ローカルホスト/api/v1/videos?page=1にアクセスしたときのJSON
{
"videos":[{"id":1,"title":"スターウォーズ"},{"id":2,"title":"プリズンブレイク"},{"id":3,"title":"ハリーポッター"},
{"id":4,"title":"アベンジャーズ"},{"id":5,"title":"バッドマン"}],
"kaminari":{"pagenation":{"current":1,"previous":null,"next":2,"limit_value":5,"pages":12,"count":56}}
}

http://localhost:3000/api/v1/videos?page=1 アクセスしてみると,
レコードが5個しかないので見事pagenationされていることが分かりますね!

  • currentは現在のページの番号です
  • previousは前のページ番号です。1ページ目で前は無いのでnullになっています。
  • nextは次のページ番号です。
  • limit_valueは読み込んだレコードの数です。
  • pagesはトータルで何ページあるのかです。これがあれば無限スクロール側がいつ終わればいいかわかりますね。
  • countは全てのレコードの数です。

仮にここまで行ったページネーションの設定をしていないと、http://localhost:3000/api/v1/videos?page=1 にアクセスしても、今回の場合は56個のレコードがJSON化されてしまいます。

ここまでいければ後はVue側の実装です!

vue-infinite-loadingをyarnでインストールする。

まずは無限スクロールを行うvue-infinite-loadingをVueにインストールします。
yarn add vue-infinite-loading
こちらのコマンドを叩きます。

次にVue側でimportするコードを書いていきます。

app/javascript/app.vue
import InfiniteLoading from 'vue-infinite-loading'

Vue.use(InfiniteLoading, {  //無限スクロール
 slots: {
     noMore: 'すべて読み込みました',
     noResults: '読み込み完了しています',
 },
});

importする場所はhello_vue.jsファイルなど何でもOkです。
これでvue-infinite-loadingのコンポーネントを使うことができます。

無限スクロールをさせたいコンポーネントに設定を書く

app/javascript/VideosIndex.vue
<template>
  <div id="app">
    <div v-for="video in videos" :key="video.id">
      <p>{{ video.title }}</p>
    </div>
    <infinite-loading @infinite="infiniteHandler"></infinite-loading>  <!-- 追加した部分 -->
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data: function () {
    return {
      videos: [],
      page: 1, //このpageの値によってpagenationされたJSONを取ってきます
    }
  },

  methods: {
    infiniteHandler($state) {  //追加した部分
        axios.get(`/api/v1/videos`, {
            params: {
                page: this.page, 
            },
        }).then(({ data }) => {
            //そのままだと読み込み時にカクつくので1500毎に読み込む
            setTimeout(() => {
                if (this.page <=data.kaminari.pagenation.pages) {
                    this.page += 1
                    this.videos.push(...data.videos)
                    $state.loaded()
                } else {
                    $state.complete()
                }
            }, 800)
        }).catch((err) => {
            $state.complete()
        })
    }
  }
}  //export default
</script>

全体はこんな感じで、「追加した部分」を書けば取り敢えず動作はします。
デフォルトでは画面を一番下にスクロールしたときに、メソッドinfiniteHandlerが呼び出されます。

app/javascript/VideosIndex.vue
axios.get(`/api/v1/videos`, {
  params: {
    page: this.page,
  },

このaxiosのgetにparams:{page: this.page}のオプションを付けてあげる部分が非常に重要です。
これはaxiosがrailsのAPIからJSONを取ってくる際のURLに?page=1というオプションをつけるということです。
https://〇〇/api/v1/videos?page=1
今回はdataでpage:1と定義しているので最初は1ページ目のレコードを取ってきます。
例えば、page:2とすると1が飛ばされ2から読み込まれていきます。

app/javascript/VideosIndex.vue
                if (this.page <=data.kaminari.pagenation.pages) {
                    this.page += 1
                    this.videos.push(...data.videos)
                    $state.loaded()
                } else {
                    $state.complete()
                }
            }, 800)
        }).catch((err) => {
            $state.complete()
        })

この部分ですが、if (this.page <=data.kaminari.pagenation.pages) で現在のpage数(this.page)がkaminariの最大ページ数より少なかったらという分岐です。
少ない場合はthis.page += 1 で現在のページ数を1多くします。
そして、this.videos.push(...data.videos) でページ数を1増やしたレコードを取ってきます。
これが最大ページ数に到達されるまで繰り返されるという仕組みです。

最後に

いかがだったでしょうか?
記事を書くのに慣れていなくて分かりづらくなってしまったかもしれませんが気付き次第修正をしていこうと思います。
僕は最初rails側でページネーションするという発想が全く無く'vue-infinite-loading'が勝手にデータの中から少しずつレコードを取ってきて無限スクロールしてくれるのかと思っていました。
rails+無限スクロールの記事は少なかったので、同じような悩みを抱えている方の助けになればとても嬉しいです!

8
6
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
8
6