無限スクロールを作成する:JavaScriptのIntersection ObserverにFetchを組み込んだプログラム 結城永人 -2019年6月25日 (火) サイトの無限スクロールのプログラムのアイデアは大きく二つの部分に分けられる。一つは要素判定で、今のページが画面のどこまで来たら次のページを表示するように動作するかを決める。もう一つは生成処理で、次のページをAjax通信でサーバーから読み込んで今のページに実際に追加する。JavaScriptのプログラムで前者に正しく画面と要素の交差を監視できて最適なIntersection Observer API、後者にAjax通信の時間差の難点も克服できて便利なFetch APIを使って無限スクロールを作成する方法を取り上げる。 無限スクロールのプログラムの基本的な流れ 通常、同じデザインのページがページネーションのリンクで幾つも続いて行くのを無限スクロールのデザインに置き換える。 HTMLはメインコンテンツ、ページネーション、サブコンテンツという順番に並ぶように記載する。 サブコンテンツをIntersection Observerの起動に指定してメインコンテンツの後に画面が到達したらFetchで次のページを自動的に読み込むようにする。 そしてメインコンテンツだけを取り出したら今のページのメインコンテンツの下/サブコンテンツの上に追加して表示する。 メインコンテンツとサブコンテンツの間のページネーションはあれば消してもしも最初のページで次のページへのリンクがなかったら無限スクロールを停止する。 ページネーションは無限スクロールを設置すればもはやサイトに不要だけれどもブラウザのJavaScriptがオフでも移動できるとかSEO対策に検索エンジンのクローラーがリンクを解析するなんて場合に備えて残しておくと良いと思う。 参考:検索エンジンとの相性を考慮した無限スクロールのベストプラクティス 無限スクロールのサンプルのソースコード HTMLのメインコンテンツとページネーションとサブコンテンツが順番に配置されたマークアップを対象としてJavaScriptのIntersection ObserverにFetchを組み込んだプログラムで無限スクロールを実行する。 HTML <main> <div class="posts"> <article> メインコンテンツ① </article> <article> メインコンテンツ② </article> <article> メインコンテンツ③ </article> </div> <nav class="pager"> <a class="older-link" href="/page2.html"> 次のページ </a> </nav> </main> <aside class="isf"> サブコンテンツ </aside> JavaScript var isf = document.querySelector("aside.isf"); var mnp = document.querySelector("main"); var bpg = document.querySelector("nav.pager"); var npl = document.querySelector("a.older-link"); let infiniteScrollObserver = new IntersectionObserver(entries => { entries.forEach(function(entry) { if (npl !== null) { (async function() { await fetch(npl.getAttribute("href")).then((response) => response.text()).then((text) => { let parserh = new DOMParser(); let doch = parserh.parseFromString(text, "text/html"); let pst = doch.querySelector("div.posts"); let bpNode = document.adoptNode(pst); npl = doch.querySelector("a.older-link"); mnp.appendChild(bpNode); });})(); if (document.querySelector("nav.pager") !== null) { bpg.remove(); }} else { infiniteScrollObserver.unobserve(isf); }});}, { rootMargin: "120px" }); infiniteScrollObserver.observe(isf); 実行結果 ブログのトップページなどのインデックスページで共通のアイデアの無限スクロールを取り入れて使用している。 プログラムの詳解:Intersection ObserverへのFetchの組み込み方 冒頭の四つの変数 var isf = document.querySelector("aside.isf") サブコンテンツのclassの取得:無限スクロールを開始する要素判定用 var mnp = document.querySelector("main") メインコンテンツ全体の要素の取得:次のページのコンテンツを追加する対象用 var bpg = document.querySelector("nav.pager") ページネーション全体のclassの取得:無限スクロールが可能ならば不要なページネーション全体の消去用 var npl = document.querySelector("a.older-link") 次のページのリンクのclassの取得:サーバーから読み込む次のページのURLの取得用 Intersection Observerのコンストラクタ作成 new演算子で変数のinfiniteScrollObserverにIntersectionObserverのコンストラクタを作成して使用する。 forEachのループ処理で監視する対象を複数の状況で逐一と扱えるようにする。 最後のobserve()メソッドの引数に監視する対象のサブコンテンツの変数のisfを入れて起動する。 オプションに「rootMargin: "120px"」を指定している。監視対象のサブコンテンツが画面に入る前からIntersection Observerを起動するためだ。同時よりもコンテンツを追加する際の遅れを防げる。無限スクロールはサーバーと通信する分だけ時間が多くかからざるを得ないからIntersection Observerを実際に必要な場所よりも早めに開始してデータを取り込んで次のページのコンテンツの生成処理へ移った方が迅速に動作する。 一つ目のif文 最初のページに次のページのページネーションはないので、除外するため、または次のページのページネーションがあるかぎりでしか無限スクロールを行わないようにする。 条件式の「npl !=null」は取得済みのページネーションのリンクタグの変数nplの内容がなくはない(ある)かどうかを判定する。 もしも変数nplの内容があれば「fetch(npl.getAttribute("href"))」で次のページのURLをhref属性から取得してコンテンツを読み込み、なければ「infiniteScrollObserver.unobserve(isf)」で監視するのを停止して無限スクロールを行わない。 Fetchのasyncとawaitによる同期的な使用 次のページのコンテンツをfetch()メソッドで読み込むけれどもそのままだとAjax通信がスクロールに追い付かなくて同じコンテンツが繰り返して読み込まれて表示される可能性がある。 例えば三回目の監視から始まる次のページのコンテンツの取得が十分に終わらないうちにサイトがスクロールで四回目の監視に入ると次のページのコンテンツから次の次のページへのリンクからURLを取得するのが間に合わなくて一回目の次のページのリンクを使って同じコンテンツしか読み込んで表示できないかも知れない。 訪問者のスクロールがゆっくりか、無限スクロールの新しいコンテンツの表示が一回目か二回目くらいまでならば追い付き易いけれどもさもないと同じコンテンツが繰り返されるバグが起きてしまう。 これを避けて順番にfetch()メソッドが動作するように、いい換えると一つの読み込みが終わったら初めて次の読み込みへ移るように同期的に使用するにはasync関数とawait演算子を使用するのが相応しい。 async 関数は、 await 式を含むことができます。 await 式は、async 関数の実行を一時停止し、 Promise の解決を待ちます。そして async 関数の実行を再開し、解決された値を返します。 async関数にawait演算子を付けたfetch()メソッドと関連する処理を入れておくと一つの読み込みの途中から他の読み込みが画面の新しいスクロールの要素判定から非同期に始まって最終的にどちらも同じページの結果を返したりはしなくなる。 fetch()メソッドは引数に次のページのリンクタグを取得したnplのhref属性にgetAttribute()メソッドをかけてURLを取得して入れる。 そして受信できる変数responseからtext()メソッドで、次のページのtextデータを変数textに取得する。 Fetchで手に入るデータはDocumentインターフェースに属さないからDOMParserによってHTMLDocumentへパースしてDOMツリーを操作できるようにする。 textデータのままだと文字列として扱わざるを得なくて次のページのコンテンツの取得や加工のプログラムが煩雑になってしまう。 Fetchの処理の四つの変数 let parserh = new DOMParser() HTMLDocumentへのパースの用意:DOMParserのコンストラクタ作成 let doch = parserh.parseFromString(text, "text/html") 次のページのHTMLDocumentの取得:textデータにDOMParserのコンストラクタをかけてパースを行う let pst = doch.querySelector("div.posts") 次のページのメインコンテンツの取得:無限スクロールで追加するためだ let bpNode = document.adoptNode(pst) 次のページのメインコンテンツの新しい文書への移動:今のページの一部に挿入する用意 さらに二つの変数の処理が続く。 npl = doch.querySelector("a.older-link") 次のページのデータからページネーションの次のページのリンクを取得:今のページからすると次の次のページのコンテンツを取得するために必要になる mnp.appendChild(bpNode) 今のページのメインコンテンツの領域の最後に次のページメインコンテンツを挿入 何れもグローバル変数(無限スクロールのプログラムのfunctionの外側に置かれたvar)の値を変更している。 nplの内容は次のページのリンクから次の次のページのリンクに入れ替わるわけで、すなわちFetchが発動して新しいページが読み込まれる、無限スクロールが進むほどに次のページのリンクが変数の内容として上書きされて新たに取得され直す。 これで無限スクロールは次の次のページ以降もページネーションのリンクタグのURLを取得して読み込めるようになる。 mnpの内容は次のページのメインコンテンツを追加して入れ換わるわけで、すなわちFetchが発動して新しいページが読み込まれる、無限スクロールが進むほどに追加されるメインコンテンツが変数の内容として上書きされて新たに取得され直す。 これで無限スクロールは次の次のページ以降のメインコンテンツを前回の分を消さずに連続して載せられるようになる。 グローバル変数でなくても最初に取得されたそれぞれの変数を今のページから次のページの読み込みに合わせて更新する必要がある。 Fetchの同期的な使用のためにはfetch()メソッドにawait演算子を付けて当該のプログラムの全体をasync関数として処理しなくてはならない。 いつ起動するかは無限スクロールでIntersection Observerの要素判定に基づくので、プログラミングとしてFetchのためのasync functionは順番が来れば直ぐに起動するように記載して構わない。 どこかの変数にそれ自体で値を返すわけでもなく、また他のところで使用されるものでもないからその場で始まってその場で終わる即時関数として扱うのが相応しいと感じる。 async functionの部分――最初のfetch()メソッドから最後のappendChild()メソッドまでFetchに関連するプログラムの全体――を半角括弧(())に入れてさらに半角括弧で実行するようにしている。 二つ目のif文 サイトに新しく追加する次のページからはページネーションを表示しないプログラムを組んでいる。訪問者のブラウザで無限スクロールが可能ならばページネーションは必要ないので、処理を減らして表示速度を上げるためにもメインコンテンツだけ表示したい。 しかし初回のページで訪問者が閲覧する当初はページネーションが表示される状態になっているので、無限スクロールで次のページが読み込まれて追加されるときに消すためのプログラムを二つ目のif文に記載する。 この処理は次のページへのページネーションが付いてない最初のページには必要ない。一つ目のif文で、最初のページは無限スクロールを行わないと判定されてIntersection Observerを停止する方の条件に入る。ページネーションを消去する二つ目のif文のプログラムは反対側の無限スクロールを行うと判定されるFetchのプログラムと併置しておけば最初のページを除外できる。 条件式の「document.querySelector("nav.pager") != null」は取得済みのページネーションがサイト上になくはない(ある)かどうかを判定する。 取得済みのページネーションがサイト上にあれば不要なので、remove()メソッドでページネーションを取得した変数bpgを削除して表示しないようにする。 関連:Imaginary|Bloggerのテンプレートの提供 コメント 新しい投稿 前の投稿
コメント