PHP 与浏览器 Ajax:从 JSONP 迁移到 Fetch
本文目录
原始发布日期保留为 2018-07-20。本文于 2026-10-19 重整:保留“输入关键词、请求 PHP、呈现建议”的目标,移除旧版 HTTP JSONP、外部搜索接口依赖和把响应拼成 HTML 的实现。
先选对通信方式
同源页面调用自己的 PHP API,优先使用 fetch() 请求 JSON。JSONP 是通过 <script> 标签载入外部 JavaScript 来绕过早期浏览器跨域限制的历史方案;响应会以当前页面权限执行,不应作为新接口的默认设计,也不应发送密码、令牌或私有数据。
跨域请求由服务端 CORS 响应头决定浏览器是否允许脚本读取结果。前后端分离时,应只允许明确的站点来源;需要携带 Cookie 时,不能用 Access-Control-Allow-Origin: *,也不能无条件反射任意 Origin。能用同源 API 时,通常不需要开放 CORS。
浏览器端:防止旧请求覆盖新结果
输入联想至少要考虑防抖、取消过期请求、HTTP 错误、响应结构和安全 DOM 更新。远端文本只当作文本显示,不把它拼进 innerHTML。
<label for="keyword">搜索建议</label>
<input id="keyword" type="search" autocomplete="off" aria-controls="suggestions">
<ul id="suggestions"></ul>
<p id="suggestion-status" role="status" aria-live="polite"></p>
const input = document.querySelector('#keyword');
const list = document.querySelector('#suggestions');
const status = document.querySelector('#suggestion-status');
let debounceTimer = 0;
let activeController;
let requestSequence = 0;
input.addEventListener('input', () => {
window.clearTimeout(debounceTimer);
activeController?.abort();
const query = input.value.trim();
const sequence = ++requestSequence;
if (query.length < 2) {
list.replaceChildren();
status.textContent = '至少输入两个字符。';
return;
}
debounceTimer = window.setTimeout(() => {
void loadSuggestions(query, sequence);
}, 180);
});
async function loadSuggestions(query, sequence) {
const controller = new AbortController();
activeController = controller;
try {
const params = new URLSearchParams({ q: query });
const response = await fetch(`/api/suggestions.php?${params}`, {
headers: { Accept: 'application/json' },
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
if (sequence !== requestSequence) return;
if (!Array.isArray(payload.items)) {
throw new TypeError('Invalid suggestion response');
}
const fragment = document.createDocumentFragment();
for (const item of payload.items) {
if (!item || typeof item.label !== 'string') continue;
const option = document.createElement('li');
option.textContent = item.label;
fragment.append(option);
}
list.replaceChildren(fragment);
status.textContent = list.childElementCount > 0
? `找到 ${list.childElementCount} 条建议。`
: '没有匹配结果。';
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return;
if (sequence !== requestSequence) return;
list.replaceChildren();
status.textContent = '暂时无法获取搜索建议,请稍后重试。';
}
}
实际产品还应补全键盘上下选择、回车确认、Esc 关闭、焦点管理和无结果状态。如果列表项可操作,需要实现完整的可访问交互,而不是只给元素添加 ARIA 属性。
PHP 端:返回受限 JSON
下面的 API 假设 bootstrap.php 创建了有超时和正确错误模式的 PDO 连接,suggestions 表有 label 与 popularity 字段。生产代码还应按部署情况增加认证、速率限制和缓存策略。
<?php
require_once __DIR__ . '/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
function sendJson(int $status, array $body): void
{
http_response_code($status);
echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
header('Allow: GET');
sendJson(405, ['error' => 'method_not_allowed']);
}
$query = $_GET['q'] ?? null;
if (!is_string($query) || !mb_check_encoding($query, 'UTF-8')) {
sendJson(400, ['error' => 'invalid_query']);
}
$query = trim($query);
if ($query === '' || mb_strlen($query, 'UTF-8') > 50) {
sendJson(400, ['error' => 'invalid_query']);
}
$escapedQuery = strtr($query, ['!' => '!!', '%' => '!%', '_' => '!_']);
try {
$statement = $pdo->prepare(
"SELECT label FROM suggestions
WHERE label LIKE :prefix ESCAPE '!'
ORDER BY popularity DESC
LIMIT 10"
);
$statement->execute(['prefix' => $escapedQuery . '%']);
$labels = $statement->fetchAll(PDO::FETCH_COLUMN);
$items = array_map(
static fn (string $label): array => ['label' => $label],
$labels,
);
sendJson(200, ['items' => $items]);
} catch (Throwable $error) {
error_log('Suggestion query failed: ' . $error::class);
sendJson(500, ['error' => 'server_error']);
}
?>
参数化语句保护 SQL 结构;LIKE 通配符仍有查询语义,所以示例显式转义 %、_ 与转义符。接口限制请求方法、输入类型、字符集、长度和返回行数;错误响应不暴露 SQL、堆栈或连接配置。示例使用 mbstring,部署时应在运行时依赖中声明并验证该扩展。
从旧 JSONP 迁移时检查
- 新接口返回 JSON,不再返回可执行脚本;浏览器按 HTTP 状态与 JSON schema 处理响应。
- 第三方建议服务若支持 CORS,前端可直接 Fetch;若不支持,应由受控后端代理,校验目标地址和响应大小,避免把代理做成 SSRF 入口。
- 不要把不可信数据拼进 HTML;普通文本用
textContent。确需富文本时使用经过审查的 HTML sanitizer 和严格白名单。 - 搜索请求应做防抖、超时/取消、结果数限制和速率限制;用户输入可能包含隐私,日志需最小化。
- 有权限要求的数据必须在服务端鉴权。CORS 不是访问控制,也不是认证机制。
参考资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。