註冊表單中即時檢查使用者名稱是否已存在,能大幅提升使用者體驗。本文實作完整的 AJAX 前後端整合,涵蓋原生 JavaScript、Fetch API、jQuery 三種寫法,並強調安全性與效能最佳化。
CREATE TABLE `cn_user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 測試資料
INSERT INTO `cn_user` (`username`) VALUES ('jimmy'), ('admin');
<?php
// 統一回應格式
header('Content-Type: application/json; charset=utf-8');
require_once 'db_config.php'; // 包含 PDO 連線設定
$response = ['success' => false, 'message' => '', 'available' => false];
try {
// 1. 取得並驗證參數
$username = trim($_GET['username'] ?? '');
if ($username === '') {
$response['message'] = '使用者名稱不可為空';
echo json_encode($response);
exit;
}
// 長度與格式驗證
if (mb_strlen($username) > 50) {
$response['message'] = '名稱過長';
echo json_encode($response);
exit;
}
if (!preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]+$/u', $username)) {
$response['message'] = '僅限中英數字與底線';
echo json_encode($response);
exit;
}
// 2. 查詢資料庫 (使用預備語句)
$stmt = $pdo->prepare("SELECT 1 FROM cn_user WHERE username = ? LIMIT 1");
$stmt->execute([$username]);
$exists = $stmt->fetchColumn();
// 3. 回應結果
$response['success'] = true;
$response['available'] = !$exists;
$response['message'] = $exists ? '此使用者名稱已被註冊' : '恭喜!此名稱可使用';
} catch (PDOException $e) {
error_log("Username check error: " . $e->getMessage());
$response['message'] = '系統錯誤,請稍後再試';
}
echo json_encode($response);
?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>使用者名稱即時檢查 - Fetch API</title>
<style>
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input { padding: 8px; width: 300px; }
.msg { margin-left: 10px; font-weight: bold; }
.available { color: #28a745; }
.taken { color: #dc3545; }
.checking { color: #6c757d; }
</style>
</head>
<body>
<h2>註冊帳號</h2>
<div class="form-group">
<label>使用者名稱:</label>
<input type="text" id="username" placeholder="輸入使用者名稱" autocomplete="username">
<span id="username-msg" class="msg"></span>
</div>
<script>
const input = document.getElementById('username');
const msgEl = document.getElementById('username-msg');
let debounceTimer = null;
input.addEventListener('input', function() {
clearTimeout(debounceTimer);
const username = this.value.trim();
if (!username) {
msgEl.textContent = '';
msgEl.className = 'msg';
return;
}
msgEl.textContent = '檢查中...';
msgEl.className = 'msg checking';
debounceTimer = setTimeout(() => {
fetch(`check_username.php?username=${encodeURIComponent(username)}`)
.then(res => res.json())
.then(data => {
if (data.success) {
msgEl.textContent = data.message;
msgEl.className = 'msg ' + (data.available ? 'available' : 'taken');
} else {
msgEl.textContent = data.message;
msgEl.className = 'msg taken';
}
})
.catch(err => {
console.error(err);
msgEl.textContent = '檢查失敗,請稍後再試';
msgEl.className = 'msg taken';
});
}, 300); // 300ms 去抖動
});
</script>
</body>
</html>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
$('#username').on('input', function() {
const $this = $(this);
const $msg = $('#username-msg');
clearTimeout($this.data('timer'));
const username = $.trim($this.val());
if (!username) return $msg.text('').removeClass();
$msg.text('檢查中...').removeClass().addClass('checking');
$this.data('timer', setTimeout(() => {
$.getJSON('check_username.php', { username })
.done(data => {
$msg.text(data.message)
.removeClass()
.addClass(data.available ? 'available' : 'taken');
})
.fail(() => $msg.text('檢查失敗').removeClass().addClass('taken'));
}, 300));
});
</script>
<script>
function checkUsername(username) {
const xhr = new XMLHttpRequest();
xhr.open('GET', `check_username.php?username=${encodeURIComponent(username)}`, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
const msgEl = document.getElementById('username-msg');
msgEl.textContent = data.message;
msgEl.className = 'msg ' + (data.available ? 'available' : 'taken');
}
}
};
xhr.send();
}
// 事件綁定同 Fetch API 範例
</script>
| 技術 | 說明 | 效益 |
|---|---|---|
| 去抖動 | 延遲 300ms 送出請求 | 減少 90%+ 不必要請求 |
| 前端驗證 | 長度、格式先在瀏覽器檢查 | 減少無效請求、即時回饋 |
| 唯一索引 | 資料庫層級 UNIQUE constraint | 防止競態條件、資料完整性 |
| 預備語句 | PDO prepare/execute | 防止 SQL 注入 |
| JSON 回應 | 結構化資料交換 | 前端處理簡單、擴充性好 |
| 錯誤日誌 | server 端記錄詳細錯誤 | 不洩露敏感資訊給前端 |
| 速率限制 | IP 級請求頻率限制 (如 30 req/min) | 防止濫用、暴力枚舉 |
前端檢查通過到實際註冊之間,可能被其他人搶註。解決方案:
<?php
// 註冊處理邏輯
try {
$pdo->beginTransaction();
// 鎖定該記錄 (若不存在則插入意向鎖)
$stmt = $pdo->prepare("SELECT 1 FROM cn_user WHERE username = ? FOR UPDATE");
$stmt->execute([$username]);
if ($stmt->fetchColumn()) {
throw new Exception('使用者名稱已存在');
}
// 實際插入
$stmt = $pdo->prepare("INSERT INTO cn_user (username) VALUES (?)");
$stmt->execute([$username]);
$pdo->commit();
echo json_encode(['success' => true, 'message' => '註冊成功']);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
AJAX 即時驗證是提升表單體驗的關鍵技術。核心在於:去抖動減少請求、前後端雙重驗證、資料庫唯一索引防重複、JSON 統一介面。現代專案推薦使用 Fetch API 配合防抖動與速率限制。