Ajax 入门:从 JSON 序列化到 XMLHttpRequest

Ajax 入门:从 JSON 序列化到 XMLHttpRequest

这次让我们了解 Web 前端中将浏览器与服务器连接起来做异步通信的技术------Ajax 以及它的最早的实现方式。

前面我们聊过 fetchasync/await,它们都是 Ajax 思想在现代语法下的写法;这一次我们回到更早的实现:XMLHttpRequest

Ajax (Asynchronous JavaScript and XML)的两个核心是:异步 (请求不阻塞页面)+ 局部更新 (拿到数据只刷新页面一部分)。

在 Ajax 出现之前,浏览器和服务端"对话"只能通过表单提交或点链接,每次都刷新整个页面,体验很差。Ajax 让 Web 从"页面跳转时代"进入了"局部刷新时代"。

名字里虽然带 XML,但现代开发中传输格式更多是 JSON(更轻量、解析更方便)。它也不是某个具体的库,而是一种思想 ------XHR、fetch、axios 都是它的具体实现。

Gmail、Google Maps 这类 Web 2.0 产品能火起来,背后做支撑的就是 Ajax。

一场完整的 Ajax 通信,抽象来看就是四步: 序列化 → 发请求 → 后端处理 → 反序列化渲染 。这是 XHR、fetch、axios 都遵循的范式。

在这次我们讲述的 XMLHttpRequest 中的具体实现是:

  1. 序列化(打包)JSON.stringify 把 JS 对象变成 JSON 字符串。
  2. 发请求(发信)XMLHttpRequest 打开一条 HTTP 通道,把请求发出去。
  3. 后端处理(回信) :后端用 Node 的 http 模块接收请求,处理完再 JSON.stringify 一次返回。
  4. 反序列化渲染(拆包)JSON.parse 还原成对象,渲染到页面。

下面就按这个顺序,一个一个拆开看。


第 1 步:序列化(打包)

JSON.stringify:对象怎么变成网络能传的文字

网络传输只能传二进制或文本,但我们在 JS 里操作的都是对象。要让对象"上得了网",就得先把它变成字符串。这就是 JSON.stringify 的核心作用:

将对象序列化为 JSON 字符串,便于网络传输。

javascript 复制代码
const todos = [
    { id: 1, title: '学习 node', completed: false },
    { id: 2, title: '学习 express', completed: false }
];

// 序列化:对象 -> JSON 字符串
const jsonStr = JSON.stringify(todos);
console.log(jsonStr);
// '[{"id":1,"title":"学习 node","completed":false},...]'

JSON.stringify 有三个参数,后两个是可选的:

  • value:要序列化的值。
  • replacer:可以是数组(指定保留哪些属性)或函数(对属性进行过滤/转换)。
  • space:指定缩进空格数,用于格式化输出,优化可读性。
javascript 复制代码
// 只保留 id 和 title
JSON.stringify(todos, ['id', 'title'], 2);

// 用函数过滤掉 completed 为 true 的项
JSON.stringify(todos, (key, value) => {
    if (key === 'completed') return undefined;
    return value;
}, 2);

这里还顺带引出了两个重要概念:

  • 浅拷贝:只拷贝对象的第一层,不拷贝内部嵌套的对象。
  • 深拷贝:递归拷贝所有层级,包括内部对象。

JSON.stringify 配合 JSON.parse 可以实现一种"简易深拷贝",但它会丢失函数、undefinedSymbol 和循环引用,所以生产环境要谨慎使用。


第 2 步:发请求(发信)

为什么请求不能阻塞页面

打包好了,接下来要发出去。但 Ajax 的请求有一个关键特性------异步:请求发出去后,页面不能停下来等结果。

JavaScript 是单线程语言,同一时间只能做一件事。如果网络请求是同步的,那在请求回来之前,页面什么都不能干------按钮点不了、动画卡死、用户体验极差。

JS 的处理方式是:遇到异步任务,先把它放到 Event Loop(事件循环) 里,然后继续执行后面的同步代码。等异步任务的回调时机到了,再从 Event Loop 中取出来执行。

javascript 复制代码
console.log('start');
setTimeout(() => {
    console.log('222');
}, 1000);
console.log('end');
// 输出:start -> end -> 222

