API 留言板練習-Part 3 : 增加[載入更多]功能


Posted by tzutzu858 on 2020-11-03

以分頁來說可以分成

  • page-based pagination (絕對分頁)
  • cursor-based (相對分頁)

所以這邊要做的就是用一個 before 來代表現在是第幾筆資料,載入更多就是依照這個 before 來決定要再載入幾筆資料


1. 更改 sql 子句

  • 因此先來改 api_comments.php 這個檔案

在 sql 子句加上 empty($_GET['before']) ? "" : "AND id < ?" ,就是根據 $_GET['before'] 是不是空的來決定他要帶什麼參數,如果有傳 $_GET['before'] ,結果就是 AND id < $_GET['before'] , 拿到 $_GET['before'] 以前資料,也就是載入更多舊資料。

完整sql 子句

$sql = "SELECT nickname, content, created_at FROM discussions WHERE site_key = ? " . (empty($_GET['before']) ? "" : "AND id < ? ") . "ORDER BY id DESC LIMIT 5";
if (empty($_GET['before'])) {
  $stmt->bind_param('s', $site_key);
} else {
  $stmt->bind_param('si', $site_key, $_GET['before']);
}

補充, sql 句有錯可以用 echo $sql; 把他印出來 debug


2. 要把 id 傳下去

所以從 MySQL 取得資料 fetch_assoc() 那邊多增加 "id" => $row["id"]

while ($row = $result->fetch_assoc()) {
    array_push($discussions, array(
        "id" => $row["id"],
        "nickname" => $row["nickname"],
        "content" => $row["content"],
        "created_at" => $row["created_at"],
    ));
}

原本的 sql 子句 SELECT 後面要多加 id ,但這樣等於拿全部欄位,所以直接 SELECT * 就好

$sql = "SELECT * FROM discussions WHERE site_key = ? " . (empty($_GET['before']) ? "" : "AND id < ? ") . "ORDER BY id DESC LIMIT 5";

用 postman 做測試,當傳 before = 30,就會回傳 id = 29、28、27、26、25 的資料,這樣就可以指定 id ,拿它前五筆資料

後端結束



前端串資料

1. 前端把載入資料先包成一個 getComments function

傳入 siteKey, before, callback function

function getCommentsAPI(siteKey, before, cb) {
    let url = `http://localhost/hw1_12/api_comments.php?site_key=${siteKey}`
    if (before) {
        url += '&before=' + before
    }
    $.ajax({
        url,
    }).done(function (data) {
        cb(data)
    });
}

原本拿資料(載入資料)變成

getCommentsAPI(siteKey, null, data => {
    if (!data.ok) {
        alert(data.message)
        return
    }

    const comments = data.discussions;
    for (let comment of comments) {
        appendCommentToDOM(commentsDOM, comment)
    }
})

2. 加入 載入更多 button

const loadMoreButtonHTML = '<button class="load-more btn btn-primary">載入更多</button>'

這樣載入完把 btn 加進去 commentsDOM.append(loadMoreButtonHTML)

const commentsDOM = $('.comments')
getCommentsAPI(siteKey, null, data => {
    if (!data.ok) {
        alert(data.message)
        return
    }

    const comments = data.discussions;
    for (let comment of comments) {
        appendCommentToDOM(commentsDOM, comment)
    }
    commentsDOM.append(loadMoreButtonHTML)
})

3. 按了載入更多 button,會重複做拿資料這件事,所以再把 getCommentsAPI 這個 function 再包一層 function

這樣要用到拿資料就直接呼叫 getComments() 就好
然後記得每呼叫一次 getComments() 要先把舊的載入更多按鈕給隱藏掉 $('.load-more').hide(); 然後會再產生新的載入更多按鈕 commentsDOM.append(loadMoreButtonHTML)

function getComments() {
    $('.load-more').hide(); // 把舊的載入更多按鈕給隱藏掉
    getCommentsAPI(siteKey, lastId, data => {
        const commentsDOM = $('.comments')
        if (!data.ok) {
            alert(data.message)
            return
        }

        const comments = data.discussions;
        for (let comment of comments) {
            appendCommentToDOM(commentsDOM, comment)
        }
        let length = comments.length
        lastId = comments[length - 1].id

        commentsDOM.append(loadMoreButtonHTML) // 產生新的載入更多按鈕
    })
}

然後拿完資料 id 要更新

const comments = data.discussions;
for (let comment of comments) {
    appendCommentToDOM(commentsDOM, comment)
}
let length = comments.length
lastId = comments[length - 1].id //最後一個 id

4. 當全部資料秀出來,就不要再有載入更多按鈕

在 getComments 這個 function 裡面來判斷 comments.length 是否小於每次秀五筆資料,如果小於五筆,代表是最後資料了,那就把 載入更多按鈕 隱藏並且 return ,但如果最後一次是剛好五筆,還沒想到解決辦法

let length = comments.length
console.log(length)
if (length < 5) {
    $('.load-more').hide();
    return
} else {
    lastId = comments[length - 1].id
    commentsDOM.append(loadMoreButtonHTML)
}

