とても便利なJavaScript APIが登場しました。要素の表示状態を検出できるIntersectionObserver APIを使えば、無限スクロールを手軽に実装できます。
Webプラットホームに最近、注目の新しいクライアントサイドJavaScript API「IntersectionObserver API」が登場しました。
軽量でしかも使い勝手の良いこのAPIは、特定のDOM要素の表示状態、つまり要素が(ブラウザーウィンドウか要素の)ビューポートに入っているかどうかを効率的に監視する手段を提供しています。要素がビューポイントと重なり合う領域の割合を指定すれば、要素の表示状態を正確に定義できます。
この機能の一般的な用途としては、次のものが挙げられます。
- コンテンツの遅延ロード
- 無限スクロール
- 広告表示
- スクロールでトリガーされるアニメーション(注:用途としては本来おすすめできない。APIによって通知される表示状態の情報はわずかに遅延する可能性があるため、データが「ピクセルパーフェクト」であることは保証されていない)
対応ブラウザー
IntersectionObserver APIはかなり新しいAPIなので、記事執筆時点では対応ブラウザーが以下に限定されています。
- Chrome desktop 51
- Chrome for Android 51
- Android WebView 51
- Opera 38
- Opera for Android 38
とはいえ、(ルート要素のマージンには対応していませんが)開発中のポリフィルがGithubで利用できるので、いますぐIntersection Observersを始められます。
この記事では、無限スクロールのUXパターンを実装します。上のポリフィルだけでなく、promise・テンプレート文字列・アロー関数などES6/ES2015の機能も使います。
無限スクロールを実装してみよう
アイテムの長いリストがあるとして、ユーザーがドキュメントの一番下近くまでスクロールするとアイテムの次の一区切りがロードされてリストの末端に追加される、無限スクロールを実装します。
以下のように構築します。
下のコードスニペットで実現されるアイデアのポイントは、リストやドキュメントの一番下近くのアイテムを、ブラウザーのビューポートがページの末端近くに来たときに合図を出す「番兵(sentinel)」として使う、ということです。
この「番兵」はIntersectionObserverインスタンスによって監視されるDOM要素です。このオブジェクトによって「番兵」が(ビューポート内に)表示されたことが報告されると、アイテムの次のセットをロードするタイミングが来たと検知され、次のセットがロード、レンダリングされ、リストに追加されて、次のページ用の新しい「番兵」が立てられます。
ページの設定
それではHTMLから始めます。ページbodyではリストを1つだけマークアップします。
<ul class="listview"></ul>
本来なら最初の(ページの)アイテムはすでにリストに取り込まれているべきですが、コードをシンプルにするために、スクロールでロードされる続きのページと同様、最初のアイテムもJavaScriptでフェッチします。
次にポリフィルをインクルードします。実際のシナリオでは、必要な場合のみロードします。APIが画面上に通知を表示できるようになっているかどうかも確認します。
<span class="polyfill-notice">The polyfill is in use</span>
<script>
if (!('IntersectionObserver' in window))
document.body.classList.add('polyfill');
</script>
<script src="../intersectionobserver-polyfill.js"></script>
CSSでは、主にリストビューのレイアウト設定とサポート通知のスタイリングにルールを適用します。この点は記事では取り上げないので、詳しくはこちらのスタイルシートを参照してください。
スクリプトの作成
最初にIntersectionObserverオブジェクトをインスタンス化します。1個のインスタンスだけですべての「番兵」を監視できます。
sentinelObserver = new IntersectionObserver(sentinelListener, {threshold: 1})
2番目の引数で渡される設定オブジェクトで、表示状態に関する閾値(要素の領域が表示されている割合)を1に設定し、アイテムが完全にビューポート内に入っている場合のみイベントリスナーを発生させます。デモではオレンジの枠線で囲まれている部分が「番兵」要素です。
イベントリスナーは上で説明したオペレーションを実行します。そのコードに進む前に、現時点での「番兵」要素を表示し管理するオブジェクトを示します。
sentinel = {
el: null,
set: function(element) {
this.el = element;
this.el.classList.add('sentinel');
sentinelObserver.observe(this.el);
},
unset: function() {
if (!this.el)
return;
sentinelObserver.unobserve(this.el);
this.el.classList.remove('sentinel');
this.el = null;
}
}
ここでもっとも重要なのは、IntersectionObserverのobserve()メソッドとunobserve()メソッドが、監視する要素の追加と削除のために呼び出される部分です。
イベントリスナーは、ヘルパーオブジェクトを使って現在の「番兵」を削除し、リストの一番下の部分にローディングインジケーターを設定し、nextPageメソッドで次のリストページをロードできます。新しいアイテムがロード、レンダリング、追加され、promiseが返されて、オペレーションの完了が示されます。この時点で新しい「番兵」アイテムを設定し、ローディングインジケーターを切り替えられます。
sentinelListener = function(entries) {
console.log(entries);
sentinel.unset();
listView.classList.add('loading');
nextPage().then(() => {
updateSentinel();
listView.classList.remove('loading');
});
}
updateSentinelメソッドは次の「番兵」を立て、新しくロードされたページの最初のアイテムを選択します。
updateSentinel = function() { sentinel.set(listView.children[listView.children.length - pageSize]);
}
コードの残りの部分は、主にnextPage関数の実装で構成されます。(ネットワークリクエストをシミュレートする)loadNextPage()によって返されるpromiseが解決されると、供給されたアイテムのオブジェクトがHTMLでレンダリングされ、リストの末端に追加されます。
できあがりです! デモに戻って、つなぎ合わされたコード全体を見てください。
(参考)関連ドキュメント
このAPIとその理論についてのさらに詳しいドキュメントを以下のリンクから参照できます。
- IntersectionObserver’s Coming into View
- Intersection Observers Explained
- MDN – IntersectionObserver API
※本記事はSimon Codrington、Tim Severienが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:Native Infinite Scrolling with the IntersectionObserver API)
[翻訳:新岡祐佳子/編集:Livit]