CommentOut

【Javascript&PHP】無限スクロールの作り方【AJAX】 【Javascript&PHP】無限スクロールの作り方【AJAX】

【Javascript&PHP】無限スクロールの作り方【AJAX】

公開日:  最終更新日:

Googleで検索した際、検索結果がどんどん下に読み込まれていきますよね?
この新しい情報が随時読み込みされて、どんどんスクロール量が増えていく動きは『無限スクロール』と呼ばれているそうです。

今回はこの無限スクロールを解説していきます。

大量のコンテンツの見せ方には『ページャー』と『無限スクロール』がある

ページャーというのは、1ページに情報が決められた数のみ表示され、それ以上の情報は2ページ目、3ページ目とページめくりしていくことで表示されます。

対して無限スクロールでは、ページ下部まで到達した時にページ遷移することなく、次々と新しいコンテンツが追加表示され、無限にスクロールし続けることができます。

最近、Google検索の検索結果がPCでも無限スクロール表示に切り替わりましたね。

無限スクロールのメリット

上記のようにコンテンツ最下部に到達した時、ページャーでは「ページ遷移ボタン」の操作が必要なため、『閲覧』という思考から『操作』思考への切り替えが発生し、そこでユーザーの集中が途切れてしまい、離脱を発生させやすいです。
しかし、無限スクロールではコンテンツを表示しきるまで、無限にコンテンツが表示されるため、ユーザーをページに引き付けておくことができます。

無限スクロールのデメリット

無限スクロールを活用することで、ユーザーにUIを意識させず、コンテンツに集中させることができるのですが、デメリットもあります。

無限スクロールのデメリット:フッターがなかなか閲覧されない

無限スクロールというくらいですから、延々コンテンツが読み込まれます。
そうすると、いつまでもフッターに到達しないんですね。

なので、アフィリエイトサイトやAdsenseを導入しているサイトで無限スクロールを使うと、いつまでもフッター到達しないので、フッターの広告設定が無駄になっちゃいますね。

無限スクロールのデメリット:SEO的に不利

後ほど説明しますが、無限スクロールではJavascriptでスクロール量を計測して、スクロールがページ下部に到達したら、次の要素をJavascriptを使って読み込むようになります。
ということは、ページが読み込まれた後にJavascriptによって、コンテンツが生成されるため、「検索エンジンのクローラーはJavascriptを動かさない」って話があるので、検索エンジンのクローラーにコンテンツを読み取ってもらえない可能性があるんですね。

無限スクロールの使いどころ

無限スクロールは上記の通りSEOに不利なため、通常のホームページに導入するのは不向きです。
対して、物件や求人などの検索サービスやSNSには、ユーザーの関心を引き付けておけるという点で向いていると考えられます。

だからこそ、GoogleもPCでの検索結果にも無限スクロールを導入しているのだと考えられます。

無限スクロールの実装に必要な要素

無限スクロールはjavascriptのAJAXによって実装できます。
必要な処理は以下の通りです。

  • スクロールがページ下部に到達したか判定する処理
  • 非同期通信(AJAX)で表示する情報をサーバーから取得する処理
  • 非同期通信(AJAX)で取得した情報をHTMLで表示する処理

意外と必要な処理は少ないです。

無限スクロールの実装

今回、各処理にはjQueryを活用します。

スクロールがページ下部に到達したか判定する処理

まず、スクロールがページ下部に到達したか判定する処理を作成します。

// スクロールが発生した時のイベント
$(window).scroll(function() {
    // pageBottom = [bodyの高さ] - [windowの高さ]
    var pageBottom = document.body.clientHeight - window.innerHeight;
    // スクロール量を取得
    var currentPos = window.pageYOffset;

    // スクロール量が最下部の位置を過ぎたか判定
    if (pageBottom <= currentPos) {
        // スクロールが画面末端に到達している時
        /*
         * 追加情報を追加&表示する処理を実行
         */
    }
});

これだけで判定ができます。

途中で出てくる以下の記述

// pageBottom = [bodyの高さ] - [windowの高さ]
var pageBottom = document.body.clientHeight - window.innerHeight;

この”pageBottom”なんですが、どこの値を取得しているかというと、ページ全体の高さ(bodyの高さ)から画面の高さを引いています。

そうすることで、スクロールが末端に到達した時の画面上部の座標が取得できます。

// スクロール量を取得
var currentPos = window.pageYOffset;

window.pageYOffsetでは、現在のスクロール量を取得できます。
スクロール量 = 現在画面内上部に見えている座標です。
なので、先ほど取得したページ下部の座標と比較することで、ページ最下部に到達したか判定できます。

