搞定 jQuery AJAX 报错:Unexpected token '<' 无效 JSON
2025-04-24 14:25:38
搞定 jQuery AJAX 报错:Uncaught SyntaxError: Unexpected token '<'...is not valid JSON
写代码的时候,碰上各种报错是家常便饭。今天咱们就来聊聊一个前端开发者,特别是用 jQuery 发送 AJAX 请求时,可能经常遇到的一个头疼问题:Uncaught SyntaxError: Unexpected token '<', "<br /> <b>"... is not valid JSON
。别看这错误信息长,其实搞清楚了,解决起来并不难。
问题来了:到底是怎么回事?
你可能正在用 JavaScript(这里是 jQuery 的 AJAX)向后台(比如 PHP)请求一些数据,期望拿到的是整整齐齐的 JSON 格式数据,好方便在前端处理和展示。就像下面这段代码想做的那样:
JavaScript (使用 jQuery):
<script>
// 给提交按钮加个点击事件监听
document.querySelector('#submit-button').addEventListener('click', function(event) { // 注意:这里最好显式传入 event 参数
// 阻止表单默认的提交动作
event.preventDefault();
// 拿到 "fromdate" 和 "todate" 下拉框选中的值
var fromDate = $('#fromdate').val();
var toDate = $('#todate').val();
// 发送 AJAX 请求给 PHP 脚本
$.ajax({
type: "POST",
url: "functions/search-sales_function.php",
data: { fromDate: fromDate, toDate: toDate },
success: function (response) {
// 解析 JSON 响应 <--- 问题根源就在这附近!
var salesData = JSON.parse(response);
// 清空旧数据(可选,但通常需要)
$('#sales-table tbody').empty();
// 遍历销售数据,塞进表格里
for (var i = 0; i < salesData.length; i++) {
var date = salesData[i]['date'];
var sales = salesData[i]['total_sales'];
$('#sales-table tbody').append(`
<tr>
<td>${date}</td>
<td>${sales}</td>
</tr>`
);
}
},
error: function(jqXHR, textStatus, errorThrown) {
// 加个错误处理回调,方便调试
console.error("AJAX 请求失败:", textStatus, errorThrown);
console.error("服务器响应:", jqXHR.responseText); // 打印原始响应,非常重要!
}
});
});
</script>
你满心欢喜地等着数据回来,结果浏览器控制台冷冰冰地甩给你一个错误:
VM652:1
Uncaught SyntaxError: Unexpected token '<', "<br />
<b>"... is not valid JSON
at JSON.parse (<anonymous>)
at Object.success (<anonymous>:18:30)
// ... (后面还有一堆调用栈信息)
这个错误信息很直白:它在尝试用 JSON.parse()
解析服务器返回的 response
时,碰到了一个非法的字符 <
。因为 <
通常是 HTML 标签的开头(或者是像错误信息里那样,是 PHP 报错信息的开头 <br />
或 <b>
),而 JSON 数据的开头应该是 {
或 [
。
对应的 PHP 后端代码可能是这样的:
<?php
include "db_conn.php"; // 包含数据库连接文件
// 获取 POST 过来的日期范围
$fromDate = $_POST['fromdate'] ?? null; // 使用空合并运算符增加健壮性
$toDate = $_POST['todate'] ?? null;
// 检查日期是否有效(基本检查)
if (!$fromDate || !$toDate) {
// 可以返回错误信息,但要确保是 JSON 格式或前端能处理的格式
// 这里为了演示,暂时简单退出
// header('HTTP/1.1 400 Bad Request'); // 发送错误状态码
// echo json_encode(['error' => '日期参数缺失']);
exit('日期参数缺失'); // 简单处理,但注意这会触发前端错误
}
// 检查数据库连接
if (!$conn) {
// 连接失败时,记录日志并返回错误信息
// 不要直接 die() 输出 HTML 错误
error_log("数据库连接失败: " . mysqli_connect_error());
// header('HTTP/1.1 500 Internal Server Error');
// echo json_encode(['error' => '数据库连接失败']);
exit('服务器内部错误'); // 同上,需要更优雅的处理
}
// 准备 SQL 语句 (注意:这里有 SQL 注入风险!)
// $sql = "SELECT date, SUM(sales) as total_sales FROM sales WHERE date BETWEEN '$fromDate' AND '$toDate' GROUP BY date ORDER BY date ASC";
// 使用预处理语句防止 SQL 注入
$sql = "SELECT date, SUM(sales) as total_sales FROM sales WHERE date BETWEEN ? AND ? GROUP BY date ORDER BY date ASC";
$stmt = mysqli_prepare($conn, $sql);
// 检查 prepare 是否成功
if (!$stmt) {
error_log("SQL prepare 失败: " . mysqli_error($conn));
exit('查询准备失败');
}
mysqli_stmt_bind_param($stmt, "ss", $fromDate, $toDate); // "ss" 表示两个参数都是字符串类型
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
// 检查查询执行是否成功
if (!$result) {
error_log("SQL execute 失败: " . mysqli_stmt_error($stmt));
exit('查询执行失败');
}
// ===> 问题关键点:这里直接输出了 HTML 表格行!<===
while ($row = mysqli_fetch_assoc($result)) {
$date = htmlspecialchars($row['date']); // 基本的 XSS 防护
$sales = htmlspecialchars($row['total_sales']); // 基本的 XSS 防护
// 本意是直接生成给前端用的 HTML,但前端期望的是 JSON
echo "<tr>
<td>$date</td>
<td>$sales</td>
</tr>";
}
// ===> 问题关键点结束 <===
// 关闭 statement 和连接
mysqli_stmt_close($stmt);
mysqli_close($conn);
?>
用户的问题也很清楚:他希望显示选中日期之间的数据行,但是遇到了这个错误,表格也没显示出来。
揪出元凶:为什么会出现这个错误?
错误的核心原因很简单:前后端数据格式约定不一致 。
- 前端期望(JavaScript): 代码里明确写了
var salesData = JSON.parse(response);
。这行代码的意思是:“嘿,我拿到服务器的响应response
了,它肯定是个 JSON 字符串,快帮我把它解析成 JavaScript 对象或数组吧!” - 后端实际输出(PHP): PHP 代码里
while
循环里的echo "<tr>...</tr>";
实际上是在输出一堆 HTML 字符串片段,而不是一个 JSON 格式的字符串。
当 JSON.parse()
遇到像 <tr>
这种以 <
开头的 HTML 字符串时,它不认识,自然就抛出 "Unexpected token '<'" 的语法错误。
还有一种可能的情况:
- PHP 代码本身在执行过程中出错了(比如数据库连接失败、SQL 语法错误、某个变量未定义等)。很多 PHP 环境默认配置下,会直接把错误信息(通常包含 HTML 标签如
<br />
,<b>
)输出到响应体中。这样一来,即使你原本打算输出 JSON,这些不请自来的 HTML 错误信息也会混入响应,导致JSON.parse()
失败。错误信息"<br /><b>"
就强烈暗示了这种情况。
对症下药:修复错误的几种方法
知道了原因,解决起来就有方向了。主要是让前后端对数据格式达成一致。
方案一:后端(PHP)按约定返回 JSON 数据 (推荐)
这是最规范、最常用的做法。既然前端期望 JSON,那后端就老老实实返回 JSON。
原理:
在 PHP 中,从数据库查询到数据后,不要直接 echo
HTML。而是应该先把数据组织成一个 PHP 数组(通常是包含多个关联数组的数组),然后使用 json_encode()
函数将其转换成标准的 JSON 字符串,最后再 echo
这个 JSON 字符串。
操作步骤与代码:
- 修改 PHP 文件 (
functions/search-sales_function.php
) :
<?php
// --- 数据库连接和参数获取部分保持不变 (建议使用预处理语句,如上文修改后的代码) ---
include "db_conn.php";
header('Content-Type: application/json'); // **重要:** 告诉浏览器返回的是 JSON
$fromDate = $_POST['fromdate'] ?? null;
$toDate = $_POST['todate'] ?? null;
if (!$fromDate || !$toDate) {
http_response_code(400); // 设置 HTTP 状态码
echo json_encode(['error' => '日期参数缺失']); // 返回 JSON 格式的错误信息
exit;
}
if (!$conn) {
error_log("数据库连接失败: " . mysqli_connect_error());
http_response_code(500);
echo json_encode(['error' => '服务器内部错误']);
exit;
}
$sql = "SELECT date, SUM(sales) as total_sales FROM sales WHERE date BETWEEN ? AND ? GROUP BY date ORDER BY date ASC";
$stmt = mysqli_prepare($conn, $sql);
if (!$stmt) {
error_log("SQL prepare 失败: " . mysqli_error($conn));
http_response_code(500);
echo json_encode(['error' => '查询准备失败']);
exit;
}
mysqli_stmt_bind_param($stmt, "ss", $fromDate, $toDate);
if (!mysqli_stmt_execute($stmt)) {
error_log("SQL execute 失败: " . mysqli_stmt_error($stmt));
http_response_code(500);
echo json_encode(['error' => '查询执行失败']);
mysqli_stmt_close($stmt); // 记得关闭
mysqli_close($conn); // 记得关闭
exit;
}
$result = mysqli_stmt_get_result($stmt);
// **修改点:** 创建一个空数组来收集数据
$salesData = [];
// 遍历结果集,将每一行添加到数组中
while ($row = mysqli_fetch_assoc($result)) {
$salesData[] = $row; // 直接添加整行数据,或按需选择字段
}
// 关闭 statement 和连接
mysqli_stmt_close($stmt);
mysqli_close($conn);
// **修改点:** 将整个数组编码为 JSON 字符串并输出
echo json_encode($salesData);
?>
- 前端 JavaScript (可以稍微优化) :
使用方案一后,前端的 JSON.parse()
就可以正常工作了。不过,jQuery 的 $.ajax
有个更方便的选项 dataType: 'json'
,它能自动帮你解析 JSON 响应,省去手动 JSON.parse()
的步骤,还能在响应头不是 application/json
或者内容不是有效 JSON 时,更容易触发 error
回调。
<script>
$('#submit-button').on('click', function(event) { // 使用 .on() 替代 addEventListener,更符合 jQuery 习惯
event.preventDefault();
var fromDate = $('#fromdate').val();
var toDate = $('#todate').val();
$.ajax({
type: "POST",
url: "functions/search-sales_function.php",
data: { fromDate: fromDate, toDate: toDate },
dataType: "json", // **新增/修改:** 告诉 jQuery 期望服务器返回 JSON
success: function (salesData) { // 直接拿到解析好的 JavaScript 对象/数组
// 不需要再 JSON.parse(response) 了
var tableBody = $('#sales-table tbody');
tableBody.empty(); // 清空旧数据
if (salesData.length === 0) {
tableBody.append('<tr><td colspan="2">没有找到该日期范围的数据</td></tr>'); // 用户体验优化
} else {
// 遍历数据并添加到表格
$.each(salesData, function(index, item) { // 使用 $.each 更符合 jQuery 风格
// 在插入 HTML 前,最好对来自服务器的数据进行 HTML 转义,防止 XSS
// 虽然 PHP 中可能已用 htmlspecialchars,但前端多一层防御更好
// 这里简单示例,实际应用中可能需要更可靠的转义库或方法
var safeDate = $('<div>').text(item.date).html();
var safeSales = $('<div>').text(item.total_sales).html();
tableBody.append(`
<tr>
<td>${safeDate}</td>
<td>${safeSales}</td>
</tr>`
);
});
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("AJAX 请求失败:", textStatus, errorThrown);
console.error("服务器响应:", jqXHR.responseText);
// 可以在这里给用户提示,比如弹窗或在页面上显示错误信息
$('#sales-table tbody').empty().append('<tr><td colspan="2">加载数据失败,请稍后再试或联系管理员。</td></tr>');
}
});
});
</script>
安全建议:
- SQL 注入防护: 原始 PHP 代码直接将
$_POST
变量拼接到 SQL 字符串中,这是非常危险的,容易受到 SQL 注入攻击。务必使用预处理语句 (Prepared Statements) (如上 PHP 修改示例中已包含) 或严格的数据过滤和转义。 - XSS 防护: 虽然数据现在通过 JSON 传输,但最终还是要在前端渲染成 HTML。如果数据内容本身包含恶意脚本,直接插入 DOM 可能导致跨站脚本攻击 (XSS)。在前端
append
数据之前,对从服务器获取的数据进行 HTML 转义是个好习惯。上面 JS 示例中用$('<div>').text(value).html()
是一种简单的转义方法,对于复杂场景可能需要更专业的库。在 PHP 端输出 JSON 前对字符串数据使用htmlspecialchars
也是一种做法,但推荐在渲染时处理。 - 错误处理: 不要在生产环境直接向用户暴露详细的 PHP 错误信息 (即关闭
display_errors
)。使用error_log()
记录错误,并返回通用的、用户友好的错误提示(最好也是 JSON 格式,如 PHP 修改示例所示)。
进阶使用技巧:
- HTTP 状态码: 在 PHP 中,对于不同类型的错误(如参数错误、服务器内部错误),使用
http_response_code()
设置合适的 HTTP 状态码 (如 400 Bad Request, 500 Internal Server Error),前端可以在error
回调中根据jqXHR.status
做更精细的处理。 - 统一响应格式: 约定一个统一的 API 响应格式,比如总是返回一个包含
data
(成功时的数据)、success
(布尔值表示成功与否)、message
(提示信息) 或error
(错误信息) 字段的对象。这样前端处理起来更一致。
// 成功示例
{
"success": true,
"data": [
{"date": "2023-10-01", "total_sales": "150.00"},
{"date": "2023-10-02", "total_sales": "230.50"}
],
"message": "查询成功"
}
// 失败示例
{
"success": false,
"error": "数据库查询失败",
"code": "DB_QUERY_ERROR" // 可选的错误代码
}
方案二:前端(JavaScript)直接处理 HTML 片段
如果由于某些原因(比如旧系统、特定需求)后端确实只能返回 HTML 片段,那么就得修改前端代码来适应它。
原理:
告诉 JavaScript,服务器返回的 response
就是一堆可以直接用的 HTML,不需要用 JSON.parse()
解析。直接把这些 HTML 片段塞进页面的目标位置。
操作步骤与代码:
- 保持 PHP 文件不变 (还是那个输出
<tr>...</tr>
的版本)。 - 修改 JavaScript 文件 :
<script>
$('#submit-button').on('click', function(event) {
event.preventDefault();
var fromDate = $('#fromdate').val();
var toDate = $('#todate').val();
$.ajax({
type: "POST",
url: "functions/search-sales_function.php",
data: { fromDate: fromDate, toDate: toDate },
// dataType: "html", // 可以明确指定 dataType 为 html,或者不指定让 jQuery 自动判断
success: function (responseHtml) { // 参数名改为 responseHtml 更清晰
// **修改点:** 删除 JSON.parse()
// var salesData = JSON.parse(response); // 注释掉或删除这行
var tableBody = $('#sales-table tbody');
tableBody.empty(); // 清空旧数据
// **修改点:** 直接将返回的 HTML 添加到 tbody
if (responseHtml.trim() === '') { // 检查返回是否为空
tableBody.append('<tr><td colspan="2">没有找到该日期范围的数据</td></tr>');
} else {
tableBody.append(responseHtml); // 直接追加服务器返回的 HTML
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("AJAX 请求失败:", textStatus, errorThrown);
// 即使期望 HTML,如果服务器返回 500 错误等,responseText 可能包含非预期的 HTML 错误信息
console.error("服务器响应:", jqXHR.responseText);
$('#sales-table tbody').empty().append('<tr><td colspan="2">加载数据失败,请稍后再试或联系管理员。</td></tr>');
}
});
});
</script>
安全建议:
- 严重警告 - XSS 风险: 这种方法的主要缺点是安全性。 因为 HTML 是由后端动态生成的,如果后端在生成 HTML 时没有对从数据库或其他来源获取的数据(比如
$date
,$sales
)进行充分的 HTML 转义 (如使用 PHP 的htmlspecialchars()
函数),那么恶意用户输入的数据就可能包含<script>
标签,当这些 HTML 被append
到前端页面时,恶意脚本就会执行,导致 XSS 攻击。必须确保后端 PHP 代码在echo
之前,对所有动态数据都使用了htmlspecialchars(..., ENT_QUOTES, 'UTF-8')
进行转义。 这是使用此方案的前提条件 。
方案三:排查并修复潜在的 PHP 错误
有时候,你 PHP 代码的 目标 是输出 JSON (使用了 json_encode
),或者你用了方案二期待 HTML,但请求依然失败,并且错误还是指向 <
。这通常意味着在你的 PHP 代码成功输出预期内容之前 ,有 PHP 错误(Notices, Warnings, Fatal errors)被触发了,并且 PHP 的错误报告机制(display_errors
)是开启的,导致这些错误的 HTML 格式信息被打印出来,污染了正常的输出。
原理:
找到并修复 PHP 代码中的错误,或者调整 PHP 的错误报告设置,确保只有预期的数据(JSON 或 HTML 片段)被发送到前端。
操作步骤:
- 检查 PHP 错误日志: 查看服务器的 PHP 错误日志文件。这是定位 PHP 错误的最佳途径,特别是那些在
display_errors
关闭时不会显示在浏览器中的错误。日志文件的位置取决于服务器配置。 - 临时开启错误显示 (仅限开发环境!): 在你的 PHP 脚本开头(
db_conn.php
或者主脚本顶部)临时加入以下代码,以便直接在浏览器中看到错误(千万不要在生产服务器上这么做! ):
<?php
error_reporting(E_ALL); // 显示所有错误
ini_set('display_errors', 1); // 将错误输出到屏幕
// ... 你的其他 PHP 代码 ...
?>
然后再次触发 AJAX 请求,在浏览器的网络(Network)监控标签页查看服务器的原始响应(Raw Response) 。你会看到除了你预期的输出外,前面可能多了 PHP 错误信息。根据错误信息修复 PHP 代码。
3. 常见错误源排查:
* 数据库连接问题: include "db_conn.php";
是否成功?$conn
变量是否真的可用?在尝试使用 $conn
之前检查它。
* SQL 语句错误: 语法是否有错?表名、字段名是否正确?可以直接在数据库管理工具 (如 phpMyAdmin) 中运行你的 SQL 语句试试。
* 变量未定义: 是否使用了未初始化的变量?(PHP Notice,但也可能影响输出)
* $_POST
变量不存在: 访问 $_POST['fromdate']
或 $_POST['todate']
之前,最好检查它们是否存在(isset()
或使用空合并运算符 ??
)。
* 函数调用错误: 比如 mysqli_fetch_assoc()
的参数 $result
是否有效?
4. 修复 PHP 代码: 找到错误后,针对性地修复。比如,确保数据库连接成功,修复 SQL 语句,初始化变量等。
5. 关闭错误显示 (生产环境必须!): 一旦问题解决,务必移除 error_reporting(E_ALL)
和 ini_set('display_errors', 1)
这两行,或者在 php.ini
中配置 display_errors = Off
。生产环境应该依赖日志来跟踪错误。
安全建议:
- 再次强调,绝对不要在生产环境中开启
display_errors
。这会暴露服务器路径、代码逻辑等敏感信息,给攻击者可乘之机。
回顾一下
这个 Unexpected token '<'
的 JSON 解析错误,十有八九是后端返回的不是纯粹的、格式正确的 JSON 字符串。要么是直接输出了 HTML,要么是 PHP 报错掺杂了 HTML 格式的错误信息。
解决的关键在于:
- 明确约定: 前后端统一数据交换格式。推荐使用 JSON。
- 后端按约定输出: 如果约定是 JSON,PHP 就该用
json_encode()
输出 JSON 字符串,并设置Content-Type: application/json
头。 - 前端按约定处理: 如果后端返回 JSON,前端(jQuery AJAX)最好使用
dataType: 'json'
让其自动解析,或者手动JSON.parse()
。如果后端返回 HTML,前端就直接处理 HTML 字符串,但务必警惕 XSS 风险并确保后端转义。 - 排查隐藏错误: 如果确定后端逻辑是输出 JSON,但依然报错,检查是否有 PHP 错误信息污染了输出。利用错误日志和临时的
display_errors
(仅开发环境) 来定位并修复它们。
搞清楚这几点,下次再碰到类似的 AJAX 问题,就能从容应对了。