返回

搞定 jQuery AJAX 报错:Unexpected token '<' 无效 JSON

php

搞定 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);
?>

用户的问题也很清楚:他希望显示选中日期之间的数据行,但是遇到了这个错误,表格也没显示出来。

揪出元凶:为什么会出现这个错误?

错误的核心原因很简单:前后端数据格式约定不一致

  1. 前端期望(JavaScript): 代码里明确写了 var salesData = JSON.parse(response);。这行代码的意思是:“嘿,我拿到服务器的响应 response 了,它肯定是个 JSON 字符串,快帮我把它解析成 JavaScript 对象或数组吧!”
  2. 后端实际输出(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 字符串。

操作步骤与代码:

  1. 修改 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); 
?>
  1. 前端 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 片段塞进页面的目标位置。

操作步骤与代码:

  1. 保持 PHP 文件不变 (还是那个输出 <tr>...</tr> 的版本)。
  2. 修改 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 片段)被发送到前端。

操作步骤:

  1. 检查 PHP 错误日志: 查看服务器的 PHP 错误日志文件。这是定位 PHP 错误的最佳途径,特别是那些在 display_errors 关闭时不会显示在浏览器中的错误。日志文件的位置取决于服务器配置。
  2. 临时开启错误显示 (仅限开发环境!): 在你的 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 格式的错误信息。

解决的关键在于:

  1. 明确约定: 前后端统一数据交换格式。推荐使用 JSON。
  2. 后端按约定输出: 如果约定是 JSON,PHP 就该用 json_encode() 输出 JSON 字符串,并设置 Content-Type: application/json 头。
  3. 前端按约定处理: 如果后端返回 JSON,前端(jQuery AJAX)最好使用 dataType: 'json' 让其自动解析,或者手动 JSON.parse()。如果后端返回 HTML,前端就直接处理 HTML 字符串,但务必警惕 XSS 风险并确保后端转义。
  4. 排查隐藏错误: 如果确定后端逻辑是输出 JSON,但依然报错,检查是否有 PHP 错误信息污染了输出。利用错误日志和临时的 display_errors (仅开发环境) 来定位并修复它们。

搞清楚这几点,下次再碰到类似的 AJAX 问题,就能从容应对了。