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

5 阅读4分钟

🚀 从零手写 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 数据的服务:

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 五种状态

状态含义
0UNSENT已创建,尚未调用 open()
1OPENED已调用 open(),通道已打开
2HEADERS_RECEIVED已收到响应头
3LOADING正在接收响应体
4DONE请求完成,数据已就绪

所以我们只在 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
# 看到列表自动渲染出「过四级」「过六级」

总结

概念一句话理解
AjaxJS 主动发 HTTP 请求,页面不刷新
XHR浏览器提供的原生请求对象,Ajax 的底层实现
JSON.stringify对象 → 字符串,方便网络传输
CORS服务端声明"我允许跨域访问"
readyStateXHR 的 5 种状态,4 = 数据就绪
async/await异步代码写成同步风格,终极方案

Ajax 不是某个库,不是某个 API,而是一种思想——用 JS 发请求、局部更新页面。 理解了这个本质,再看 axiosfetch、甚至 React Query,都是在这个基础上的封装和增强。


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