5. 原本在 part2 那邊新增留言直接把資料放上去,但因為要拿到 id 和時間,所以改成再 query API 拿資料

因此在 api_add_comments.php 新增留言這個 API 多增加 sql 子句,讓前端可以拿到資料

$sql = "SELECT * FROM discussions WHERE id = @@IDENTITY";

api_add_comments.php 全部程式

<?php
require_once "conn.php";
header('Content-type:application/json;charset=utf-8'); //要輸出 json 記得要加這行 header,瀏覽器才會知道他是 json 格式的資料
header('Access-Control-Allow-Origin: *');

if (empty($_POST['content']) ||
    empty($_POST['nickname']) ||
    empty($_POST['site_key'])) {
    $json = array(
        "ok" => false,
        "message" => "Please input missing fields",
    );

    $response = json_encode($json); //變成 json 的格式
    echo $response;
    die();
}

$site_key = $_POST['site_key'];
$nickname = $_POST['nickname'];
$content = $_POST['content'];

$sql = "INSERT INTO discussions(site_Key, nickname, content) VALUES (?, ?, ?) ";
$stmt = $conn->prepare($sql);
$stmt->bind_param('sss', $site_key, $nickname, $content);
$result = $stmt->execute();

if (!$result) {
    $json = array(
        "ok" => false,
        "message" => $conn->error,
    );

    $response = json_encode($json);
    echo $response;
    die();
}

$sql = "SELECT * FROM discussions WHERE id = @@IDENTITY";
$stmt = $conn->prepare($sql);
$result = $stmt->execute();
$result = $stmt->get_result();
$discussions = array();
$row = $result->fetch_assoc();
array_push($discussions, array(
    "id" => $row["id"],
    "nickname" => $row["nickname"],
    "content" => $row["content"],
    "created_at" => $row["created_at"],
));

if (!$result) {
    $json = array(
        "ok" => false,
        "message" => $conn->error,
    );

    $response = json_encode($json);
    echo $response;
    die();
}

$json = array(
    "ok" => true,
    "discussions" => $discussions
);

$response = json_encode($json);
echo $response;

js 的全部程式


function escape(toOutput) {
    return toOutput.replace(/\&/g, '&amp;')
        .replace(/\</g, '&lt;')
        .replace(/\>/g, '&gt;')
        .replace(/\"/g, '&quot;')
        .replace(/\'/g, '&#x27')
        .replace(/\//g, '&#x2F');
}

function appendCommentToDOM(container, comment, isPrepend) {
    const html = `
    <div class="card">
        <div class="card-body">
            <div class="d-flex justify-content-between">
            <div class="d-flex">
                <div class="card_avartar text-center text-white">
                    ${comment.id}
                </div>
                <h5 class="card-title">${comment.nickname}</h5>
            </div>
                <p>${comment.created_at}</p>
            </div>
        <p class="card-text">${comment.content}</p>
        </div>
    </div>
        `
    if (isPrepend) {
        container.prepend(html)
    } else {
        container.append(html)
    }
}

function getCommentsAPI(siteKey, before, cb) {
    let url = `http://tzutzu858.tw/json/api_comments.php?site_key=${siteKey}`
    if (before) {
        url += '&before=' + before
    }
    $.ajax({
        url,
    }).done(function (data) {
        cb(data)
    });
}

function getComments() {
    $('.load-more').hide();
    getCommentsAPI(siteKey, lastId, data => {
        const commentsDOM = $('.comments')
        if (!data.ok) {
            alert(data.message)
            return
        }

        const comments = data.discussions;
        for (let comment of comments) {
            appendCommentToDOM(commentsDOM, comment)
        }
        let length = comments.length
        if (length < 5) {
            $('.load-more').hide();
            return
        } else {
            lastId = comments[length - 1].id
            commentsDOM.append(loadMoreButtonHTML)
        }
    })
}

const siteKey = 'tzu'
const loadMoreButtonHTML = '<button class="load-more btn btn-primary">載入更多</button>'
let lastId = null

$(document).ready(() => {
    const commentsDOM = $('.comments')

    //第一次載入資料
    getComments();

    // 載入更多
    $('.comments').on('click', '.load-more', () => {
        getComments();
    })

    // 新增留言
    $('.add-comment-form').submit(e => {
        e.preventDefault();
        const newCommentData = {
            site_key: 'tzu',
            nickname: $('input[name=nickname]').val(),
            content: $('textarea[name=content]').val()
        }

        $.ajax({
            type: 'POST',
            url: 'http://tzutzu858.tw/json/api_add_comments.php',
            data: newCommentData
        }).done(function (data) {
            if (!data.ok) {
                alert(data.message)
                return
            }
            $('input[name=nickname]').val('') // 成功之後傳個空字串清空
            $('textarea[name=content]').val('')
            const comments = data.discussions;
            for (let comment of comments) {
                appendCommentToDOM(commentsDOM, comment, true)
            }
        });
    })
})









Related Posts

DAY 01 : 資料結構緒論

DAY 01 : 資料結構緒論

Object 物件屬性特徵

Object 物件屬性特徵

AJAX - HTTP狀態碼

AJAX - HTTP狀態碼


Comments