照着做完,你会得到一个可运行的 PHP + jQuery Ajax 无刷新表单:前端不跳页、按钮防重复点击、错误信息原样回显,后端带 CSRF 校验和参数验证,返回 JSON 给前端判断。
下面用一个「留言表单」做完整示例,文件只有三个:index.php(页面 + 表单)、submit.js(Ajax 逻辑)、submit.php(接收端)。
第一步:先把后端接收端写出来
前后端约定好返回格式,后面就不会来回改。统一返回 {code, msg, data},code=0 表示成功。
submit.php:
<?php
declare(strict_types=1);
session_start();
header('Content-Type: application/json; charset=utf-8');
function out(int $code, string $msg, array $data = []): void {
echo json_encode(['code' => $code, 'msg' => $msg, 'data' => $data], JSON_UNESCAPED_UNICODE);
exit; // 必须 exit,否则后续输出会污染 JSON
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
out(1, '请求方式不正确');
}
// CSRF 校验
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
out(1, '页面已过期,请刷新后重试');
}
$nickname = trim((string)($_POST['nickname'] ?? ''));
$content = trim((string)($_POST['content'] ?? ''));
if ($nickname === '' || mb_strlen($nickname) > 20) {
out(1, '昵称请填 1-20 个字');
}
if (mb_strlen($content) < 5) {
out(1, '内容至少 5 个字');
}
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$stmt = $pdo->prepare('INSERT INTO messages (nickname, content, created_at) VALUES (?, ?, NOW())');
$stmt->execute([$nickname, $content]);
out(0, '提交成功,感谢留言');
注意:header('Content-Type: application/json') 之后不能再有任何 echo、var_dump、BOM 或空行输出,否则前端 dataType: 'json' 会直接进 fail 分支。
第二步:生成并埋入 CSRF Token
在 index.php 顶部开 session、生成 token,再塞进表单隐藏域:
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
}
?>
<form id="msgForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'], ENT_QUOTES) ?>">
<input type="text" name="nickname" placeholder="昵称">
<textarea name="content" placeholder="说点什么"></textarea>
<button type="submit">提交</button>
</form>
<p id="result"></p>
注意:表单控件必须写 name 属性。serialize() 只认识有 name 的字段,而且被 disabled 的字段不会被提交——提交时禁用按钮是安全的,但别去禁用输入框。
第三步:写 jQuery 提交逻辑
submit.js,核心就四件事:阻止默认跳转、禁用按钮、发请求、按返回码给提示。
$(function () {
var $form = $('#msgForm');
var $btn = $form.find('button[type=submit]');
var $tip = $('#result');
$form.on('submit', function (e) {
e.preventDefault(); // 关键:阻止浏览器默认跳转
if ($btn.prop('disabled')) return; // 双保险,防连点
$btn.prop('disabled', true).text('提交中…');
$tip.text('');
$.ajax({
url: 'submit.php',
type: 'POST',
data: $form.serialize(),
dataType: 'json',
timeout: 15000
}).done(function (res) {
if (res.code === 0) {
$tip.css('color', 'green').text(res.msg);
$form[0].reset();
} else {
$tip.css('color', 'red').text(res.msg);
}
}).fail(function (xhr, status) {
$tip.css('color', 'red')
.text(status === 'timeout' ? '请求超时,请重试' : '网络异常(' + xhr.status + ')');
}).always(function () {
$btn.prop('disabled', false).text('提交');
});
});
});
$form.serialize() 会把所有带 name 的字段拼成 nickname=xx&content=yy&csrf_token=zz 这种 query string,PHP 端用 $_POST 正常读取,中文会自动 URL 编码,不用手动处理。
第四步:验证跑通,再处理边界
打开页面填内容点提交,观察两点:页面没有刷新(地址栏 URL 不变),#result 出现提示。如果提示出不来,按顺序排查:
- 浏览器 F12 → Network,看
submit.php 请求的响应体是不是合法 JSON;
- 是 JSON 但进
fail,多半是状态码不是 200,或响应前面混进了 PHP 警告文字;
- 后端一直返回「页面已过期」,检查 session 是否正常、站点是否 www 和裸域混用导致 session 丢失。
注意:前端校验只是体验优化,后端验证绝不能省。用户可以直接用 Postman 绕过你的 JS。另外 $_POST 只能接 application/x-www-form-urlencoded 和 multipart/form-data;如果你把 contentType 改成 application/json,PHP 端得用 json_decode(file_get_contents('php://input'), true) 取值,此时 $_POST 是空的——这是最常见的坑。
注意:如果表单要传文件,把 data: $form.serialize() 换成 new FormData(this),并加上 processData: false, contentType: false,否则 jQuery 会把 FormData 序列化成字符串。
小结
- 后端先定好 JSON 返回格式,前端只认
code,逻辑最清爽;
e.preventDefault() 是实现无刷新的前提,按钮禁用防重复提交;
- CSRF Token 走隐藏域随表单一起提交,服务端用
hash_equals 比对;
- PHP 端输出 JSON 后立即
exit,避免多余输出破坏 JSON 解析;
Content-Type: application/json 时 $_POST 为空,要读 php://input;
- 前端校验给体验,后端校验保安全,两套都得有。