JavaScript の Intersection Observer API を使用し、コンテンツを遅延読み込みして div を描画するような、無限スクロールを実装します。
Twitter のタイムラインを下に辿っていくような表示になります (上にスクロールして新しいコンテンツを読むような機能は、ここでは実装しません) 。
1. はじめに
自分が WEB 上で調べてみたところ、下記の記事が最初に出てきたのですが、サンプルコードにバグを含んでいて、解説通りの動作をしませんでした…。
また、古い JavaScript の書き方をしていたり、本質的ではないコードが大半をしめていたり、then の中で then するような書き方をしていました。
参考「jQueryにはもう頼らない!無限スクロールをネイティブで実装する最新テクニック – WPJ」
本記事では、なるべく本質的な部分だけを記述し、シンプルに分かりやすく実装したいと思います。
2. サンプル
2.1. コード
画面下に到達するたびに、新しいコンテンツを読み込みます。
ここでは、30 件まで読み込んだらそれ以上読み込みません。
<meta charset="UTF-8">
<div id="contents"></div>
<script src="./fetch-dummy.js"></script>
<script src="./main.js"></script>
(() => {
const contents = document.getElementById('contents');
//
const infiniteScrollObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if ( ! entry.isIntersecting ) return;
infiniteScrollObserver.unobserve(entry.target);
loadContent();
});
});
//
let i = 0;
const max = 30;
const loadContent = async () => {
const response = await fetchDummy('https://example.com/load?i=' + i);
contents.insertAdjacentHTML('beforeend',
'<div>' +
'#' + (i + 1) + '<br>' +
await response.text() +
'</div>');
//
i++;
if ( i < max ) infiniteScrollObserver.observe(contents.lastElementChild);
};
//
loadContent();
})();
(() => {
window.fetchDummy = url => new Promise(resolve => {
setTimeout(() => {
const content = ('Content '.repeat(6) + '<br>').repeat(3) + url + '<br><br>';
resolve({ text: async () => content });
}, 100)
});
})();
※ fetchDummy()
は疑似的に fetch()
するための関数で、実用的に使用するコードではありません。
2.2. 動作確認環境
- Chrome 81
- Firefox 77
- Edge Legacy 44
IE で動作させるには、トランスコンパイルや Polyfill が必要です。
未確認ですが、IE 以外ならおおよそ動作すると思われます。
3. 説明
3.1. おおまかな流れ
以下の 1, 2 と 3, 4 を繰り返します。
- 新しいコンテンツを読み込む
- そのコンテンツを監視する
- 画面上にあったら監視をやめる
- 次のコンテンツの読み込みを開始する
3.2. IntersectionObserver
を作る
上記の「おおまかな流れ」の 3, 4 に相当する部分を実装します。
以下は、IntersectionObserver
のテンプレートのような記述です。
const infiniteScrollObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if ( ! entry.isIntersecting ) return;
// ...
});
});
ここでは、本質的なコードは以下の 2 行のみです。
infiniteScrollObserver.unobserve(entry.target);
loadContent();
監視対象である entry.target
を unobserve し、次に loadContent()
関数で非同期にコンテンツの読み込みを開始しています。
(ちなみに、あらかじめコンテンツのデータをすべて読み込んでいるなど、非同期に読み込む必要がない場合も同様のコードで記述できます。)
参考「Intersection Observer API - Web API | MDN」
3.3. loadContent()
の実装
上記の「おおまかな流れ」の 1, 2 に相当する部分を実装します。
コンテンツのデータを読み込み、DOM に要素を追加し、その要素を observe します。
コンテンツの終端の判定は場合により異なると思いますが、ここでは単に max = 30
件まで読み込んだらもう observe しないようにしました。
※ fetchDummy()
は疑似的に fetch()
するための関数で、実用的に使用するコードではありません。
3.4 おまけ: 読み込み中などの表示
loadContent()
内で要素にクラスを付け外し、CSS で表示を切り替えれば、コードをあまり複雑にすることなく、スマートに実装できます。
const loadContent = async () => {
contents.insertAdjacentHTML('beforeend', '<div class="loading"></div>');
const content = contents.lastElementChild;
//
const response = await fetchDummy('https://example.com/load?i=' + i);
content.classList.remove('loading');
content.insertAdjacentHTML('beforeend',
'#' + (i + 1) + '<br>' +
await response.text());
.loading:before {
content: 'Loading ...';
}
3.5. おまけ: 非同期ジェネレータを使用して拡張しやすくする
コンテンツの合間に見出しや広告を挿入するなどを考えると、コンテンツの読み込みを非同期ジェネレータで定義したほうが拡張しやすくなります。
※ Edge Legacy では非同期イテレータ・非同期ジェネレータはサポートされていません。
//
const loadContent = async () => {
const nextContent = await contentsIterable.next();
if ( ! nextContent.done )
infiniteScrollObserver.observe(nextContent.value);
};
const contentsIterable = (async function*() {
for (let i = 0; i < 30; i++) {
//
const response = await fetchDummy('https://example.com/load?i=' + i);
contents.insertAdjacentHTML('beforeend',
'<div>' +
'#' + (i + 1) + '<br>' +
await response.text() +
'</div>');
yield contents.lastElementChild;
}
})();
4. その他
レスポンシブ対応や、一度に読み込む件数の調整などは、必要に応じて機能を追加してください。
そもそも無限スクロールにすべきかどうかも場合によりますので、何も考えずに使用するのは避けるべきです。
個人的に、Twitter の非公式クライアントなどを作成する場合に無限スクロールを使用すると、良いと思います。