AJAX PHP
Writing a JSON endpoint in PHP and consuming it with fetch.
Key PointsAn endpoint emits JSON only, never HTML, and must set `Content-Type`.
The endpoint
PHP
<?php
// /api/greet.php
declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");
header("X-Content-Type-Options: nosniff");
function respond(array $data, int $status = 200): never {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
$name = trim((string)($_GET["name"] ?? ""));
if ($name === "") {
respond(["ok" => false, "msg" => "name 不能为空"], 400);
}
if (mb_strlen($name) > 20) {
respond(["ok" => false, "msg" => "name 太长"], 422);
}
respond([
"ok" => true,
"msg" => "你好,{$name}",
"time" => "2023-11-15 06:13:20",
]);A normal request ?name=Tom returns:
Output
{"ok":true,"msg":"你好,Tom","time":"2023-11-15 06:13:20"}An empty parameter returns HTTP 400:
Output
{"ok":false,"msg":"name 不能为空"}Declaring
never (8.1+) tells static analysers the function never returns because it calls exit — removing false positives downstream.The client
JavaScript
async function greet(name) {
const url = "/api/greet.php?name=" + encodeURIComponent(name);
const res = await fetch(url, { headers: { "Accept": "application/json" } });
const data = await res.json(); // 4xx 也有 JSON body
if (!data.ok) {
document.querySelector("#err").textContent = data.msg;
return;
}
document.querySelector("#out").textContent = data.msg;
}
document.querySelector("#btn").addEventListener("click", () => {
greet(document.querySelector("#name").value);
});The key: **parse the JSON first, then check data.ok**. A 400 response still carries a JSON body, so branching on res.ok alone loses the message.
POST with a JSON body
JavaScript
const res = await fetch("/api/user.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Tom", age: 18 }),
});
const data = await res.json();
console.log(data);PHP
<?php
// /api/user.php
header("Content-Type: application/json; charset=utf-8");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo json_encode(["ok" => false, "msg" => "只接受 POST"]);
exit;
}
$raw = file_get_contents("php://input");
$data = json_decode($raw, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["ok" => false, "msg" => "非法 JSON"]);
exit;
}
echo json_encode([
"ok" => true,
"name" => $data["name"] ?? "",
"age" => (int)($data["age"] ?? 0),
], JSON_UNESCAPED_UNICODE);Output
{"ok":true,"name":"Tom","age":18}With a JSON body
$_POST is empty — read php://input. The most common trap in PHP API work with Vue/React.API conventions
| Concern | Practice |
|---|---|
| 状态码 | 200 成功、400 参数错、401 未登录、403 无权限、404 不存在、422 校验失败、500 服务错 |
| 响应体 | 固定 {ok, msg, data} 结构 |
| 字符集 | application/json; charset=utf-8 |
| 错误信息 | 给人看的话,不含堆栈和 SQL |
| 鉴权 | Authorization: Bearer <token>,别用 Cookie 传 token |
接口脚本
PHP
<?php
// /api/greet.php
declare(strict_types=1);
header("Content-Type: application/json; charset=utf-8");
header("X-Content-Type-Options: nosniff");
function respond(array $data, int $status = 200): never {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
$name = trim((string)($_GET["name"] ?? ""));
if ($name === "") {
respond(["ok" => false, "msg" => "name 不能为空"], 400);
}
if (mb_strlen($name) > 20) {
respond(["ok" => false, "msg" => "name 太长"], 422);
}
respond([
"ok" => true,
"msg" => "你好,{$name}",
"time" => "2023-11-15 06:13:20",
]);正常请求 ?name=Tom 返回:
Output
{"ok":true,"msg":"你好,Tom","time":"2023-11-15 06:13:20"}空参数返回 HTTP 400:
Output
{"ok":false,"msg":"name 不能为空"}respond() 返回类型写 never(PHP 8.1+),表示「这个函数永远不返回」。它内部 exit,静态分析工具据此知道后面代码不可达,能少报一堆误告警。前端调用
JavaScript
async function greet(name) {
const url = "/api/greet.php?name=" + encodeURIComponent(name);
const res = await fetch(url, { headers: { "Accept": "application/json" } });
const data = await res.json(); // 4xx 也有 JSON body
if (!data.ok) {
document.querySelector("#err").textContent = data.msg;
return;
}
document.querySelector("#out").textContent = data.msg;
}
document.querySelector("#btn").addEventListener("click", () => {
greet(document.querySelector("#name").value);
});关键点:**先 await res.json() 再看 data.ok**。因为 400 响应体里也有 JSON,直接按 res.ok 分支会丢掉错误信息。
POST 与 JSON 体
JavaScript
const res = await fetch("/api/user.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Tom", age: 18 }),
});
const data = await res.json();
console.log(data);PHP
<?php
// /api/user.php
header("Content-Type: application/json; charset=utf-8");
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo json_encode(["ok" => false, "msg" => "只接受 POST"]);
exit;
}
$raw = file_get_contents("php://input");
$data = json_decode($raw, true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(["ok" => false, "msg" => "非法 JSON"]);
exit;
}
echo json_encode([
"ok" => true,
"name" => $data["name"] ?? "",
"age" => (int)($data["age"] ?? 0),
], JSON_UNESCAPED_UNICODE);Output
{"ok":true,"name":"Tom","age":18}发 JSON 时
$_POST 是空的,必须读 php://input。这是 PHP 接口开发最容易踩的坑,尤其在用 Vue / React 的时候。接口约定
| 关注点 | 做法 |
|---|---|
| 状态码 | 200 成功、400 参数错、401 未登录、403 无权限、404 不存在、422 校验失败、500 服务错 |
| 响应体 | 固定 {ok, msg, data} 结构 |
| 字符集 | application/json; charset=utf-8 |
| 错误信息 | 给人看的话,不含堆栈和 SQL |
| 鉴权 | Authorization: Bearer <token>,别用 Cookie 传 token |