本篇為 [BE101] 用 PHP 與 MySQL 學習後端基礎 這門課程的學習筆記。如有錯誤歡迎指正。
hw1:JavaScript 留言板
參考筆記
什麼是 API?
API 就是純資料的交換。資料以 JSON 形式儲存。
在第八週時,我們學會使用 JavaScript 來串接 API,前端負責顯示資料,後端只負責提供資料。
之前實作的留言板是透過 PHP 直接輸出內容。這週我們會透過 PHP 實作 API,再使用 JavaScript 串接 API 來動態顯示資料。
如何測試 API
有幾種方式能夠測試 API 是否能成功運行。可參考這篇文章介紹:API 實作(三):以 Postman 測試 API
- 瀏覽器:撰寫程式碼不易,步驟繁瑣
- curl 工具:不易進行 debug
- Postman:方便使用,能夠針對不同分頁或欄位進行測試
練習:實作無會員機制的留言版 API
PHP 相關語法
header('Content-Type: application/json; charset=utf-8');:指定瀏覽器以 JSON 格式內容,UTF-8 字元編碼
array_push():在一個陣列中,再插入一個值進去
- 語法:
array_push(欲增加的陣列, 值)1 2 3 4 5 6
| <?php $array = array(); array_push($array, "Test"); print_r($array); ?>
|
用 PHP 實作 API
首先要瞭解如何使用 PHP 做出 API,以 api_comments.php 下列程式碼為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?php $comments = array(); array_push($comments, array( "id" => 1, "username" => "aaa", "content" => "123" )); array_push($comments, array( "id" => 2, "username" => "bbb", "content" => "456" )); $json = array( "comments" => $comments );
$response = json_encode($json); header('Content-Type: application/json; charset=utf-8'); echo $response; ?>
|
在瀏覽器接收到的 response 就是 JSON 格式的物件,可使用開發者工具查看內容:

實作 API:列出所有文章
把之前實作留言板 index.php 時,使用的語法結合到 api_comments.php,即可得到只輸出資料的 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php require_once("conn.php");
$page = 1; if (!empty($_GET['page'])) { $page = intval($_GET['page']); } $items_per_page = 5; $offset = ($page - 1) * $items_per_page;
$sql = "SELECT ". "C.id as id, C.content AS content, ". "C.created_at AS created_at, U.nickname AS nickname, U.username AS username ". "FROM heidi_comments AS C ". "LEFT JOIN heidi_users AS U ON C.username = U.username ". "WHERE C.is_deleted IS NULL ". "ORDER BY C.id DESC ". "LIMIT ? OFFSET ? "; $stmt = $conn->prepare($sql); $stmt->bind_param("ii", $items_per_page, $offset); $result = $stmt->execute(); if (!$result) { die('Error:' . $conn->error); } $result = $stmt->get_result(); $comments = array();
while($row = $result->fetch_assoc()) { array_push($comments, array( "id" => $row['id'], "username" => $row['username'], "nickname" => $row['nickname'], "content" => $row['content'], "created_at" => $row['created_at'] )); } $json = array( "comments" => $comments );
$response = json_encode($json); header('Content-Type: application/json; charset=utf-8'); echo $response; ?>
|
上述程式碼,和 index.php 同樣是讀取資料,差別在於 API 是把資料放到陣列 $comments,裡面再建立陣列 array,概念比較像 JS 物件。
實作 API:新增文章
以 api_add_comment.php 為例,寫法會和 handle_add_comment.php(新增留言功能)的邏輯類似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php require_once('conn.php');
header('Content-Type: application/json; charset=utf-8'); if ( empty($_POST['content']) ) { $json = array( "ok" => false, "message" => "Please input content" );
$response = json_encode($json); echo $response; die(); } $username = $_POST['username']; $content = $_POST['content'];
$sql = "INSERT INTO heidi_comments(username, content) VALUES(?, ?)"; $stmt = $conn->prepare($sql); $stmt->bind_param('ss', $username, $content); $result = $stmt->execute(); if (!$result) { $json = array( "ok" => false, "message" => $conn->error );
$response = json_encode($json); echo $response; die(); } $json = array( "ok" => true, "message" => "Success" );
$response = json_encode($json); echo $response; ?>
|

