事前準備
バックエンドは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に送る訳です。
##ルーティング
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させる
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側で
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のデータを取り出す場合は
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を作成します。
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使用)
こちらの記事が参考になりました。
{
"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するコードを書いていきます。
import InfiniteLoading from 'vue-infinite-loading'
Vue.use(InfiniteLoading, { //無限スクロール
slots: {
noMore: 'すべて読み込みました',
noResults: '読み込み完了しています',
},
});
importする場所はhello_vue.jsファイルなど何でもOkです。
これでvue-infinite-loadingのコンポーネントを使うことができます。
##無限スクロールをさせたいコンポーネントに設定を書く
<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が呼び出されます。
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から読み込まれていきます。
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+無限スクロールの記事は少なかったので、同じような悩みを抱えている方の助けになればとても嬉しいです!