从零手写 Ajax:用原生 XHR 搭建前后端交互全流程

🚀 从零手写 Ajax:用原生 XHR 搭建前后端交互全流程

很多人用了无数遍 axiosfetch,却说不清底层的 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 数据的服务:

js 复制代码
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
js 复制代码
// 生产环境:紧凑传输
JSON.stringify(todos)           // '[{"id":"1","title":"过四级"}]'

// 开发调试:格式化输出
JSON.stringify(todos, null, 2)  // 带缩进,方便阅读
② CORS 跨域头
js 复制代码
res.setHeader('Access-Control-Allow-Origin', '*');

浏览器的同源策略 会阻止前端页面请求不同源(域名/端口不同)的接口。这行代码告诉浏览器:"我允许任何来源的请求访问"。* 表示不限制,生产环境应指定具体的域名。

三、前端:用原生 XHR 发起 Ajax 请求

html 复制代码
<!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

js 复制代码
// 更简洁的写法
xhr.onload = function () {
    const todos = JSON.parse(xhr.responseText);
    // ...
};

onload 等价于 readyState === 4 时的回调,代码更简洁。但 onreadystatechange 让你能监听到每一个中间状态,适合做细粒度的进度控制(比如文件上传进度)。

四、从 XHR 到 Fetch:演进之路

scss 复制代码
原始时代        Ajax 时代           现代
<a> 标签    →    XMLHttpRequest  →    fetch()
表单提交     →    jQuery.ajax()  →    axios()
页面跳转     →    局部刷新        →    Promise 驱动

fetch 重写上面的请求,只需要两行:

js 复制代码
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)

js 复制代码
xhr.onreadystatechange = function () {
    // 回调地狱的起点...
};

问题:嵌套多了就是"回调地狱",代码横向膨胀,难以维护。

2️⃣ Promise

js 复制代码
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(✅ 最推荐)

js 复制代码
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

bash 复制代码
# 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 发请求、局部更新页面。 理解了这个本质,再看 axiosfetch、甚至 React Query,都是在这个基础上的封装和增强。


如果这篇文章对你有帮助,点个 👍 收藏一下吧!有问题欢迎评论区讨论 🎉

相关推荐
袋鱼不重1 分钟前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
竹林8185 分钟前
Web3表单签名验证:我用 wagmi 和 ethers 给 DApp 加了一个“免密登录”,踩坑记录全在这了
javascript
用户6990304848758 分钟前
try catch使用场景 处理同步代码错误兼容用的
javascript·uni-app
雪碧聊技术11 分钟前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
Fireworks23 分钟前
深入vue3源码解读 -- 1、响应式的基础概念
前端
程序员黑豆23 分钟前
JDK 下载安装与配置详细教程
java·前端·ai编程
hunterandroid27 分钟前
文件存储:内部存储与外部存储
前端
VidDown35 分钟前
VidDown 工具站:免费、本地优先的开发者工具箱
javascript·编辑器·音视频·视频编解码·视频
NorBugs1 小时前
飞机大战 Low 版 (Made in AI)
前端
angerdream1 小时前
Android手把手编写儿童手机远程监控App之agentweb如何实现全屏
前端