vue.jsで無限スクロール(infinite scrolling)
サーバからデータを取得した時に一括ですべてのデータを取得するのではなく画面を一定間隔スクロールする毎にデータを取得する無限スクロールをvue.jsとIntersection Observer APIを利用して実装します。
実装後は下記のようにスクロールをするごとにサーバへのアクセスが行われデータを取得していることがわかります。デベロッパーツールのネットワークタブにスクロールする度にName列の処理が増えていることを確認してください。
目次
Intersection Observer APIとは
Intersection Observer APIにObserveという単語が含まれているように要素の監視(観察)を行うことができるAPIです。何を監視するのかというとずばり要素です。監視対象の要素がviewportに交差(Intersection)、もう少しわかりやすくいうと監視対象の要素がユーザが閲覧しているブラウザの表示画面に入ってきたかどうかをIntersection Observer APIで判別することができます。この監視と判別機能を利用することで無限スクロールを実現します。
ではIntersection Observer APIを使ってどのように無限スクロールを実現するのでしょう?
例えばブログの記事が数百件サーバに保存されており、ユーザからトップページにアクセスがあった場合一括ですべての記事を取得するのではなく10件分の記事をサーバから取得して表示したとします。監視対象の要素を10件分のデータの下(その場所はユーザが見えない場所)に設定しておきます。監視対象の要素は最初はユーザからは見えない場所にありますがスクロールを行ってその要素がブラウザに表示された瞬間に次の10件分の記事を取得します。要素がブラウザに表示された瞬間をIntersection Observer APIを使って判別します。
vue.jsの環境構築
Intersection Observer APIの動作確認は、Vue.jsのcdnを利用して行うことができるためVue.jsの開発環境等を構築する必要はなく手元のPCでも行うことができます。
無限スクロールの設定
任意のディレクトリにindex.htmlファイルを作成し、下記のコードを記述します。長めに思うかもしれませんが一つ一つ説明を行っていきます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div ref="observe_element">この要素を監視します</div>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
observer: null,
},
mounted() {
this.observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry && entry.isIntersecting) {
console.log('画面に入ったよ');
}
});
const observe_element = this.$refs.observe_element;
this.observer.observe(observe_element);
},
});
</script>
</body>
</html>
ref属性の設定
vue.jsからref属性を利用してvue.jsからobserve_elementにアクセスできるように設定を行っています。
<div id="app">
<div ref="observe_element">この要素を監視します</div>
</div>
IntersectionObserverの処理
vue.jsのコードではmountedで処理を行っています。createdではなくmountedを利用する理由はDOMの作成が完了している状態でないと監視対象の要素を見つけることができないためです。
const app = new Vue({
el: '#app',
data: {
observer: null,
},
mounted() {
this.observer = new IntersectionObserver(entries => {
const entry = entries[0]
if (entry && entry.isIntersecting) {
console.log('画面に入ったよ')
}
})
const observe_element = this.$refs.observe_element
this.observer.observe(observe_element)
}
})
newでIntersctionObserverのインスタンスを作成して引数にはコールバック関数が入ります。entriesは監視対象の要素が配列で入っており、今回監視対象の要素がref=”observe_element”で指定した1つなのでentires[0]としています。entryのisIntersectingのプロパティがtrueの場合はその監視要素がユーザが閲覧しているブラウザに入っているためif文の中身が実行されます。
this.observer = new IntersectionObserver(entries => {
const entry = entries[0]
if (entry && entry.isIntersecting) {
console.log('画面に入ったよ')
}
})
監視要素の設定と監視設定
refで指定した要素を監視している処理が以下となります。IntersectionObserverのobserverで監視対象の要素を指定し、監視した要素が画面に入るとイベントが発火しIntersectionObserverのコールバッグ関数が実行されます。
const observe_element = this.$refs.observe_element
this.observer.observe(observe_element)
ブラウザで確認を行い、開発ツールのコンソールログを見てみましょう。コンソールには画面に入ったよが表示されます。
もしmountedではなくcreatedで実行してい場合は、Vue warn]: Error in created hook: “TypeError: Failed to execute ‘observe’ on ‘IntersectionObserver’: parameter 1 is not of type ‘Element’.”がコンソールに表示されます。その場合はcreatedからmountedに変更してください。
entryの中の確認
entryの中にはどのような値が入っているのかも確認しておきましょう。監視対象の要素に関するさまざまな情報が保存されています。
inIntersectingの値や監視している要素の中身もtargetの中に入ってます。inIntersectingの値はtrueで、上記の図では表示されていませんがtargetのdivを見ると”この要素を監視します”も確認することができます。
JSONPLACEHOLDER
index.htmlファイルからデータが取得できるサーバが必要となります。本文書でも何度か利用させてもらっているhttps://jsonplaceholder.typicode.com/を今回も利用します。
https://jsonplaceholder.typicode.com/posts
上記のURLにアクセスすると100件分の記事の情報をjsonで取得することができます。
データの取得
fetchメソッドを利用してvue.jsからpostsデータを取得します。 vue.jsのデータプロパティにpostsを追加します。
data: {
observer: null,
posts: [],
},
getPostsメソッドを追加したJSONPLACHOLDERからpostsデータを取得してthis.postsに挿入します。
methods: {
async getPosts() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts`)
this.posts = await res.json()
}
},
mounted時にgetPostsメソッドを実行します。
mounted() {
this.getPosts()
・
・
HTML側ではv-forディレクティブを利用して取得したpostsデータを展開します。
<div id="app">
<div v-for="post in posts" :key="post.id">
<h2>{{post.id}}:{{ post.title }}</h2>
<p>{{ post.body }}</p>
</div>
<div ref="observe_element">この要素を監視します</div>
</div>
ブラウザで確認すると取得した100件分のデータが表示されます。
10件のみデータ取得するために下記のように書き換えます。postsの後ろに_page=1と設定すると1ページ目の情報10件分が取得できます。2を設定すると2ページ目の10件が表示されます。
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=1`)
上記では1ページ目の最初の10件のみを取得してしまうので回避するためにデータプロパティpageを追加します。初期値を1として、実行される度に1増えるように設定します。
data: {
observer: null,
posts: [],
page: 1,
},
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${this.page++}`)
取得したpostsデータをthis.postsの配列に追加していくためにspread operatorを利用します。
methods: {
async getPosts() {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${this.page++}`)
const posts = await res.json()
this.posts = [...this.posts, ...posts]
}
},
Intersection Observerの監視からgetPost実行
要素が画面に入ったらgetPostメソッドを実行できるように”画面に入ったよ”の下にgetPostsを追加します。
this.observer = new IntersectionObserver(entries => {
const entry = entries[0]
if (entry && entry.isIntersecting) {
console.log('画面に入ったよ')
this.getPosts()
}
})
これで設定は完了です。ブラウザを開いて画面をスクロールしてください。
画面の下に”この要素を監視します”が表示される度にデータが取得され、無限スクロールが実現できます。
Networkタブを確認すると”この要素を監視します”が画面に表示されるタイミングでサーバへアクセスされていることも確認できます。