如果你带过前端新人,下面这段对话大概率不陌生:
新人:老师,现在 axios 都封装好了,为什么还要学 AJAX?
我:那如果项目里不让你引第三方库,让你自己写请求层,你怎么办?
新人:直接把 axios 引入项目不就行了吗……
这个对话我经历过很多次。很多刚入门前端的人觉得“原生JS手写AJAX请求”是老掉牙的内容,实际上一旦你开始面试、开始排查线上问题、开始读懂 axios 源码,就会明白这块基础反而最值钱。AJAX 不是某个框架的特性,它是浏览器提供的 HTTP 通信能力,是所有前端网络请求的地基。
这篇文章我会从一个实际带人的角度,把原生 XHR 从原理到实战完整拆一遍。内容包括:最简请求怎么写、GET 和 POST 的参数怎么组装、Content-Type 和中文编码的坑、超时中断缓存竞态这些边界场景、以及最后封装一个 Promise 版的 request 函数。没有框架包装,没有语法糖,全部用原生 JS 实现。
如果你是把“会用 axios”当成“会网络请求”的初学者,或者正准备前端面试想补一轮基础,这篇文章应该能帮到你。看完之后你再去用 axios,看到的不再是一个黑盒,而是一层封装里面的设计思路。
1. 为什么框架满地走,新人还是要亲手把 AJAX 写一遍
1.1 你学的不是 API,而是浏览器的 HTTP 能力
AJAX 的全称是 Asynchronous JavaScript And XML,中文叫“异步 JavaScript 和 XML”。名字听起来像一门新技术,但它并不是,它只是描述了一种能力:页面不刷新,就能通过 JavaScript 向服务器发请求、拿响应、更新局部内容。
这套能力在浏览器里的载体,主要就是 XMLHttpRequest 对象,也就是大家常说的 XHR。虽然它名字里带 XML,但现在传输 JSON 才是常态。这个对象从 IE 时代就存在,后来被标准化,一直到今天仍然是浏览器网络请求的核心 API 之一。
新人不理解这一点,就容易把 AJAX 当成语法去背:new XMLHttpRequest()、open、send、onreadystatechange……背完就忘。一旦你真理解了它背后在做什么——你是在页面里通过 JS 发起一次 HTTP 请求,然后等浏览器把服务端的响应交还给你——你就能看懂所有网络请求库的底层逻辑。
1.2 你不用 XHR,不代表 XHR 不重要
很多人会问:我现在写项目都用 axios,为什么还要手写 XHR?
答案很简单:axios 在浏览器端并不是魔法,它内部封装的就是 XHR。不同环境它选不同的适配器,浏览器环境里它的核心请求逻辑就是围绕 XHR 封装出来的。我们常用的 get、post、interceptors、timeout,本质上都是对 XHR 行为的上层加工。
当年我带新人排查过一个上传进度条的问题。上传文件要显示进度,UI 组件库没有直接暴露这个能力,有人翻了一晚上文档找不到配置项,其实就是对 XHR 的 upload.onprogress 事件做监听。你只会在 axios 的 onUploadProgress 里传回调,却不知道它包装的是什么,那一旦封装的库不支持某个底层能力,就会卡住。
手写 XHR 还有一个直接价值:面试。前端面试题里“用原生 JS 实现一个 AJAX 请求”是高频题,很多候选人写得出 axios.get,却写不出一个干净的 XHR 实例。2026 年了,这个题我依然在问,因为它能快速判断一个人是背了框架,还是真的理解请求链路。
1.3 完整请求生命周期:一张图能说明白的事
在进入代码之前,先建立整体认知。一次完整 XHR 请求大致是这样一个流程:
- 创建 XHR 对象
- 调用
open方法,确定请求方式、请求地址、是否异步 - 注册事件回调,处理响应、错误、超时
- 调用
send方法,发送请求 - 浏览器在后台与服务端交换数据
- 响应回来后触发回调,你在回调里读取状态码和响应体
- 根据状态码判断业务成功还是失败,再去做后续处理
这个流程,我会在下文通过代码一层一层拆开。你应该感觉到它和“页面整体刷新”的传统模式完全不同,核心区别就在“异步”两个字上。页面刷新的流程是:用户操作、浏览器重新请求整个 HTML、白屏、再渲染。而 XHR 的流程是:用户操作、JS 发一个请求、页面保持原样、数据回来后只更新需要变化的那部分 DOM。
这也是 AJAX 当年颠覆用户体验的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从最小请求开始:XHR 对象的完整运行链路拆解
2.1 先跑通一个最小可用示例
理论说太多容易飘,直接上一个最简例子。下面的代码会创建一个按钮,点击后向某个接口发请求,然后把返回的文本渲染到页面上,整个过程不刷新页面。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>最小 AJAX 请求</title>
</head>
<body>
<button id="btn">点击加载数据</button>
<pre id="result">还没有数据</pre>
<script>
var btn = document.getElementById('btn');
var result = document.getElementById('result');
btn.addEventListener('click', function () {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
result.textContent = xhr.responseText;
} else {
result.textContent = '请求失败,状态码:' + xhr.status;
}
}
};
xhr.send();
});
</script>
</body>
</html>
把它放到一个本地静态服务里,`/api/user
