欢迎光临
我们一直在努力

从零讲透 Ajax:XHR、Fetch、JSON 序列化到异步编程全链路

head-_val含义 (2).png

前言

前后端分离的时代,Ajax 是前端工程师每天都要打交道的技术。但很多同学对 Ajax 的理解停留在"调个接口、拿个数据"的表面,对底层发生了什么一知半解。

这篇文章用一个最小的 Node.js 后端 + 原生前端 项目,把 Ajax 全链路串起来讲清楚——从服务端怎么响应请求,到客户端怎么发请求,再到 JS 异步编程的三种写法,以及 JSON 序列化的细节,全部覆盖。

读完你会对"前端请求一个接口"这件事,有从里到外的完整理解。


一、先跑起来:项目全貌

image.png

项目结构极其简单,两个文件:

ajax/
├── backend/
│ ├── index.js # Node.js 后端服务
│ └── package.json
└── frontend/
└── index.html # 前端页面

后端:用 Node.js 内置的 http 模块起一个服务,暴露 /todos 接口,返回一组待办事项的 JSON 数据。

前端:用原生 XMLHttpRequest 发异步请求拿数据,动态渲染到页面上。

我们先看后端代码。


二、后端:用 Node.js 原生 http 模块提供 API

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");
}
if (req.url === '/todos') {
res.setHeader('AccessControlAllowOrigin', '*')
res.setHeader('ContentType', 'application/json; charset=utf8');
res.end(JSON.stringify(todos));
}

}).listen(3000, () => {
console.log(`server is running on 3000 port`);
})

2.1 require vs import——JS 模块化的一小段历史

你可能会好奇为什么用 require 而不是 import。这背后是 JS 模块化演进的一个缩影:

  • 早期前端没有模块化:所有代码写在全局作用域,靠 <script> 标签引入,变量冲突是家常便饭。
  • Node.js 带来了 CommonJS:require + module.exports,让 JS 第一次有了正经的模块系统。
  • ESM(ES Modules)是升级版:import + export default,静态分析更友好,支持 tree-shaking,现在是标准写法。

这里用 require 是因为 package.json 里指定了 "type": "commonjs",保持早期 Node 项目的风格。实际生产中更推荐 ESM。

2.2 http.createServer 做了什么

http 是 Node.js 内置模块,不需要安装。createServer 接收一个回调函数,每个请求到来时都会调用这个函数。

  • req(IncomingMessage):包含请求信息——URL、请求头、请求方法等。
  • res(ServerResponse):用来构造响应——设置响应头、写入响应体、结束响应。

2.3 关键的响应头设置

head-_val含义 (3).png

res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Content-Type', 'application/json; charset=utf-8');

  • Access-Control-Allow-Origin: *:经典 CORS 头。浏览器默认禁止跨域请求,加上这个头告诉浏览器"这个接口允许任何来源的请求"。没有它,前端页面在本地打开时无法调通接口。

  • Content-Type: application/json; charset=utf-8:告诉客户端"我返回的是 JSON 格式,用 UTF-8 编码"。这样客户端才能正确解析。

2.4 JSON.stringify——对象到字符串的桥梁

这是本文第一个重点。网络传输只能传二进制/文本,不能传 JS 对象。JSON.stringify 负责把 JS 对象序列化为 JSON 字符串。

res.end(JSON.stringify(todos));

它的完整签名是:

JSON.stringify(value, replacer?, space?)

三个参数各有讲究:

参数作用常见用法
value 要序列化的对象 必传,没啥好说
replacer 控制哪些属性参与序列化 传 null 原样输出;传数组可以只序列化指定 key;传函数可以自定义转换逻辑
space 缩进空格数 传 2 或 4 让输出格式化,适合调试和团队规范要求可读性的场景

举个简单例子感受 replacer 和 space 的效果:

const obj = { name: "张三", age: 25, password: "123456" };

