嘿,朋友!既然你点开了这篇文章,说明你可能正被那个让人头秃的 Network Error 或者 Unexpected token < in JSON at position 0 折磨得快要放弃治疗了。别慌,深呼吸。作为在这个坑里摸爬滚打多年的“老学长”,我太懂这种痛了。今天我不跟你扯那些晦涩难懂的学术定义,咱们就搬个小板凳,泡杯茶,我把 AJAX(Asynchronous JavaScript and XML,虽然现在其实很少用 XML 了)那点事儿,连同跨域的坑、JSON 解析的雷,给你扒得干干净净。
我们要做的,是让前端这个“服务员”和后端的“厨师”顺畅地对话,而且还要确保他们不在不同的城市(跨域)导致信号中断。
一、 为什么我们需要 AJAX?先聊聊“传统”的痛点
在 AJAX 出现之前,网页就像是一本死板的书。你想更新页面上的某个数据?比如点赞数从 10 变成 11。以前怎么做?刷新整个页面。这就好比你为了喝一口水,把整本书撕下来重写一遍,累不累?而且用户体验极差,刚才输入到一半的文字全没了。
AJAX 的核心思想就一个字:“异步”,或者说“局部刷新”。
想象一下,你在写一封长邮件,突然想查个资料。以前你得关掉邮件窗口去浏览器搜,回来还得重新打字。现在呢?你打开一个侧边栏查资料,主窗口里的邮件草稿还在,互不影响。AJAX 就是那个侧边栏,它在后台悄悄去服务器拿数据,拿到后再悄无声息地更新页面的某一块,而用户根本感觉不到页面“闪”了一下。
二、 基础篇:用原生 XMLHttpRequest 握手(虽然有点复古,但必须懂原理)
尽管现在大家多用 fetch 或 axios,但理解底层的 XMLHttpRequest (XHR) 能让你在面对怪异 Bug 时拥有上帝视角。我们来看一段最原始的代码。
function getData() {
// 1. 创建 XHR 对象
var xhr = new XMLHttpRequest();
// 2. 配置请求:GET 方式,请求地址是 /api/data
xhr.open('GET', '/api/data', true); // true 表示异步
// 3. 设置响应头,告诉服务器我们想要 JSON 数据
xhr.setRequestHeader('Content-Type', 'application/json');
// 4. 监听状态变化
xhr.onreadystatechange = function () {
// readyState 为 4 表示请求已完成,status 为 200 表示成功
if (xhr.readyState === 4 && xhr.status === 200) {
try {
// 5. 解析 JSON
var response = JSON.parse(xhr.responseText);
console.log('获取到的数据:', response);
updateUI(response); // 假设有一个函数负责更新界面
} catch (e) {
console.error('JSON 解析失败:', e);
}
}
};
// 6. 发送请求
xhr.send();
}
function updateUI(data) {
document.getElementById('result').innerText = data.message;
}
// 调用函数
getData();
这里有个大坑要注意: onreadystatechange 会触发多次。readyState 从 0 到 4 变化。很多新手在这里没做 status === 200 的判断,导致请求还没发出去或者正在加载中就执行了代码,结果拿到的是空值或者错误。一定要加上双重保险。
三、 现代篇:Fetch API 与 Axios —— 更优雅的交互
现在的项目里,谁还写那么多回调函数呢?我们用 Promise 风格的工具。
1. Fetch API (浏览器原生)
fetch('/api/data')
.then(response => {
// 注意:fetch 只有在网络故障时才 reject,HTTP 错误(如 404, 500)不会自动报错!
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // 返回一个新的 Promise,解析 JSON
})
.then(data => {
console.log('成功获取:', data);
})
.catch(error => {
console.error('出错了:', error);
});
知识点拨: response.json() 是异步的,它返回一个 Promise。很多人忘记 await 或者 .then() 就直接用 data,那是拿不到的。
2. Axios (第三方库,推荐)
Axios 是对 Fetch 的封装,处理 JSON 和错误更人性化。
import axios from 'axios';
axios.get('/api/data')
.then(response => {
// Axios 会自动解析 JSON,response.data 就是解析后的对象
console.log('Axios 获取的数据:', response.data);
})
.catch(error => {
// 自动捕获 HTTP 错误和网络错误
console.error('请求失败:', error.response ? error.response.data : error.message);
});
四、 核心难题:跨域报错 (CORS) —— 为什么浏览器拦住了我?
这是新手最容易懵的地方。你明明写了代码,后端也返回了数据,为什么控制台报红:Access to XMLHttpRequest at 'http://b.com/api' from origin 'http://a.com' has been blocked by CORS policy?
1. 什么是同源策略?
浏览器出于安全考虑,默认只允许当前页面访问同源的资源。
- 协议相同:http vs https
- 域名相同:example.com vs api.example.com (不同!)
- 端口相同:80 vs 8080 (不同!)
只要这三个有一个不同,就是跨域。浏览器认为跨域请求可能不安全(比如 CSRF 攻击),所以直接拦截了响应数据。
2. 解决方案 A:后端配置 CORS 头(最正规做法)
跨域问题是后端需要解决的,前端搞不定(除非你用代理)。后端需要在响应头中添加允许跨域的信息。
Node.js (Express) 示例:
const express = require('express');
const app = express();
// 使用 cors 中间件
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000', // 指定允许跨域的源
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.get('/api/data', (req, res) => {
res.json({ msg: "Hello from backend" });
});
app.listen(3001);
关键 Header 解释:
Access-Control-Allow-Origin: 必须设置!可以是具体域名,也可以是*(允许所有,但不建议用于生产环境,尤其是涉及 Cookie 时)。Access-Control-Allow-Methods: 允许的 HTTP 方法。Access-Control-Allow-Headers: 如果请求头包含自定义字段(如 Token),必须在这里声明。
Python (Flask) 示例:
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 允许所有跨域
@app.route('/api/data')
def get_data():
return jsonify({"msg": "Hello from Python"})
3. 解决方案 B:开发环境代理 (Proxy) —— 前端开发的捷径
如果你在后端还没准备好 CORS 配置,或者你只是想快速调试,可以在前端构建工具(如 Vite, Webpack, Create React App)中配置代理。
Vite (vite.config.js) 配置:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001', // 后端地址
changeOrigin: true, // 修改请求头中的 Host 为目标地址
rewrite: (path) => path.replace(/^\/api/, '') // 可选:去掉 /api 前缀
}
}
}
})
原理: 当你请求 /api/data 时,Vite 的开发服务器会拦截这个请求,然后代替你去请求 http://localhost:3001/api/data。因为是从同源的服务器发出的请求,所以没有跨域问题。
注意:这只在开发环境有效,生产环境部署时,通常需要 Nginx 反向代理来解决。
五、 常见坑点:JSON 解析失败与数据格式陷阱
即使跨域解决了,有时候你还是会看到控制台报错:SyntaxError: Unexpected token < in JSON at position 0。
1. 为什么会出现 <?
JSON 通常以 { 或 [ 开头。如果以 < 开头,说明你收到的根本不是 JSON,而是 HTML。
最常见的原因: 你请求的接口地址错了,或者后端发生了 404⁄500 错误,后端没有返回 JSON 格式的错误提示,而是返回了一个默认的 HTML 错误页面(比如 Nginx 的 404 page 或 Spring Boot 的 Whitelabel Error Page)。
排查步骤:
- 打开浏览器的开发者工具 (F12)。
- 点击 Network (网络) 标签。
- 找到那个失败的请求。
- 查看 Response (响应) 标签页。
- 你会发现里面全是 HTML 代码,而不是 JSON。
修复方法:
- 检查 URL 路径是否正确,是否多了或少了斜杠
/。 - 检查后端服务是否真的启动成功。
- 在后端添加全局异常处理,确保出错时也返回 JSON 格式的
{ "error": "message" }。
2. 后端返回了字符串,而不是对象
有时候后端很“贴心”地把 JSON 对象又转成了字符串给你。
错误示范:
后端返回:"{\"id\": 1, \"name\": \"test\"}" (注意外面的双引号,这是一个字符串)
前端 JSON.parse() 后得到的是一个字符串对象,而不是你可以直接访问属性的对象。
如何避免:
- 在 Chrome Network 面板中,看 Response 的 Content-Type 是否为
application/json。如果是text/plain或text/html,那大概率是格式问题。 - 如果是字符串形式的 JSON,你需要先判断类型:
if (typeof responseText === 'string') {
// 可能是被二次序列化的字符串,尝试再次解析
try {
const data = JSON.parse(responseText);
console.log(data.id);
} catch (e) {
console.log("这甚至不是一个合法的 JSON 字符串");
}
} else {
console.log(responseText.id);
}
3. 中文乱码问题
如果后端返回的 JSON 中包含中文,前端显示为乱码,通常是编码不一致导致的。
- 后端: 确保设置响应头
charset=utf-8。- Java/Spring:
@RequestMapping(produces = "application/json;charset=UTF-8") - Node/Express: 通常默认 UTF-8,但如果用了某些模板引擎或旧版配置,可能需要手动设置。
- Java/Spring:
- 前端: 浏览器通常会根据 Content-Type 自动解码,但如果强制指定了其他编码,就会乱码。检查 Network 面板中该请求的 Headers -> Response Headers -> Content-Type。
六、 给小朋友也能听懂的比喻总结
好了,说了这么多技术细节,我们来做个总结,保证连你家刚上小学的小侄子都能听懂。
AJAX 是什么? 就像你在家(前端页面)看电视,突然想问厨房(后端服务器)妈妈饭好了没。你不用跑进厨房喊一声然后站在门口等,也不用把整个房子拆了重建。你只需要对着对讲机(AJAX 请求)问一句:“妈,饭好了吗?”然后继续看电视。等你妈在对讲机里说“好了”,你再过去吃饭。这就是异步。
跨域是什么? 你家住在 A 小区,厨房在 B 小区。保安(浏览器)很严格,他不允许你随便给 B 小区打电话,怕你有坏心思(安全问题)。
- 解决方法 1(后端 CORS): 你跟 B 小区的保安说:“嘿,A 小区的人是我亲戚,让他们打电话没问题。”保安记在小本本上,下次就放行。
- 解决方法 2(前端 Proxy): 你在 A 小区门卫室装个电话,你打给门卫,门卫帮你转接到 B 小区。因为是你家内部的电话,所以保安不拦你。
JSON 解析报错是什么? 你问妈妈“饭好了吗?”,结果妈妈递给你一张写满字的纸,上面写着
<html><body>404 Not Found</body></html>。你本来想吃米饭(JSON 数据),结果拿到了一张餐巾纸(HTML 代码),当然没法吃了,还会噎住(报错)。所以你要看看妈妈到底给了你什么,是不是走错厨房了。
七、 实战演练:一个完整的前后端对接 Checklist
下次再遇到数据交互问题,别急着改代码,按这个清单过一遍:
- 检查网络: F12 -> Network,看请求发出去了吗?Status Code 是多少?
0: 可能被浏览器拦截(CORS 或网络断开)。200: 成功。看 Response 内容对不对。404: 接口地址错了。500: 后端代码崩了,去后端日志看报错。
- 检查 Headers:
- Request Headers 里有没有带必要的 Token?
- Response Headers 里有没有
Access-Control-Allow-Origin?
- 检查数据类型:
- 后端返回的是 JSON 字符串还是 JS 对象?
- 前端是用
JSON.parse()还是直接用?
- 检查编码:
- 中文乱码?确认 UTF-8。
结语
前端与后端的交互,看似只是几行代码的 fetch 或 axios,背后其实涉及了网络协议、浏览器安全策略、数据序列化等多个层面的知识。
记住,报错不可怕,可怕的是不看 Network 面板。那个红色的报错信息,其实是浏览器在试图保护你,同时也在告诉你哪里出了问题。多调试,多观察,当你第一次成功看到后端数据在前端完美渲染的那一刻,那种成就感,绝对值得你熬过的每一个夜。
加油,未来的全栈工程师!如果有具体的报错截图或代码片段,欢迎随时丢过来,我们一起拆解。