依据 Event Loop 生长出的处理异步的方式有好几代:

  1. 回调函数(callback) :最早期的写法,比如 xhr.onreadystatechange
  2. Promise + .then():ES6 引入,把异步任务封装成"承诺"。
  3. async/await:目前最推荐的写法,让异步代码看起来几乎和同步一样。后面讲 fetch 时会看到它的完整用法。

理解了 Event Loop,才能理解后面 XHR 的回调为什么这么写。

XMLHttpRequest:最早的 Ajax API

XMLHttpRequest(简称 XHR)是最早实现 Ajax 的 API。fetch 是它的"后辈",但底层本质一样:赋予 JS 发起 HTTP 请求并获取数据的能力。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax 示例</title>
</head>
<body>
    <ul id="data"></ul>
    <button id="btn">按钮</button>

    <script>
        document.getElementById('btn').addEventListener('click', () => {
            console.log('点击了按钮');
        });

        const xhr = new XMLHttpRequest();
        // 打开一个 HTTP 通道:方法、URL、是否异步
        xhr.open('GET', 'http://localhost:3000/todos', true);

        // 监听状态变化
        xhr.onreadystatechange = function () {
            // readyState 4 表示请求完成,status 200 表示成功
            if (xhr.readyState === 4 && xhr.status === 200) {
                const data = JSON.parse(xhr.responseText);
                document.getElementById('data').innerHTML =
                    data.map(item => `<li>${item.title}</li>`).join('');
            }
        };

        xhr.send();
    </script>
</body>
</html>

XHR 的核心流程是:

  1. new XMLHttpRequest():创建一个 XHR 对象。
  2. xhr.open(method, url, async):配置请求方法、地址和是否异步。
  3. xhr.onreadystatechange:注册状态变化回调。
  4. xhr.send():发送请求。

其中 readyState 表示请求状态:

  • 0:请求未初始化
  • 1open() 已调用
  • 2send() 已调用
  • 3:接收响应中
  • 4:请求完成

onreadystatechange 这个回调要写在 send() 之前,否则可能错过状态变化。

另外,open() 的第三个参数 async 建议设置为 true(异步)。如果设为 false(同步),请求会阻塞主线程,造成页面假死,实际开发中几乎不使用。

一个 XHR 实例通常只对应一次完整的请求-响应周期。如果需要发送带请求体的数据,应该先用 open('POST', ...) 配置 POST 请求,再在 send(body) 中传入序列化后的 body。


第 3 步:后端处理(回信)

请求发出去得有人接。现在我们用 Node.js 内置的 http 模块搭一个最简单的后端服务。

用 http 模块搭一个后端

javascript 复制代码
// 传统项目的模块引入
// node 内置的 http 模块
const http = require('http');

// 创建一个 HTTP 服务器,处于伺服状态
// 通过端口号指定数据通道
http.createServer((req, res) => {
    const todos = [
        { id: 1, title: '学习 node', completed: false },
        { id: 2, title: '学习 express', completed: false }
    ];

    // 设置响应头,声明返回 JSON 且使用 UTF-8 编码
    res.setHeader('Content-Type', 'application/json;charset=utf-8');

    if (req.url === '/') {
        res.end('hello world');
    }

    if (req.url === '/todos') {
        // 解决跨域:允许所有域名访问
        res.setHeader('Access-Control-Allow-Origin', '*');
        // 把对象序列化为 JSON 字符串再返回
        res.end(JSON.stringify(todos));
    }
}).listen(3000, () => {
    console.log('服务启动在 http://localhost:3000');
});

这段代码虽然短,但藏着后端的几个核心概念:

  • 端口listen(3000) 表示服务器监听 3000 端口。IP 地址对应一台服务器,端口号对应服务器上的具体服务进程。
  • HTTP 协议:基于请求-响应模型,服务器被动等待客户端请求。
  • Content-Type :告诉浏览器返回的内容类型,这里用 application/json
  • CORS 跨域Access-Control-Allow-Origin: * 允许前端页面从不同域名访问这个接口。
  • JSON 序列化 :后端返回的必须是字符串,JSON.stringify 把 JS 对象转成 JSON 文本。

后端的本质并不复杂:解析请求、处理逻辑、返回响应http 模块把这三个步骤赤裸裸地展现在我们面前。

CommonJS 模块化与 MVC 设计模式(补充)

在浏览器里,我们可以用 <script src="./a.js"></script> 引入 JS 文件。但 Node 里没有 script 标签,所以必须有自己的模块化方案。

Node 早期默认使用 CommonJS 规范:

  • require 引入模块。
  • module.exports 导出模块。