// replacer 传数组——只要 name 和 age,过滤掉敏感字段
JSON.stringify(obj, ["name", "age"], 2);
// 输出:
// {
// "name": "张三",
// "age": 25
// }

// replacer 传 null,space 传 2——原样序列化 + 格式化
JSON.stringify(obj, null, 2);
// 输出:
// {
// "name": "张三",
// "age": 25,
// "password": "123456"
// }

// 不传 space——压缩输出,适合生产环境减少带宽
JSON.stringify(obj);
// 输出:{"name":"张三","age":25,"password":"123456"}

总结:replacer 做取舍(过滤字段),传 null 原样序列化。space 给缩进让输出可读,开发调试用得多,生产环境一般不传以节省带宽。


三、前端:XMLHttpRequest 发起异步请求

后端跑在 localhost:3000,现在看前端怎么调它:

<ul id="todos"></ul>
<button id="btn">按钮</button>

<script>
console.log('start');

// 注册按钮点击事件
document.getElementById('btn')
.addEventListener('click', () => {
console.log('点击按钮');
})

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/todos', true);

xhr.onreadystatechange = function() {
console.log(xhr.readyState);
if (xhr.status === 200 && xhr.readyState === 4) {
const todos = JSON.parse(xhr.responseText);
document.getElementById('todos').innerHTML =
todos.map(todo => `<li>${todo.title}</li>`).join('');
}
}
xhr.send();

console.log('end')
</script>

3.1 XHR 的生命周期

