JavaScript の Fetch API で無限スクロールを実装する

はじめに

実務にて、ライブラリやフレームワークを利用しないで、ページ最下部までスクロールした際に、自動的に次の投稿を取得して表示してほしいという小仕事の依頼があり、それを解決した情報になります。ソースコードをコピペ(コピー&ペースト)するだけで簡単に流用ができます。

ソースコード

<!DOCTYPE html>
<html>
<body>
  <h1>Great People</h1><hr>
  <div id="infiniteScrollId">
    <div>
      <h2>[ Name ]<br>Timothy "Tim" John Berners-Lee</h2>
      <h3>[ Gender ]<br>man</h3>
      <h3>[ Age ]<br>65</h3>
      <h3>[ Job ]<br>Computer scientist</h3><hr>
    </div>
    <div>
      <h2>[ Name ]<br>William Henry "Bill" Gates III</h2>
      <h3>[ Gender ]<br>man</h3>
      <h3>[ Age ]<br>64</h3>
      <h3>[ Job ]<br>Businessman</h3><hr>
    </div>
    <div>
      <h2>[ Name ]<br>Bjarne Stroustrup</h2>
      <h3>[ Gender ]<br>man</h3>
      <h3>[ Age ]<br>69</h3>
      <h3>[ Job ]<br>Computer scientist</h3><hr>
    </div>
    <div>
      <h2>[ Name ]<br>John Warner Backus</h2>
      <h3>[ Gender ]<br>man</h3>
      <h3>[ Age ]<br>82</h3>
      <h3>[ Job ]<br>Mathematician</h3><hr>
    </div>
  </div>

<script>
let url = `json.php`
let execution_flag = false

document.addEventListener('scroll', () => {
  try {
    let children = infiniteScrollId.children
    let {top, height} = children[children.length - 1].getBoundingClientRect()
    if (top + height <= window.innerHeight && !execution_flag) {
      execution_flag = true
      fetch(url)
      .then(response => response.text())
      .then(posts => {
        let tmp = ''
        JSON.parse(posts).forEach(post => {
          tmp += `
            <div style="color: blue;">
              <h2>[ Name ]<br>${post.name}</h2>
              <h3>[ Gender ]<br>${post.gender}</h3>
              <h3>[ Age ]<br>${post.age}</h3>
              <h3>[ Job ]<br>${post.job}</h3><hr>
            </div>
          `
        })
        infiniteScrollId.insertAdjacentHTML('beforeend', tmp)
        execution_flag = false
      })
      .catch(() => {e => console.error(e)})
    }
  } catch (e) {
    console.error(e)
  }
}, {passive: true})
</script>
</body>
</html>
<?php
$great_people = [
  [
    'name' => 'Alan Mathieson Turing',
    'gender' => 'man',
    'age' => '41',
    'job' => 'Mathematician'
  ],
  [
    'name' => 'Grace Murray Hopper',
    'gender' => 'woman',
    'age' => '85',
    'job' => 'Rear admiral'
  ],
  [
    'name' => 'Linus Benedict Torvalds',
    'gender' => 'man',
    'age' => '50',
    'job' => 'Programmer'
  ],
  [
    'name' => 'Kenneth Lane Thompson',
    'gender' => 'man',
    'age' => '77',
    'job' => 'Computer scientist'
  ],
  [
    'name' => 'Dennis MacAlistair Ritchie',
    'gender' => 'man',
    'age' => '70',
    'job' => 'Computer scientist'
  ]
];

header('Content-type: application/json');
echo json_encode($great_people);

検証環境

解説

document から addEventListener() メソッドを呼び出し、scroll event 発生後に関数が呼び出される仕掛けを作ります。

document.addEventListener('scroll', () => {
  //処理
}, {passive: true})

addEventListener() の options に passive: true を設定して preventDefault() を呼び出さないようにしています。これにより scroll jank を防ぐことができます、Improving Scroll Performance with Passive Event Listeners に情報があります。

例外処理文 を記述します。

try {
  //処理
} catch (e) {
  console.error(e)
}

id グローバル属性が infiniteScrollId の div 要素の children プロパティにアクセスして子要素の HTMLCollection を取得します。

let children = infiniteScrollId.children

HTMLCollection の最後の要素から getBoundingClientRect() メソッドを呼び出して Viewport に対する位置を取得します。

let {top, height} = children[children.length - 1].getBoundingClientRect()

ページ最下部までスクロールした際に、ブラウザによっては scroll event が複数発生する場合があるので、対処方法として処理中フラグを定義しています。

let execution_flag = false

スクロール位置が window.innerHeight(ウィンドウの高さ)に到達、且つ、処理中フラグが OFF の場合に処理を実行するようにしています。また、処理が完了した際は、処理中フラグを ON にします。

if (top + height <= window.innerHeight && !execution_flag) {
  execution_flag = true
  //処理
  execution_flag = false
}

ページ最下部の検知に、要素が完全にスクロールされたかどうかを判定する という解決策がありますが、scrollHeight の値はブラウザによって異なる問題が発生するのでプログラムに組み込みません。

Fetch APIfetch() メソッドで PHP から JSON を取得します。

let url = `json.php`
fetch(url)

メソッドチェーンで Promise オブジェクトから then() メソッドを呼び出し、コールバック関数で ResponseUSVString オブジェクトで解決する promise として返します。

.then(response => response.text())

さらにメソッドチェーン、JSON.parse() メソッドで JSON を解析して配列を構築、forEach() メソッドで各要素の HTML を生成して、infiniteScrollId に insertAdjacentHTML() メソッドで HTML を最後の子要素の後に挿入します。

.then(posts => {
  let tmp = ''
  JSON.parse(posts).forEach(post => {
    tmp += `
      <div style="color: blue;">
        <h2>[ Name ]<br>${post.name}</h2>
        <h3>[ Gender ]<br>${post.gender}</h3>
        <h3>[ Age ]<br>${post.age}</h3>
        <h3>[ Job ]<br>${post.job}</h3><hr>
      </div>
    `
  })
  infiniteScrollId.insertAdjacentHTML('beforeend', tmp)
  execution_flag = false
})

さらにさらにメソッドチェーン、Promise が拒絶された場合は catch() メソッドで関数を呼び出し、Console.error() でデバッガの Web コンソールにエラーメッセージを出力します。

以上です。

おわりに

無限スクロールを実現する手段は他にもあります、JavaScript plugin の Infinite Scroll を利用する方法、jQuery plugin の jScroll を利用する方法、jQueryjQuery.ajax() を利用する方法など。

また、案件によってはページ表示速度が遅くなるので jQuery を禁止している場合があります。jQuery は多くのエンジニアを救った素晴らしいライブラリですが、現代の JavaScript は進化しています、そのプロジェクトで本当に jQuery は必須なのか考えないといけません。