🚀 从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
很多人用了无数遍
axios、fetch,却说不清底层的 Ajax 到底在干什么。今天我们从零开始,用 Node.js + 原生 XMLHttpRequest 搭一个完整的前后端交互 demo,彻底搞懂 Ajax 的本质。
一、Ajax 到底是什么?
Ajax(Asynchronous JavaScript And XML) 的核心只有一句话:
JS 可以主动发起 HTTP 请求,而当前页面不会刷新。
在 Ajax 出现之前,用户每次想要获取新数据,都必须跳转页面——输入 URL、点击 <a> 标签、提交表单。而 Ajax 的出现让页面可以在不刷新的情况下,动态地从服务器拉取数据并更新 DOM。
这就是 Web 2.0 时代繁荣的根基。
二、搭建后端服务
我们用 Node.js 内置的 http 模块,零依赖搭建一个返回 JSON 数据的服务:
const http = require('http');
http.createServer((req, res) => {
const todos = [
{ id: "1", title: "过四级", completed: false },
{ id: "2", title: "过六级", completed: false }
];
// 根路由
if (req.url === "/") {
res.end("hello world");
}
// /todos 路由 —— 返回 JSON
if (req.url === '/todos') {
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许跨域
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.end(JSON.stringify(todos));
}
}).listen(3000, () => {
console.log('server is running at port 3000');
});
关键点解析
① JSON.stringify(value, replace?, space?)
这个方法将 JS 对象序列化为 JSON 字符串,便于网络传输:
| 参数 | 作用 | 示例 |
|---|---|---|
value | 要序列化的对象 | todos |
replace | 过滤/替换函数,null 表示原样序列化 | null |
space | 缩进空格数,提升可读性 | 2 |
// 生产环境:紧凑传输
JSON.stringify(todos) // '[{"id":"1","title":"过四级"}]'
// 开发调试:格式化输出
JSON.stringify(todos, null, 2) // 带缩进,方便阅读
② CORS 跨域头
res.setHeader('Access-Control-Allow-Origin', '*');
浏览器的同源策略会阻止前端页面请求不同源(域名/端口不同)的接口。这行代码告诉浏览器:"我允许任何来源的请求访问"。* 表示不限制,生产环境应指定具体的域名。
三、前端:用原生 XHR 发起 Ajax 请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajax Demo</title>
</head>
<body>
<ul id="todos"></ul>
<script>
// 1. 实例化 XHR 对象
const xhr = new XMLHttpRequest();
// 2. 打开一个 HTTP 通道
// 参数:(method, url, async) —— true 表示异步请求
xhr.open('GET', 'http://localhost:3000/todos', true);
// 3. 监听状态变化(回调函数)
xhr.onreadystatechange = function () {
console.log('readyState:', xhr.readyState);
if (xhr.readyState === 4) {
// 4. 解析 JSON 并渲染到页面
const todos = JSON.parse(xhr.responseText);
document.getElementById('todos').innerHTML =
todos.map(todo => `<li>${todo.title}</li>`).join('');
}
};
// 5. 发送请求
xhr.send();
</script>
</body>
</html>
XHR 的 readyState 五种状态
| 值 | 状态 | 含义 |
|---|---|---|
| 0 | UNSENT | 已创建,尚未调用 open() |
| 1 | OPENED | 已调用 open(),通道已打开 |
| 2 | HEADERS_RECEIVED | 已收到响应头 |
| 3 | LOADING | 正在接收响应体 |
| 4 | DONE | 请求完成,数据已就绪 ✅ |
所以我们只在 readyState === 4 时才去处理数据。
为什么不直接监听 onload?
// 更简洁的写法
xhr.onload = function () {
const todos = JSON.parse(xhr.responseText);
// ...
};
onload 等价于 readyState === 4 时的回调,代码更简洁。但 onreadystatechange 让你能监听到每一个中间状态,适合做细粒度的进度控制(比如文件上传进度)。
四、从 XHR 到 Fetch:演进之路
原始时代 Ajax 时代 现代
<a> 标签 → XMLHttpRequest → fetch()
表单提交 → jQuery.ajax() → axios()
页面跳转 → 局部刷新 → Promise 驱动
用 fetch 重写上面的请求,只需要两行:
fetch('http://localhost:3000/todos')
.then(res => res.json())
.then(todos => {
document.getElementById('todos').innerHTML =
todos.map(todo => `<li>${todo.title}</li>`).join('');
});
但 fetch 的底层仍然是 Ajax,只不过浏览器帮我们封装好了。
五、JS 异步处理的三个阶段
Ajax 请求天然是异步的——你发完请求不能傻等,得让 JS 继续干别的事。JS 的异步处理经历了三个阶段:
1️⃣ 回调函数(Callback)
xhr.onreadystatechange = function () {
// 回调地狱的起点...
};
问题:嵌套多了就是"回调地狱",代码横向膨胀,难以维护。
2️⃣ Promise
function fetchTodos() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/todos', true);
xhr.onload = () => resolve(JSON.parse(xhr.responseText));
xhr.onerror = () => reject(new Error('请求失败'));
xhr.send();
});
}
fetchTodos()
.then(data => console.log(data))
.catch(err => console.error(err));
进步:用链式调用解决了回调地狱,错误处理也更集中。
3️⃣ async / await(✅ 最推荐)
async function loadTodos() {
try {
const res = await fetch('http://localhost:3000/todos');
const todos = await res.json();
document.getElementById('todos').innerHTML =
todos.map(todo => `<li>${todo.title}</li>`).join('');
} catch (err) {
console.error('加载失败:', err);
}
}
终极方案:看起来像同步代码,写起来直觉,读起来清晰。底层还是 Promise,但语法糖让异步代码的可读性质变。
对比总结
| 方式 | 可读性 | 错误处理 | 链式调用 | 推荐度 |
|---|---|---|---|---|
| Callback | ⭐⭐ | 分散 | 困难 | ❌ |
| Promise | ⭐⭐⭐ | .catch() 集中 | ✅ | ✅ |
| async/await | ⭐⭐⭐⭐⭐ | try/catch | 天然支持 | ✅✅✅ |
六、运行 Demo
# 1. 进入后端目录
cd backend
# 2. 启动服务
node index.js
# server is running at port 3000
# 3. 浏览器打开 frontend/index.html
# 看到列表自动渲染出「过四级」「过六级」
总结
| 概念 | 一句话理解 |
|---|---|
| Ajax | JS 主动发 HTTP 请求,页面不刷新 |
| XHR | 浏览器提供的原生请求对象,Ajax 的底层实现 |
| JSON.stringify | 对象 → 字符串,方便网络传输 |
| CORS | 服务端声明"我允许跨域访问" |
| readyState | XHR 的 5 种状态,4 = 数据就绪 |
| async/await | 异步代码写成同步风格,终极方案 |
Ajax 不是某个库,不是某个 API,而是一种思想——用 JS 发请求、局部更新页面。 理解了这个本质,再看
axios、fetch、甚至React Query,都是在这个基础上的封装和增强。
如果这篇文章对你有帮助,点个 👍 收藏一下吧!有问题欢迎评论区讨论 🎉