XHR(XMLHttpRequest)是 Ajax 的鼻祖,整个流程分四步:

  • new XMLHttpRequest():实例化一个 XHR 对象,这是发起 Ajax 请求的载体。
  • xhr.open(method, url, async):打开一个 HTTP 通道。第三个参数 true 表示异步请求——这是 Ajax 的灵魂,如果是同步(false),浏览器会卡死直到响应返回。
  • xhr.onreadystatechange:注册回调函数,监听请求状态变化。
  • xhr.send():真正发送请求。
  • 3.2 readyState 的五种状态

    onreadystatechange 会在请求状态变化时被触发,readyState 有 5 个值:

    readyState含义说明
    0 UNSENT open() 还没调用
    1 OPENED open() 已调用
    2 HEADERS_RECEIVED 收到响应头
    3 LOADING 正在接收响应体
    4 DONE 请求完成

    // 只有 status === 200 且 readyState === 4 时,
    // 才表示请求成功完成,可以安全地处理响应数据
    if (xhr.status === 200 && xhr.readyState === 4) {
    const todos = JSON.parse(xhr.responseText);
    // 渲染数据到页面
    }

    3.3 JSON.parse——字符串回到对象

    后端用 JSON.stringify 序列化,前端用 JSON.parse 反序列化,把 JSON 字符串还原为 JS 对象。这一对方法是前后端数据交换的基石。

    3.4 Fetch——XHR 的现代替代

    代码里还留了一段被注释掉的 fetch 写法:

    fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => {
    console.log(data);
    })

    fetch 是 XHR 的现代化升级:

    • Promise 风格,告别回调地狱
    • 语法更简洁,语义更清晰
    • 但需要注意:res.json() 又返回一个 Promise,需要再 .then() 一次才能拿到数据

    XHR 和 fetch 的底层本质是一样的:JS 主动发起 HTTP 请求,拿到数据后动态更新页面。这就是 Web 2.0 时代的核心——页面不用全部刷新,数据异步加载,用户体验飞跃。


    四、JS 异步编程:从 Callback 到 async/await

    这是本文第二个重点,也是面试高频考点。

    4.1 为什么需要异步

    JS 是单线程语言,同一时间只能做一件事。如果网络请求是同步的,发请求期间整个页面会冻结,用户什么都点不了。

    JS 的解决方案:遇到异步任务(网络请求、定时器、事件监听)时,不等待它完成,而是把它丢到 Event Loop 里,继续往下执行。等异步任务有了结果,再从 Event Loop 里拿出来执行对应的回调。

    看这段代码的执行顺序:

    console.log('start'); // ① 先打印

    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3000/todos', true);
    xhr.onreadystatechange = function() {
    if (xhr.status === 200 && xhr.readyState === 4) {
    console.log('数据回来了'); // ③ 最后打印(等网络请求完成)
    }
    }
    xhr.send();

    console.log('end'); // ② 紧接着打印,不会等 XHR 返回

    输出顺序是:start → end → 数据回来了,而不是直觉上的 start → 数据回来了 → end。这就是异步的力量。

    4.2 三种异步写法的演进

    JS 异步编程经历了三代演进:

    第一代:回调函数(Callback)

    xhr.onreadystatechange = function() {
    if (xhr.status === 200 && xhr.readyState === 4) {
    // 在这里处理数据
    }
    }

    最早的写法,直接把"数据到了之后做什么"写成回调函数。问题也很明显——回调地狱:层层嵌套,代码向右无限延伸,可读性灾难。

    第二代:Promise + then

    fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => {
    console.log(data);
    })

    Promise 把异步任务封装成一个对象,通过 .then() 链式调用,解决了回调地狱。代码从左向右延伸变成从上向下延伸,可读性大幅提升。

    第三代:async/await(最推荐)

    async function loadTodos() {
    const res = await fetch('http://localhost:3000/todos');
    const data = await res.json();
    console.log(data);
    }

    async/await 是最优解:

    • 写法跟同步代码几乎一模一样,没有回调嵌套,没有 .then() 链条
    • 错误处理可以直接用 try/catch,而 Promise 的 .catch() 需要单独写
    • 调试友好,断点可以像同步代码一样逐行停住

    建议:新项目一律用 async/await,老项目逐步迁移。


    五、全链路串联:一张图看懂 Ajax

    把以上所有知识点串起来,一次完整的 Ajax 请求经历了这些步骤:

    [浏览器] [Node.js 服务器]
    | |
    | 1. xhr.open('GET', '/todos', true) |
    | 2. xhr.send() ———— HTTP 请求 ———————→ |
    | |
    | 3. http.createServer 回调触发
    | 4. 根据 req.url 匹配路由
    | 5. JSON.stringify(todos)
    | 6. 设置 CORS + Content-Type 头
    | |
    | 7. onreadystatechange 回调触发 ←——— 响应 ———— |
    | 8. readyState 从 01234 |
    | 9. JSON.parse(xhr.responseText) |
    | 10. 动态渲染 DOM |
    | |
    用户看到最新数据

    整个过程的本质就一句话:JS 单线程通过 Event Loop 机制,用异步非阻塞的方式发起 HTTP 请求,拿到 JSON 数据后动态更新 DOM,实现无刷新页面更新。


    六、总结

    这篇文章覆盖了 Ajax 全链路的关键知识点:

    知识点核心要点
    JSON.stringify / JSON.parse 序列化与反序列化,replacer 过滤字段,space 控制格式
    XHR Ajax 鼻祖,readyState 五种状态,open() + send() + 回调
    Fetch XHR 的现代化替代,Promise 风格,链式调用
    JS 异步编程 单线程 + Event Loop → Callback → Promise → async/await
    Node.js http 模块 createServer,req/res,CORS 头,Content-Type
    CORS Access-Control-Allow-Origin 解决跨域

    从最底层的 Event Loop 到最上层的 async/await,从服务端的 JSON.stringify 到客户端的 JSON.parse,从 Node.js 的 http 模块到浏览器的 XHR——当你把这些全部串起来,"前端请求一个接口"就不再是一个黑盒操作,而是一张清晰的蓝图。


    赞(0)
    未经允许不得转载:171主机测评 » 从零讲透 Ajax:XHR、Fetch、JSON 序列化到异步编程全链路
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址