JavaScriptで無限スクロールを実装するのはとても簡単(jQuery不要)
ある意味、前回の「JavaScriptとCSSを使ったスライドショーで画像を遅延ロードする方法」と関連するのですが、最後までスクロールしたと思ったら、スクロールバーが増えて画像が追加された!みたいなページってあるじゃないですか。
あれって実は簡単な仕組みで、
- 最後までスクロールしたのを検出する
- 次の画像をロードする
と、やっていることはただそれだけなので、ついでにこの話もしておこうかなーと思って記事を書いています。もちろん、JavaScriptライブラリやjQueryも使いません。
無限スクロールの実行例
無限スクロールと言いつつホントに無限だとデータ量的に困るので、20枚の写真を用意しました。最後までスクロールすると次の画像が読み込まれるのが確認できるかと思います。
この無限スクロール、技術的に面白いなぁー、というだけの話ではなく、実用面として、
- ページを開いた直後は表示されている画像しか読み込まない(=表示速度/通信量的に有利)
- スマホの場合、指で小さなページ番号や「次へ」ボタンを押すより、親指でスワイプしたほうが楽
などの明確なメリットがあります。
逆に
- ロードされた画像が開放されないのでメモリを食い続ける
- 大量にスクロールした場合、最初のほうへ戻るのが面倒
というデメリットも。
このあたりについては最後のまとめでも少し触れようと思います。
HTML部分の解説
<div class="scroll" data-max="20" data-lastnum="2">
<img src="1.jpg" />
<img src="2.jpg" />
</div>
HTMLは特にシンプルで、クラス名にscrollを指定したdivタグで最初に表示するimgタグを2枚分囲っているだけです。
それと、カスタムデータ属性に data-max と data-lastnum を追加しているのが特徴的でしょうか。
- data-max ... 写真の最大枚数を指定
- data-lastnum ... 現在表示されている最後の画像番号を指定
画像をロードする際に、次にロードするべき画像番号がわからないと困るので、このカスタムデータ属性を参照しています。
JavaScriptのグローバル変数を使っても構いませんが、1ページ内に無限スクロールを複数配置することもあるかな、と考えてカスタムデータ属性にしておきました。配置した数だけグローバル変数を用意するより、配置したdivタグの中のカスタムデータ属性にロード状況を記録したほうがシンプルじゃないかな?と。
CSS部分の解説
<style>
.scroll {
overflow:auto;
width:400px;
height:300px;
}
.scroll img {
height:180px;
display:block;
margin:1px;
}
</style>
CSSも特に難しいことはしていないハズ。
scrollクラスでは写真を表示する領域のサイズ指定と、overflow:auto;を指定して、スクロールバーが表示できるように。imgタグのほうはdisplay:block;でブロック要素にすることにより、写真が縦並びで表示されるようにしているだけです。
JavaScript部分の解説
<script>
document.querySelectorAll('.scroll').forEach(elm => {
elm.onscroll = function () {
if (this.scrollTop + this.clientHeight >= this.scrollHeight) {
//スクロールが末尾に達した
if (parseInt(this.dataset.lastnum) < parseInt(this.dataset.max)) {
//未ロードの画像がある場合
this.dataset.lastnum = parseInt(this.dataset.lastnum) + 1;
let img = document.createElement('img');
img.src = this.dataset.lastnum +'.jpg';
this.appendChild(img);
}
}
};
});
</script>
一番重要なJavaScriptの部分です。かなり短いコードでしょう?
querySelectorAllは過去記事で何度も登場しているので、今更解説の必要はないと思いますが、'.scroll'を指定することでクラス名scrollが指定された要素をすべて列挙しています。
1ページ内に複数の無限スクロールを配置する可能性を考えて、ですね。
そして、onscrollイベントでは scrollTop(スクロールした位置の上部座標)とthis.clientHeight (スクロール領域の高さ)を足した値が、scrollHeight(スクロール全体の高さ)と同等になったかどうかを確認し、末尾かどうかを判定しています。
末尾に達する少し前に次の画像をロードしたい場合はここをちょっといじれば良いでしょう。例えばscrollHeightの80%を超えたあたりでロードする、など。
そしてロード判定の部分ですが…
if (parseInt(this.dataset.lastnum) < parseInt(this.dataset.max)) {
parseIntを使っているのでちょっと見づらくなっていますが、HTML部分の解説で書いた data-lastnum と data-max の部分を参照しているだけです。
(カスタムデータ属性は文字列なのでparseIntで型変換しないと数値として認識できない)
画像の最大枚数と、現在ロード済の最後の画像番号がわかれば、次にロードすべき画像番号がわかりますよね?
this.dataset.lastnum = parseInt(this.dataset.lastnum) + 1;
let img = document.createElement('img');
img.src = this.dataset.lastnum +'.jpg';
this.appendChild(img);
そう、data-lastnum+1の画像番号を持つ画像をcreateElementしたimg要素に設定するだけ。
これで基本的な画像の無限ロード(あるいはこれも遅延ロードと言うべきかな?)が実装できるわけです。
ただ、今回の実装の問題点としては画像のファイル名が連番になっている前提で作られていることですかねー。
そうじゃないページの場合はもうひと工夫必要そうです。例えば前回の「JavaScriptとCSSを使ったスライドショーで画像を遅延ロードする方法」でやったようにimgタグのsrcではなく、data-srcのほうにファイル名を書いた状態であらかじめ20個のimgタグを用意するのも良いでしょう。
それならスクロールの末尾まできた際にdata-srcをsrcへ振り替えるだけです。
あるいは次の画像ファイル名を知るためにサーバーとの通信が必要なら、XMLHttpRequestで非同期通信して次の画像ファイル名をサーバーから教えてもらうというのも良いでしょう。
ともあれ、そのへんは画像のファイル名管理の話であって、無限スクロールとは関係ないので端折ります。あくまで無限スクロールのキモの部分は onscroll での末尾判定と createElement だけだと思っています。
まとめ
JavaScriptで画像の無限スクロールを実装するには、
- 最後までスクロールしたのを検出する
- 次の画像をロードする
だけでOKなので実はとっても簡単、というお話でした。
これを応用して、ページネーション…いわゆる「 [前へ] 1 2 3 4 5 [次へ] 」みたいなページ切り替えを無限ロード版に変更することも容易ですが、ん~~~個人的にはほとんどの場面でページネーションのほうが便利だと思っています。
ショッピングサイトやメルカリなどのフリマアプリで、ぼぉーっとしながらスマホでくりくりスクロールする分には無限スクロールも便利とは思いますが、それ以外のもっと詳細に検索条件などを付けて絞り込むページではページ番号がふってあったほうが何かと便利かな、と。
あとでそのページ番号に飛んでくることもできるし、ページごとにURLが変わるから、知り合いにURL送るのも楽でしょ?
Web業界の流れ的にはユーザーにURLを意識させない方向性らしいですけど、う~~~ん、いま現在はまだページネーションのほうが便利なんじゃないかなぁ…と個人的には思うのでした。
ただ、ちょっとしたパーツとして、そうですね、例えばオススメ商品をページの一部に貼り付けたりだとか、そういうときに無限ロード機能付きのパーツだとちょっと便利かも知れませんよね。
今はそうした使い分けが大事かな、と思います。