12
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

もっとシンプルに無限スクロールをネイティブで実装する

Last updated at Posted at 2020-06-18

JavaScript の Intersection Observer API を使用し、コンテンツを遅延読み込みして div を描画するような、無限スクロールを実装します。

Twitter のタイムラインを下に辿っていくような表示になります (上にスクロールして新しいコンテンツを読むような機能は、ここでは実装しません) 。

1. はじめに

自分が WEB 上で調べてみたところ、下記の記事が最初に出てきたのですが、サンプルコードにバグを含んでいて、解説通りの動作をしませんでした…。

また、古い JavaScript の書き方をしていたり、本質的ではないコードが大半をしめていたり、then の中で then するような書き方をしていました。

参考「jQueryにはもう頼らない!無限スクロールをネイティブで実装する最新テクニック – WPJ

本記事では、なるべく本質的な部分だけを記述し、シンプルに分かりやすく実装したいと思います。

2. サンプル

2.1. コード

画面下に到達するたびに、新しいコンテンツを読み込みます。

ここでは、30 件まで読み込んだらそれ以上読み込みません。

index.html
<meta charset="UTF-8">

<div id="contents"></div>

<script src="./fetch-dummy.js"></script>
<script src="./main.js"></script>
main.js
(() => {

	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();

})();
fetch-dummy.js
(() => {

	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 を繰り返します。

  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 で表示を切り替えれば、コードをあまり複雑にすることなく、スマートに実装できます。

loadContent()
	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());
CSS
.loading:before {
	content: 'Loading ...';
}

3.5. おまけ: 非同期ジェネレータを使用して拡張しやすくする

コンテンツの合間に見出しや広告を挿入するなどを考えると、コンテンツの読み込みを非同期ジェネレータで定義したほうが拡張しやすくなります。

※ Edge Legacy では非同期イテレータ・非同期ジェネレータはサポートされていません。

loadContent()
	// 
	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 の非公式クライアントなどを作成する場合に無限スクロールを使用すると、良いと思います。

12
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?