Ajax 入门:从 JSON 序列化到 XMLHttpRequest
这次让我们了解 Web 前端中将浏览器与服务器连接起来做异步通信的技术------Ajax 以及它的最早的实现方式。
前面我们聊过
fetch和async/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中的具体实现是:
- 序列化(打包) :
JSON.stringify把 JS 对象变成 JSON 字符串。- 发请求(发信) :
XMLHttpRequest打开一条 HTTP 通道,把请求发出去。- 后端处理(回信) :后端用 Node 的
http模块接收请求,处理完再JSON.stringify一次返回。- 反序列化渲染(拆包) :
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 可以实现一种"简易深拷贝",但它会丢失函数、undefined、Symbol 和循环引用,所以生产环境要谨慎使用。
第 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 生长出的处理异步的方式有好几代:
- 回调函数(callback) :最早期的写法,比如
xhr.onreadystatechange。 - Promise +
.then():ES6 引入,把异步任务封装成"承诺"。 - 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 的核心流程是:
new XMLHttpRequest():创建一个 XHR 对象。xhr.open(method, url, async):配置请求方法、地址和是否异步。xhr.onreadystatechange:注册状态变化回调。xhr.send():发送请求。
其中 readyState 表示请求状态:
0:请求未初始化1:open()已调用2:send()已调用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 规范,用 import 和 export 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、学全栈开发,都会顺很多。