前端串接 API
最後就是在前端頁面 index.html 串接寫好的 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| <body> <div class="wrapper"> <main class="board"> <div class ="board__header"> <h1 class="board__tittle">Comments</h1> <div class="board__btn-block"> </div> </div> <form class="board__new-comment-form"> <textarea name="content" rows="5" placeholder="請輸入留言..."></textarea> <input class="board__submit-btn" type="submit"> </form> <div class="board__hr"></div>
<section> // 動態新增留言的區塊... </section> </main> </div>
<script> var request = new XMLHttpRequest(); request.open('GET', 'api_comments.php', true);
request.onload = function() { if (this.status >= 200 && this.status < 400) { var resp = this.response; var json = JSON.parse(resp) var comments = json.comments
for (var i = 0; i < comments.length; i++) { var comment = comments[i] var div = document.createElement('div') div.classList.add('card') div.innerHTML = ` <div class="card__avatar"></div> <div class="card__body"> <div class="card__info"> <span class="card__author"> ${encodeHTML(comment.nickname)}(@${encodeHTML(comment.username)}) </span> <span class="card__time"> ${encodeHTML(comment.created_at)} </span> </div> <p class="card__content">${encodeHTML(comment.content)}</p> </div> ` document.querySelector('section').appendChild(div) } } }; request.send();
var form = document.querySelector('.board__new-comment-form') form.addEventListener('submit', function(e) { e.preventDefault() var content = document.querySelector('textarea[name=content]').value var request = new XMLHttpRequest(); request.open('POST', 'api_add_comment.php', true); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); request.send("username=aaa&content=" + encodeURIComponent(content)); request.onload = function() { if (this.status >= 200 && this.status < 400) { var resp = this.response; var json = JSON.parse(resp) if (json.ok) { location.reload() } else { alert(json.message) } } } }) function encodeHTML(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"'); } </script> </body>
|
實戰:增強版 JavaScript 留言板
接著要來打造後端 API,再利用前端 JavaScript 來串接 API 實作留言板功能。
建立後端 API
Step1. 建立資料庫 discussions
- id
- site_key
- nickname
- content
- created_at
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php require_once('conn.php'); header('Content-Type: application/json; charset=utf-8'); if ( empty($_POST['nickname']) || empty($_POST['site_key']) || empty($_POST['content']) ) { $json = array( "ok" => false, "message" => "Please input content" ); $response = json_encode($json); echo $response; die(); }
$nickname = $_POST['nickname']; $site_key = $_POST['site_key']; $content = $_POST['content'];
$sql = "INSERT INTO heidi_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(); } $json = array( "ok" => true, "message" => "success" ); $response = json_encode($json); echo $response; ?>
|
利用 postman 以 POST 方式發出 request 測試,確認是否能新增留言到資料庫:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?php require_once('conn.php'); header('Content-Type: application/json; charset=utf-8'); if ( empty($_GET['site_key']) ) { $json = array( "ok" => false, "message" => "Please add site_key in url" );
$response = json_encode($json); echo $response; die(); }
$site_key = $_GET['site_key'];
$sql = "SELECT nickname, content, created_at FROM heidi_discussions WHERE site_key = ? ORDER BY id DESC"; $stmt = $conn->prepare($sql); $stmt->bind_param('s', $site_key); $result = $stmt->execute(); if (!$result) { $json = array( "ok" => false, "message" => $conn->error ); $response = json_encode($json); echo $response; die(); } $result = $stmt->get_result(); $discussions = array(); while($row = $result->fetch_assoc()) { array_push($discussions, array( "nickname" => $row["nickname"], "content" => $row["content"], "created_at" => $row["created_at"] )); }
$json = array( "ok" => true, "discussions" => $discussions ); $response = json_encode($json); echo $response; ?>
|
利用 postman 以 GET 方式發出 request 測試,確認是否能讀取留言:

這樣就完成後端 API 的新增留言和顯示留言功能。
前端串接 API
Step1. 建立 UI 頁面
首先利用 Bootstrap 來快速建立前端頁面 index.html
Step2. 將前端頁面串接 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Week12 留言板</title> <!-- 引入 jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.js"></script> <!-- 引入 Bootstrap --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> <style> .add-comment-form { margin-bottom: 10px; } .card { margin-bottom: 10px; } .card-body h5, .card-body span { display: inline-block; margin-right: 20px; } </style> <script> // 跳脫函式 function escape(toOutput) { return toOutput .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // 渲染 comment: 處理讀取的資料 & 決定加在最前面或最後面 function appendCommentToDOM(container, comment, isPrepend) { const html = ` <div class="card"> <div class="card-body"> <h5 class="card-title">${escape(comment.nickname)}</h5> <span>${escape(comment.created_at)}</span> <p class="card-text">${escape(comment.content)} </p> </div> </div> `; if (isPrepend) { container.prepend(html); } else { container.append(html); } }
const showUrl = 'http://localhost/heidi/week12_local/hw1/api_comments.php?site_key=heidi'; const addUrl = 'http://localhost/heidi/week12_local/hw1/api_add_comments.php';
$(document).ready(() => { // 顯示留言 const commentDOM = $('.comments') $.ajax({ url: showUrl, }).done(function (data) { if (!data.ok) { alert(data.message); return; } // 若 request 成功讀取資料 const comments = data.discussions; for (let comment of comments) { appendCommentToDOM(commentDOM, comment); } }); // 新增留言: 將資料存到後端 $('.add-comment-form').submit(e => { e.preventDefault(); // 取消原生行為 -> 不會送出表單 const newCommentData = { 'site_key': 'heidi', 'nickname': $('input[name=nickname]').val(), 'content': $('textarea[name=content]').val() } $.ajax({ type: 'POST', url: addUrl, data: newCommentData }).done(function(data) { // done(): 以函數處理回傳的 data 資料 // 執行失敗 if (!data.ok) { alert(data.message); return; } // 執行成功: 按下送出後把欄位清空 $('input[name=nickname]').val(''); $('textarea[name=content]').val(''); // 新增留言後以 JS 動態方式加到最上方 appendCommentToDOM(commentDOM, newCommentData, true); }); }); }); </script> </head>
<body> <div class="container"> <form class="add-comment-form"> <div class="form-group"> <label for="form-nickname">暱稱</label> <input name="nickname" type="text" class="form-control" id="form-nickname" > </div> <div class="form-group"> <label for="content-textarea">留言內容</label> <textarea name="content" class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea> </div> <button type="submit" class="btn btn-dark">送出</button> </form> <div class="comments"> <!-- 以 JavaScript 動態顯示資料的區塊 --> </div> </div>
</body> </html>
|
Step3. 實作分頁機制
1 2
| SELECT * FROM comments ORDER BY id DESC LIMIT 5 OFFSET 5
|
Cursor-based pagination
- 基於 Cursor(指標)的分頁
- 可透過指定明確的起始點(Pointer)來回傳資料,例如:id 或 created_at
- 缺點:沒有「總和」和「頁數」的概念
相關函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function getComments() { const commentDOM = $('.comments'); $('.load-more').hide(); if (isEnd) { return; } getCommentsAPI(siteKey, lastId, data => { if (!data.ok) { alert(data.message); return; } const comments = data.discussions; for (let comment of comments) { appendCommentToDOM(commentDOM, comment); } let length = comments.length; if (!lastId && length < 5) { return (comments.length < 5); } if (length === 0) { isEnd = true; $('.load-more').hide(); } else { lastId = comments[length - 1].id; $('.comments').append(loadMoreButtonHTML); } }); }
|
參考資料:
- 深入淺出 GraphQL Pagination 實作
debug
錯誤一 Reason
1
| Reason: CORS header 'Access-Control-Allow-Origin' missing
|
- 原因:缺少表頭
header('Access-Control-Allow-Origin: *');
- 實際情況:可能是 php 檔語法上有錯誤,才會出現這個錯誤訊息
錯誤二 TypeError
1
| TypeError: Cannot read property 'replace' of undefined
|

- 原因:要進行跳脫的值為 null
- 解決辦法:先判斷該值是否為空再進行 replace 操作
參考網站:Cannot read property ‘replace’ of undefined