サーバからデータを取得した時に一括ですべてのデータを取得するのではなく画面を一定間隔スクロールする毎にデータを取得する無限スクロールをvue.jsとIntersection Observer APIを利用して実装します。

Intersection Observer APIはvue.jsの特有の機能ではないのでvue.js以外でも利用することができます。
fukidashi

実装後は下記のようにスクロールをするごとにサーバへのアクセスが行われデータを取得していることがわかります。デベロッパーツールのネットワークタブにスクロールする度にName列の処理が増えていることを確認してください。

無限スクロール
無限スクロール

Intersection Observer APIとは

Intersection Observer APIにObserveという単語が含まれているように要素の監視(観察)を行うことができるAPIです。何を監視するのかというとずばり要素です。監視対象の要素がviewportに交差(Intersection)、もう少しわかりやすくいうと監視対象の要素がユーザが閲覧しているブラウザの表示画面に入ってきたかどうかをIntersection Observer APIで判別することができます。この監視と判別機能を利用することで無限スクロールを実現します。

ではIntersection Observer APIを使ってどのように無限スクロールを実現するのでしょう?

例えばブログの記事が数百件サーバに保存されており、ユーザからトップページにアクセスがあった場合一括ですべての記事を取得するのではなく10件分の記事をサーバから取得して表示したとします。監視対象の要素を10件分のデータの下(その場所はユーザが見えない場所)に設定しておきます。監視対象の要素は最初はユーザからは見えない場所にありますがスクロールを行ってその要素がブラウザに表示された瞬間に次の10件分の記事を取得します。要素がブラウザに表示された瞬間をIntersection Observer APIを使って判別します。

Intersection Observer APIを利用しない場合はscrollイベントを利用して実装する必要があります。scrollイベントは頻繁にイベントが発生して処理に負荷が発生するために負荷が気になる場合はthrottoleを利用してイベントの発生回数を間引く等の処理が必要となります。
fukidashi

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>
監視する要素を特定するためとvue.jsを使っているため意図的にrefを利用しています。実際はidでもclassでもJavaScriptから要素が特定できれば問題ありません。
fukidashi

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)
    }
})
監視要素にid=”observer_element”を利用している場合であれば$refsを使用せず、document.getElementById(‘observer_element’)と記述することができます。
fukidashi

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('画面に入ったよ')
    }
})
例えば画像のimg要素を監視対象とした場合に複数の画像がHTMLに存在する場合はentriesには複数のimg要素の情報が入ります。
fukidashi

監視要素の設定と監視設定

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の中にはどのような値が入っているのかも確認しておきましょう。監視対象の要素に関するさまざまな情報が保存されています。

entryの中身を確認
entryの中身を確認

inIntersectingの値や監視している要素の中身もtargetの中に入ってます。inIntersectingの値はtrueで、上記の図では表示されていませんがtargetのdivを見ると”この要素を監視します”も確認することができます。

JSONPLACEHOLDER

index.htmlファイルからデータが取得できるサーバが必要となります。本文書でも何度か利用させてもらっているhttps://jsonplaceholder.typicode.com/を今回も利用します。


https://jsonplaceholder.typicode.com/posts

上記のURLにアクセスすると100件分の記事の情報をjsonで取得することができます。

JSON PLACEHOLDERから記事の取得
JSON PLACEHOLDERから記事の取得

データの取得

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件分のデータが表示されます。

ブラウザでpostsデータを表示
ブラウザでpostsデータを表示

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タブを確認すると”この要素を監視します”が画面に表示されるタイミングでサーバへアクセスされていることも確認できます。

無限スクロール
無限スクロール