本篇為 [BE101] 用 PHP 與 MySQL 學習後端基礎 這門課程的學習筆記。如有錯誤歡迎指正。
hw2:Todo List
前端實作
目標功能:
- 新增功能:add
- 刪除功能:delete
- 編輯功能:update
- 更新狀態功能:checked / unchecked
- 切換列表:All / Active / Completed
- 清除所有 todo:Clear
以上功能只要利用前端就能達成,程式碼如下:
前端介面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Week12 Todo List</title>
<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="wrapper">
<div class="todo__header">
<button type="button" class="btn btn-save" href="#">Save</button>
<button type="button" class="btn clear-all" href="#">Reset</button>
</div>
<h1>Todo List</h1>
<div class="todo__input-block">
<input class="todo__input" type="text" placeholder="Add New Todo Here..." minlength="1" maxlength="128">
<button class="btn-new"></button>
</div>
<ul class="nav nav-middle justify-content-center todo__status">
<li class="nav-item">
<a class="nav-link active" href="#" data-filter="all">All</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-filter="in-progress">In Progress</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-filter="completed">Completed</a>
</li>
</ul>
<ul class="todo__list">
<!-- 要新增 template 的區塊 -->
<li class="todo">
<input class="todo__check" type="checkbox" id="todo-0">
<label class="todo__title" for="todo">Coding</label>
<button class="btn-delete"></button>
</li>
</ul>
</div>
</body>
</html>
前端 JavaScript
<script>
let id = 1;
const template = `
<li class="todo">
<input class="todo__check" type="checkbox" id="todo-{id}">
<label class="todo__title" for="todo-{id}">{content}</label>
<button class="btn-delete"></button>
</li>
`
// 新增功能: 點擊
$('.btn-new').click(() => {
addTodo()
});
// 新增功能: 按 Enter
$('.todo__input').keydown(e => {
if (e.key === 'Enter') {
addTodo()
}
});
// 刪除功能: 利用事件代理
$('.todo__list').on('click', '.btn-delete', (e) => {
$(e.target).parent().remove();
});
// 標記狀態: 已完成 / 未完成
$('.todo__list').on('change', '.todo__check', (e) => {
const target = $(e.target);
const isChecked = target.is(":checked");
if (isChecked) {
target.parents('.todo').addClass('checked');
} else {
target.parents('.todo').removeClass('checked');
}
});
// 篩選 todo 狀態
$('.todo__status').on('click', 'a', e => {
const target = $(e.target);
const filter = target.attr('data-filter');
$('.todo__status a.active').removeClass('active');
target.addClass('active');
if (filter === 'all') {
$('.todo').show();
} else if (filter === 'in-progress') {
$('.todo').show();
$('.todo.checked').hide();
} else { // completed
$('.todo').hide();
$('.todo.checked').show();
}
});
// 清除所有 todo
$('.clear-all').click(() => {
$('.todo').remove();
});
// 儲存 todo
$('.btn-save').click(() => {
let todos = [];
$('.todo').each((i, element) => {
const input = $(element).find('.todo__check');
const label = $(element).find('.todo__title');
todos.push({
id: input.attr('id').replace('todo-', ''), // 把 todo-id 的 todo- 換成空字串
content: label.text(),
isDone: $(element).hasClass('checked')
});
});
JSON.stringify(todos); // 將 JS 物件轉換成 JSON 字串
});
function addTodo() {
const value = $('.todo__input').val();
if (!value) return;
$('.todo__list').prepend(
template
.replace('{content}', escape(value))
.replace(/{id}/g, id)
);
id += 1;
$('.todo__input').val('');
}
function escape(toOutput) {
return toOutput
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
</script>
前後端串接
目標功能
- 儲存功能:Save
PHP API
將前端得到的資料 JSON.stringify(todos)
,利用 Ajax 方式 POST 到資料庫:
// 儲存 todo
$('.btn-save').click(() => {
let todos = [];
$('.todo').each((i, element) => {
const input = $(element).find('.todo__check');
const label = $(element).find('.todo__title');
todos.push({
id: input.attr('id').replace('todo-', ''), // 把 todo-id 的 todo- 換成空字串
content: label.text(),
isDone: $(element).hasClass('checked')
});
});
const data = JSON.stringify(todos); // 將 JS 物件轉換成 JSON 字串
$.ajax({
type: 'POST',
url: 'http://localhost/heidi/week12_local/hw2/api_add_todo.php',
data: {
todo: data
},
success: function(resp) {
const respId = resp.id
window.location = 'index.html?id=' + respId;
},
error: function () {
alert('Error!');
}
});
});
如此即可利用前端 JavaScript 從後端拿取 JSON 格式的資料,並用 JSON.parse()
將 JSON 字串轉換成 JavaScript 物件:
// URLSearchParams(): 解析網址參數
const serchParams = new URLSearchParams(window.location.search); // ?id=...
const todoId = serchParams.get('id');
if (todoId) {
$.getJSON('http://localhost/heidi/week12_local/hw2/api_get_todo.php?id=' + todoId, function (data) {
const todos = JSON.parse(data.data.todo);
restoreTodos(todos);
});
}
將拿到的資料再以 JS 處理新增到頁面,把模版 template 加上 content、id、todoClass:
function restoreTodos(todos) {
if (todos.length === 0) return;
// id 要從讀取的最後一個 todo id 繼續增加
id = todos[todos.length - 1].id + 1;
for(let i = 0; i < todos.length; i++) {
const todo = todos[i];
$('.todo__list').prepend(
template
.replace('{content}', escape(todo.content))
.replace(/{id}/g, todo.id)
.replace('{todoClass}', todo.isDone ? 'checked' : '')
);
}
}
Single Page Application
Single Page Application(單頁面應用程式),簡稱 SPA。是前端利用 Ajax 以非同步方式串接後端 API,如此可將前後端分離,在交換資料時不需換頁,可透過動態方式更新部分頁面。
而早期的網頁主要採用 Multiple Page Application(多頁式應用程式)設計,與 SPA 概念相對應,每次交換資料時都需換頁。
SPA 的優缺點
優點
- 增進使用者體驗
不需換頁即可載入新的資訊。例如 Gmail 或影音播放網站,可以在播放音樂的同時,繼續瀏覽網站其他資訊。 - 前後端分離
後端只需負責制定 API 文件,提供前端資料。前端則利用 Ajax 從後端拿取資料,並以 JavaScript 在 html 動態產生內容。
缺點
- SEO(搜尋引擎最佳化)較差
由於 SPA 是利用 JavaScript 動態產生內容,檢視原始碼會發現原始內容是空的,
解決方法:第一次頁面由 Server side render,之後的操作都改用 Client side render,就可以保證搜尋引擎也能爬到完整的 HTML。 - 前端工作複雜化
原先是利用不同路由處理不同功能,改成由單一頁面統一管理,就像在網頁上實作 APP。 - 初次載入頁面費時
初次瀏覽頁面時會需要下載 JavaScript 或是其他頁面的 template。
由後端負責提供只輸出資料的 API vs PHP 直接輸出內容
後端負責提供只輸出資料的 API
- Server 端接收到請求,會回傳 JSON 或其他特定格式的資料給前端,瀏覽器再將資料動態更新至頁面
- 因為是動態產生資料,檢視原始碼會發現動態更新的內容是空的
PHP 直接輸出內容
- Server 端接收到請求,會將所需資料與頁面經處理後回傳 html 檔給前端,瀏覽器透過重整頁面顯示
- 因此回傳的頁面,檢視原始碼是有包含資料的
參考資料:
- 跟著小明一起搞懂技術名詞:MVC、SPA 與 SSR
- 前後端分離與 SPA
- Day20– 前端小字典三十天【每日一字】– SPA