// pageBottom = [bodyの高さ] - [windowの高さ]
var pageBottom = document.body.clientHeight - window.innerHeight;
// スクロール量を取得
var currentPos = window.pageYOffset;

// スクロール量が最下部の位置を過ぎたか判定
if (pageBottom <= currentPos) {
    // ページ下部に到達している!
}

ただ、AJAXによる読み込みは非同期で行われるため、画面最下部に到達してから読み込んでいると、スクロールと読み込みに若干のタイムラグが発生します。
そこで、画面最下部ではなく、もう少し手前から読み込みを開始する場合には以下のようになります。

// スクロール量が最下部の位置を過ぎたか判定
if (pageBottom-400 <= currentPos) { // 最下部から400px手前で反応する
    // ページ下部に到達している!
}

非同期通信(AJAX)で表示する情報をサーバーから取得する

次に、非同期通信(AJAX)でサーバーから追加情報を取得する必要があります。

$.ajax({
    type: "GET",
    url: "https://~~~~",
    dataType : "json",
})
// Ajaxリクエストが成功した場合
.done(function(data) {
    // 取得した情報を表示する処理
})
// Ajaxリクエストが失敗した場合
.fail(function(XMLHttpRequest, textStatus, errorThrown) {
    alert(errorThrown);
});

jQueryのAJAXはこんな感じで使えます。

$.ajax({
    type: "GET",
    url: "https://~~~~",
    dataType : "json",
})

ここでurlの値にGETリクエストを送って、jsonデータを受け取るようにしています。

最近のAPIってRESTで設計されていることが多いので、GETリクエストにしていますが、POSTでも大丈夫です。
受け取るデータもjsonにしてますけど、textでも大丈夫です。

// Ajaxリクエストが成功した場合
.done(function(data) {
    // 取得した情報を表示する処理
})
// Ajaxリクエストが失敗した場合
.fail(function(XMLHttpRequest, textStatus, errorThrown) {
    alert(errorThrown);
});

そして、非同期通信(AJAX)が成功したか、失敗したかで実行される関数が変わります。
成功した時はdoneの引数に代入した関数が実行され、失敗した時はfailの引数に代入した関数が実行されます。

当然、成功したら情報が返ってくるので、成功した時に画面に表示する処理を行います。

非同期通信(AJAX)で取得した情報をHTMLで表示する処理

さて、上記コードで非同期通信(AJAX)して、無事にデータが取得できたらデータを表示しましょう。
最近では、REST APIでJSON形式のデータを受け取れることが多いです。

// Ajaxリクエストが成功した場合
.done(function(data) {
    for (var i=0; i<data.length; i++) {
        $('body').append(
            `<a class="item_block">
                <img src="${data[i].thumbnail_url}">
                <h2>${data[i].title}</h2>
            </a>`
        );
    }
})

doneの中で、受け取ったデータの件数でループ処理を行い、appendを使うことで要素を追加できます。

無限スクロールが動作するサンプル

今回のサンプルはニュース記事をどんどん読み込む無限スクロールです。
ただし、ニュース情報取得元のニュースAPIから取得できる情報量の上限があるので、実際には無限ではありません。

【index.html】

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>無限スクロール</title>

    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="inner">
        <h2>ニュース記事一覧</h2>
        <div id="news_list"></div>
    </div>

    <!-- jQuery読み込み -->
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <!-- スクリプトの読み込み -->
    <script src="script.js"></script>
</body>
</html>

【style.css】

* { box-sizing: border-box; }

.inner {
    display: block;
    margin: 0px auto;
    max-width: 960px;
    background: #e1e1e1;
}
        
#news_list {
    display: block;
    margin: 0px 0px;
    padding: 0px;
    font-size: 0px;
}

#news_list > * {
    display: inline-block;
    margin: 0px;
    padding: 10px;
    width: 33.333%;

    vertical-align: top;
    font-size: 16px;
}

/* 読込中表示 */
#news_list > .loading {
    font-size: 16px;
    font-weight: 500;
}

.news_block {
    display: block;
    padding: 10px;

    text-decoration: none;
    background: white;
    border: 2px solid #ccc;
}
.news_block > img {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
}
.news_block > .title {
    display: -webkit-box;
    margin: 5px 0px;
    height: calc(1.3em * 3);
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;

    color: #333;
    text-decoration: none;
    font-size: 18px;
    font-weight: 700;
    line-height: 130%;
}
.news_block > .description {
    display: -webkit-box;
    margin: 5px 0px;
    height: calc(1.3em * 3);
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; /* 制限したい行数が3の場合 */
    overflow: hidden;

    color: #333;
    text-decoration: none;
    font-size: 14px;
    font-weight: 400;
    line-height: 130%;
}
.news_block > .author {
    margin: 0px;
    color: #333;
    font-size: 12px;
    font-weight: 400;
    line-height: 100%;
}