javascript 复制代码
// a.js
function add(a, b) {
    return a + b;
}
module.exports = { add };

// b.js
const { add } = require('./a.js');
console.log(add(1, 2)); // 3

后来 ES6 推出了更现代的 ESM 规范,用 importexport default。Bun 原生支持的就是这一套,但 Node 默认仍然是 CommonJS。

接下来简单介绍一下MVC 设计模式,它是后端项目常见的分层思想:

  • M(Model):模型层,负责数据和数据库。
  • V(View):视图层,负责输出和展示。
  • C(Controller):控制器层,负责路由和业务逻辑。

虽然现在流行前后端分离,前端自己就是 View,但理解 MVC 能帮助我们看懂后端项目的组织方式。


第 4 步:反序列化渲染(拆包)

数据从后端回来了,但它是 JSON 字符串,JS 不能直接用。得先 JSON.parse 还原成对象,再渲染到页面。

这一步在前面的 XHR 示例里已经出现过:

javascript 复制代码
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        // 反序列化:JSON 字符串 -> JS 对象
        const data = JSON.parse(xhr.responseText);
        // 渲染到页面
        document.getElementById('data').innerHTML =
            data.map(item => `<li>${item.title}</li>`).join('');
    }
};

JSON.parse 的用法很简单:

javascript 复制代码
const jsonStr = '[{"id":1,"title":"学习 node","completed":false}]';
const data = JSON.parse(jsonStr);
console.log(data[0].title); // '学习 node'

需要注意的是,如果 JSON 格式不合法(比如多了逗号、少了引号),JSON.parse 会抛错。生产环境通常要加 try-catch

javascript 复制代码
try {
    const data = JSON.parse(xhr.responseText);
} catch (e) {
    console.error('JSON 解析失败', e);
}

fetch:XHR 的现代替代

虽然本文重点讲 XHR,但现代开发中更常用的是 fetch。它返回一个 Promise,配合 async/await 写起来更清爽:

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

fetch 把 XHR 的四步流程封装得更简洁:res.json() 自动帮你做反序列化,不用手动 JSON.parse

如果没有 Ajax(无论是 XHR 还是 fetch),前端只能通过输入 URL 或点击 <a> 标签来跳转页面,每次交互都会刷新整个页面。Ajax 让 Web 从"页面跳转"进化到了"局部更新",这才是现代 Web 应用的基础。


现在怎么理解 Ajax

本篇文章的知识点串起来其实是一条很清晰的链路:

对象 → JSON 字符串 → HTTP 请求 → 后端服务 → JSON 响应 → 前端渲染

  • JSON.stringify 解决了"对象怎么上网"的问题。
  • http.createServer 解决了"谁来响应请求"的问题。
  • XMLHttpRequest / fetch 解决了"前端怎么主动要数据"的问题。
  • Event Loop 和异步机制解决了"请求不能卡死页面"的问题。
  • CommonJS 和 MVC 解决了"后端代码怎么组织"的问题。

前后端交互本质上围绕着 HTTP 协议数据格式 这两个核心。前端把数据打包成 JSON 发出去,后端解析后处理,再打包成 JSON 发回来------整个互联网就是这么对话的。

Ajax 不是某个具体的库或框架,而是一种思想:让页面在不需要整体刷新的情况下,与服务器交换数据并更新部分视图。理解了这一点,后面学 axios、学 RESTful、学全栈开发,都会顺很多。

相关推荐
林希_Rachel_傻希希3 小时前
react hooks速通笔记
前端
Csvn3 小时前
🚨 组件卸载后还在 setState?一个被你忽视的内存泄漏和报错根源
前端
乘风gg3 小时前
AI GenUI 真正落地时,前端到底要做什么?
前端·ai编程·cursor
铁皮饭盒3 小时前
@kognitivedev/rag, 用js做AI Agent开发
javascript·后端
恋猫de小郭4 小时前
苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力
android·前端·flutter
IT_陈寒4 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
kyriewen16 小时前
别再 console.log 了:5 个 Chrome DevTools 调试技巧,用过就回不去了
前端·javascript·面试
IT_陈寒18 小时前
Python搞不定字符串编码?这破玩意坑我两小时!
前端·人工智能·后端
To_OC18 小时前
LC 1 两数之和:面试第一道必考题,暴力解法直接被面试官 pass
javascript·算法·leetcode