【script.js】

const html = document.querySelector('html');

var ApiKey = {ここにはご自身で登録したAPI Keyを入力してください};
var loading_flg = false;
// 現在読み込んでいる記事のインデックス
var PostIndex = 1;
// 一度に読み込む記事数
var PostNum = 10;

// 記事を非同期取得して、ニュースリストに追加する関数
function addPosts() {
    $("#news_list").append(`<div class="loading">読込中...</div>`);

    // AJAXを使って、ローカルのPHP(を経由してNewsAPIから情報を取得)にアクセス
    // NewsAPIのURLを送る(テクノロジーカテゴリーのヘッドラインニュースを取得するURL)
    $.ajax({
        type: "GET",
        url: "./getNewsList.php",
        data: {
            url: `https://newsapi.org/v2/top-headlines?country=jp&category=technology&pageSize=${PostNum}&page=${PostIndex}&apiKey=${ApiKey}`,
        },
        dataType : "json",
    })
    // Ajaxリクエストが成功した場合
    .done(function(data) {
        // 読込中を削除
        $('#news_list .loading').remove();

        // ニュースを表示
        for (var i=0; i<data.length; i++) {
            $("#news_list").append(
                `<div>
                    <a class="news_block" href="${data[i]["url"]}" target="_blank">
                        <img src="${data[i]["urlToImage"]}">
                        <h2 class="title">${data[i]["title"]}</h2>
                        <p class="description">${data[i]["description"]}</p>
                        <p class="author">${data[i]["author"]}</p>
                    </a>
                </div>`
            );
        }

        // 読込が終わったので、インデックスを進める
        PostIndex += 1;

        // 読込中フラグをオフに戻す
        loading_flg = false;
    })
    // Ajaxリクエストが失敗した場合
    .fail(function(XMLHttpRequest, textStatus, errorThrown) {
        alert(errorThrown);
    });
}

// ページを開いた時
$(function() {
    addPosts();
});

// スクロールが発生した時
$(window).scroll(function() {
    // 読込処理中ではないか判定
    if (!loading_flg) {

        // 画面に見えている最上部の座標が分かる = [bodyの高さ] - [windowの高さ]
        var bottomPoint = document.body.clientHeight - window.innerHeight;
        // スクロール量を取得
        var currentPos = window.pageYOffset;
        // スクロール量が最下部の位置を過ぎたか判定
        if (bottomPoint-300 <= currentPos) {
            loading_flg = true;

            // スクロールが画面末端に到達している時
            addPosts();
            
            // 末端に到達を検知!
            console.log(`ページ末端への到達を検知!\nbottomPoint:${bottomPoint}\ncurrentPos:${currentPos}`);
        }
    }
});

【getNewsList.php】

<?php
$url = $_GET['url'];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$json = curl_exec($ch);
// 文字コード変換
$json = mb_convert_encoding($json, 'UTF8', 'ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN');
$json = json_decode($json, true);

$array = [];
foreach ($json['articles'] as $row) {
    // 配列に追加
    array_push($array, array(
        "url"           => $row['url'],
        "urlToImage"    => $row['urlToImage'],
        "title"         => $row['title'],
        "description"   => $row['description'],
        "author"        => $row['author'],
        "publishedAt"   => $row['publishedAt'],
    ));
}
echo json_encode($array, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);

NewsAPIではjavascriptから直接GETリクエストできなかったので、一度PHPから情報を取得して、PHPの中でデータフォーマットを整えて、再度JSON形式にして返しています。

宣伝
WordPressサイトのテンプレート編集やトラブル対応、バグ修正、簡単なJavascriptの作成(カルーセルやバリデーション等)など、小規模なスポット対応を受け付けております。
もしお困りごとがありましたら、お問い合わせフォームよりご相談ください。

この記事を書いた人

uilou

uilou

プログラマー

基本的に、自分自身の備忘録のつもりでブログを書いています。 自分と同じ所で詰まった人の助けになれば良いかなと思います。 システムのリファクタリングを得意としており、バックエンド、フロントエンド、アプリケーション、SQLなど幅広い知識と経験があります。 広いだけでなく、知識をもっと深堀りしていきたいですね。