提到 AJAX,很多前端新同学脑子里冒出来的还是 jQuery 里的 $.ajax({...}) 或者 axios 的 axios.get('/api')。我自己带前端团队这几年,发现一个规律:能用库不奇怪,真正拉开差距的是在不借助框架的情况下,能不能用原生 JS 把一个请求从零写出来、讲明白。尤其面试的时候,一道“你来手写一个 AJAX 请求”基本能筛掉一半人,不是不会写,而是不知道底层发生了什么,出了错只会怀疑后端。
这篇文章就把原生 JS 手写 AJAX 这件事拆开揉碎讲一遍。从底层原理说起,到 XMLHttpRequest 的每个核心细节,再带着你封装一个类似 axios 的轻量工具库,最后用真实接口跑一遍完整流程,连带新人常见的坑和排查套路一起聊。不管你是准备面试,还是想彻底搞懂网络请求这层逻辑,这篇应该都能帮到你。
1. 先把 AJAX 的原理聊透:它到底做了什么
1.1 没有 AJAX 之前,网页是怎么“刷新”的
想理解 AJAX 的价值,得先退回没有它的年代。早期 Web 应用,用户每点一个按钮、提交一次表单,浏览器都会把整个页面打包发回服务器,服务器返回一份全新的 HTML,浏览器再把整个页面重新渲染一遍。也就是你每次操作,都会看到页面刷地一下白屏,然后再弹出来。
这种方式不是不能用,但体验很差。你只是给文章点了个赞,整个页面都要重来,响应慢、流量浪费,交互感也基本为零。AJAX 的出现就是为了解决这个问题:它的核心思想是在不刷新整个页面的前提下,由浏览器在后台悄悄发出 HTTP 请求,拿到响应的数据之后,用 JS 局部更新页面。
简单说,AJAX = 异步 JavaScript + XML,虽然名字里带 XML,但今天绝大多数场景传的都是 JSON,所以你在代码里看到 .responseText 出场率远高于 .responseXML,这个是后话,先记住这个技术解决的核心问题:局部更新、无刷新交互。
1.2 你发出去一个请求,浏览器背后做了什么
我一直觉得 AJAX 特别像你去餐厅点菜。你坐在座位上(页面),叫来服务员(XMLHttpRequest 对象),服务员帮你拿着订单(请求报文)跑到后厨(服务器),后厨做好菜(响应数据)再让服务员端回来,这期间你完全不用站到后厨门口干等,还可以继续刷手机聊天,这就是“异步”。
具体到浏览器层面,一次 AJAX 请求从发出到结束大致会经过这么几步:
- 在 JS 里创建 XMLHttpRequest 对象,相当于“叫了个服务员”。
- 调用
open()方法初始化请求,这一步只是配置,告诉浏览器“我准备用 GET 方法去拿这个地址的数据”,请求并没有真正发出去。 - 调用
send()方法,浏览器这时候才真正开始发起网络请求,把你的请求头发往服务器。 - 服务器处理完业务逻辑,返回响应报文,包含状态行(如 200)、响应头、响应体。
- 浏览器把响应内容交给 XHR 对象,触发相应的事件(比如
readystatechange),让你能在 JS 里读取数据并更新页面。
这里有个很关键的认知:AJAX 本身就是浏览器提供的能力,XMLHttpRequest 就是浏览器开给 JS 的一扇网络窗口。所以你别觉得这是什么神奇魔法,它就是一套精心设计的 API,把底层的 HTTP 通信细节包装成了对象和方法,你只需要按规则调用就能拿到数据。
1.3 同步和异步,区别大到你想象不到
XHR 的 open() 方法第二个参数是布尔值,true 表示异步,false 表示同步。你八成听过“不要用同步请求”,但你知道为什么吗?
我用一个例子说明。假设页面上有一个按钮,点击后发起同步请求,在请求返回之前,整个浏览器的主线程会原地卡死,用户点哪里都没反应,页面白花花一片,加载慢的时候甚至看起来像死机了。因为 JS 是单线程的语言,同步请求会阻塞事件循环,后续的 UI 渲染、事件处理全部排队等待。而异步请求则把网络通信放到了后台线程去处理,等数据返回了再去执行回调函数,页面全程可交互,体验天差地别。
所以我可以明确告诉你:async: false 除了极少数特殊情况(比如某些测试场景),一定要避免。现在很多浏览器已经不允许在主线程使用同步 XHR 了,就是因为这个设计太影响用户体验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零写一个能用的原生 AJAX:XMLHttpRequest 核心配置
2.1 一个最小可用示例:GET 请求怎么发
先不整封装那些花活,我们用最原始的方式发一个 GET 请求。假设我们想请求一个公共测试接口 https://jsonplaceholder.typicode.com/posts/1,拿一篇文章的数据。
javascript复制// Step 1: 创建 XHR 对象
var xhr = new XMLHttpRequest();
// Step 2: 初始化一个异步 GET 请求
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
// Step 3: 发送请求
xhr.send();
// Step 4: 监听请求状态变化
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
console.log('请求成功拿到的数据:', data);
} else {
console.error('请求出错,HTTP 状态码:', xhr.status);
}
}
};
这个示例只有十几行,但它把 XHR 最基本的使用方式全部串起来了:创建对象、配置请求、发送、监听、处理响应。我第一次写的时候有一件事非常困惑:open() 和 send() 之间到底做了什么,为什么不直接 send() 传 URL?后来才理解到,open() 是预配置阶段,好比你去快递站填快递单,填好发件人收件人(这对应 URL 和方法),然后才真正把包裹交出去(对应 send()),两个步骤分开是为了让你有机会在发送前设置请求头、超时时间等参数,这个下面会展开讲。
2.2 readyState 是怎么一回事:五个状态逐一理解
readystatechange 事件触发非常频繁,原因是 XHR 对象有一个叫 readyState 的属性,它会随着请求生命周期不断变化,每变化一次就触发一次事件。
- 0(UNSENT):XHR 对象刚被
new出来,open()还没调用。 - 1(OPENED):
open()已经调用了,连接还没建立。 - 2(HEADERS_RECEIVED):
send()已经调用,响应头已经接收。 - 3(LOADING):响应体正在下载中,
responseText可能已经有部分数据。 - 4(DONE):响应体下载完成,可以安全读取数据了。
新手最容易踩的坑就是在 readyState === 3 的时候去读 responseText,这个时候数据是不完整的,解析 JSON 会报错。只有当状态变成 4,才代表整个通信流程彻底走完了。
关于 HTTP 的 status(如 200、404、500),它代表的是服务器的最终返回状态,跟 readyState 完全是两码事。一个表示传输到了哪一步,一个表示服务器处理得怎么样,这俩一定要混清楚。我给你一个最直观的判断逻辑:readyState === 4 表示“响应拿到了”,status === 200 表示“服务器说这次处理成功了”,缺一个都不能算作正常请求。
2.3 处理响应数据:JSON 解析和异常处理的完整配合
拿到响应之后最常做的事就是 JSON.parse(xhr.responseText),把后端返回的 JSON 字符串变成 JS 对象。但这一步其实很容易“翻车”,如果后端返回的不是标准 JSON,或者中途网络中断导致响应体不完整,解析就会直接抛异常,卡掉整个脚本。
所以我建议你处理响应的时候带上 try-catch,别嫌麻烦,这是生产环境的标配:
javascript复制xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
var data = JSON.parse(xhr.responseText);
// 业务处理...
} catch (e) {
console.error('响应数据不是合法 JSON:', xhr.responseText);
}
} else {
console.error('请求失败,状态码:' + xhr.status, xhr.statusText);
}
}
};
顺便说个细节:判断成功不一定非要用 === 200,HTTP 的 2xx 范围内的状态码(201 Created、204 No Content 等)都属于成功。如果你写死了 200,遇到后端某些接口返回 201 就判成失败,就会莫名其妙多出一些 bug。区间判断会稳妥很多。
2.4 POST 请求:请求体的几种编码格式
GET 请求通常不传请求体,数据拼在 URL 上。但 POST 不一样,它的核心在于请求体的编码格式。几乎所有新人都会在这个环节遇到问题:明明数据发给后端了,后端却说接不到参数。
问题大概率出在请求头 Content-Type 和请求体的格式没配对。常见的有两种情况:
第一种,表单格式 application/x-www-form-urlencoded。这是 HTML 表单的默认编码方式,请求体长这样:name=zhangsan&age=18。如果你用 Java 的 Spring Boot 且后端用 @RequestParam 接收,就用这种。
第二种,JSON 格式 application/json。开发中用的最多,请求体是一个 JSON 字符串:{"name":"zhangsan","age":18}。Spring Boot 里对应的是 @RequestBody。
下面这段代码是标准的 JSON 格式 POST:
javascript复制var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts', true);
xhr.setRequestHeader('Content-Type', 'application/json');
var payload = JSON.stringify({
title: '学习原生 AJAX',
body: '这是请求体的内容',
userId: 1
});
xhr.send(payload);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
console.log('服务器返回:', xhr.responseText);
}
};
注意到 setRequestHeader 这行了吗?它必须在 open() 之后、send() 之前调用,因为只有 open() 之后连接才算建立,请求头才有地方挂载。你如果把它放在 open() 前面,浏览器会直接忽略。
3. 进阶实操:把你的 AJAX 封装成一个小工具库
3.1 为什么要封装:裸写的代码痛点太多
直接使用 XHR 最大的问题是心智负担重。每次请求你都要写 new XMLHttpRequest()、open、send、监听事件、判断状态、解析响应……如果项目里有七八个接口,同样的代码要复制七八遍,而且回调嵌套一多,代码就像个倒金字塔,阅读起来非常痛苦。
更关键的是,用裸 XHR 处理“请求参数序列化”这种通用逻辑时,你需要每个地方都自己拼 URL 或请求体,很容易出现编码遗漏。这些问题在真实项目里几乎必现,所以封装一个统一的工具函数是每个前端迟早要做的事。这里我带你一步步封装一个轻量版,足够理解 axios 这类库的核心设计思路。
3.2 先解决最核心的问题:参数序列化
参数序列化,说白了就是把 JS 对象转成 HTTP 请求能传输的字符串。GET 请求要拼到 URL 里,POST 请求要放到请求体内,两者格式还不一样,所以这也是封装工具库里的第一个核心模块。
GET 请求参数:
javascript复制function serializeParams(params) {
if (!params) return '';
return Object.keys(params)
.map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
})
.join('&');
}
// 使用示例
var queryString = serializeParams({ q: '前端开发', page: 2 });
// 结果: q=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&page=2
这里有一个特别重要的细节一定要养成习惯:参数值必须做 encodeURIComponent 编码。为什么?因为 URL 里有很多字符是有特殊意义的,比如空格、中文、&、=。如果你直接把中文拼到 URL 上,服务器可能解码失败,甚至把请求路径搞乱。只有经过编码,浏览器和服务器才能正确识别这些特殊字符。我见过很多线上故障,排查到头都是因为这里少编码了一次。
POST 请求参数要根据 Content-Type 来做不同的序列化方法。如果是 JSON,直接 JSON.stringify 就行;如果是表单格式,用上面同样的 serializeParams 方法。
3.3 基于 Promise 封装:告别回调地狱
接下来我来写一个完整的封装,把参数序列化、请求头设置、超时控制、Promise 化这些统统合进去。
javascript复制function ajax(options) {
return new Promise(function (resolve, reject) {
var method = (options.method || 'GET').toUpperCase();
var url = options.url;
var data = options.data || null;
var timeout = options.timeout || 10000;
var contentType = options.contentType || 'application/json';
if (!url) {
reject(new Error('请求地址不能为空'));
return;
}
var xhr = new XMLHttpRequest();
// 超时处理:超过指定时间直接判定失败
xhr.timeout = timeout;
xhr.ontimeout = function () {
reject(new Error('请求超时,请稍后重试'));
};
// 网络异常处理,比如断网
xhr.onerror = function () {
reject(new Error('网络请求错误'));
};
// 处理 GET 请求参数,追加到 URL 后
if (method === 'GET' && data) {
var queryString = serializeParams(data);
url += (url.indexOf('?') === -1 ? '?' : '&') + queryString;
data = null;
}
xhr.open(method, url, true);
// POST / PUT 等需要请求体的请求,根据类型设置 Content-Type
if (method !== 'GET' && data) {
if (contentType === 'application/json') {
xhr.setRequestHeader('Content-Type', 'application/json');
data = typeof data === 'string' ? data : JSON.stringify(data);
} else if (contentType === 'application/x-www-form-urlencoded') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
data = serializeParams(data);
}
}
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status >= 200 && xhr.status < 300) {
// 尝试解析 JSON,解析失败就返回原始字符串
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error('请求失败,HTTP 状态码:' + xhr.status));
}
};
xhr.send(data);
});
}
这个封装虽然简单,但已经把 axios 的核心模式囊括进来了。有一说一,真实项目里的拦截器、取消请求、适配器这些概念,底层也都是建立在 Promise 和 XHR 之上的,只要你理解了这个最小实现,看 axios 源码会轻松非常多。
使用的时候就很舒服了:
javascript复制ajax({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/posts',
data: { userId: 1 }
})
.then(function (posts) {
console.log('拿到文章列表:', posts);
})
.catch(function (error) {
console.error('请求出错:', error.message);
});
3.4 实用功能补全:超时、取消请求、上传进度
一个生产级的请求库,光有发送和接收还远远不够,还得考虑用户体验和性能。这里重点说三个常用功能。
超时控制我在上面封装里已经加上了,核心就是 xhr.timeout 和 ontimeout 事件。这个对提升用户体验很关键,没有超时控制的话,如果服务器一直不返回,那个转圈动画就会一直转,用户也摸不清到底是卡了还是在加载。设置一个合理的超时时间(比如 10 秒),到点主动失败并给出提示,体验会明显好很多。
取消请求对应的方法是 xhr.abort()。这个在场景上非常实用,比如一个搜索框,用户敲一个字发一次请求,前面的请求还没回来后面的又发出去了,这时候就需要把上一次的请求 abort() 掉,避免旧响应覆盖新响应,这种问题在网络上叫“竞态”。
上传进度是很多新人不知道的原生能力。XHR 的 upload 属性上挂载了 progress 事件,可以实时拿到上传百分比:
javascript复制xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.round((event.loaded / event.total) * 100);
console.log('上传进度:' + percent + '%');
// 这里可以更新页面上的进度条
}
};
这个能力在实现文件上传、图片上传功能时非常有用,不用额外引入插件,浏览器原生就能搞定。
4. 实战演练:用封装好的库请求真实接口
4.1 跑通一个 GET 请求并渲染页面
光说不练没有意义,我们拿一个真实可用的公共测试接口(JSONPlaceholder,一个专门给开发者练习 HTTP 请求的免费接口站点)来跑一遍完整流程。第一步,请求文章列表并渲染到页面上。
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原生 AJAX 实战示例</title>
</head>
<body>
<ul id="postList"></ul>
<script>
function serializeParams(params) {
if (!params) return '';
return Object.keys(params)
.map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
})
.join('&');
}
// 这里使用上面封装好的 ajax 函数
function ajax(options) {
return new Promise(function (resolve, reject) {
var method = (options.method || 'GET').toUpperCase();
var url = options.url;
var data = options.data || null;
var contentType = options.contentType || 'application/json';
var xhr = new XMLHttpRequest();
if (method === 'GET' && data) {
url += (url.indexOf('?') === -1 ? '?' : '&') + serializeParams(data);
data = null;
}
xhr.open(method, url, true);
if (method !== 'GET' && data) {
xhr.setRequestHeader('Content-Type', contentType);
if (contentType === 'application/json') {
data = JSON.stringify(data);
} else if (contentType === 'application/x-www-form-urlencoded') {
data = serializeParams(data);
}
}
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error('HTTP ' + xhr.status));
}
}
};
xhr.send(data);
});
}
// 发起请求并渲染
ajax({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/posts'
})
.then(function (posts) {
var list = document.getElementById('postList');
posts.slice(0, 5).forEach(function (post) {
var li = document.createElement('li');
li.textContent = post.id + '. ' + post.title;
list.appendChild(li);
});
})
.catch(function (error) {
console.error('拉取列表失败:', error.message);
});
</script>
</body>
</html>
你把它保存成 HTML 文件直接双击打开,应该就能看到文章标题渲染在页面上了。这里可以直观感受到无刷新交互的体验:整个页面没有跳转,只有列表区被动态更新了。
4.2 POST 提交数据和踩坑提醒
接着我们模拟创建一个新文章。这一步的重点在于观察请求头和数据格式的配合:
javascript复制ajax({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/posts',
contentType: 'application/json',
data: {
title: '原生 AJAX 实战学习',
body: '这篇文章是前端新人写的',
userId: 1
}
})
.then(function (response) {
console.log('创建成功,服务器分配的新 ID:', response.id);
})
.catch(function (error) {
console.error('创建失败:', error.message);
});
如果你在浏览器控制台里看到了 响应 id: 101,就说明 POST 请求成功跑通了。这个测试接口比较友好,无论你发什么都会返回一个模拟的新 ID。
此时有一个非常典型的坑想拿出来提醒一下:POST 请求里,如果 Content-Type 写成 application/x-www-form-urlencoded,但发送的 body 是 JSON 字符串,后端用 @RequestBody 接,十有八九会报 400 或者直接解析不到参数。反之亦然,你发的是表单编码格式,后端用 @RequestParam 接倒是没问题,但如果你用了 @RequestBody,同样接不到。这是后端接口联调时最常见的问题来源,每次联调前先看清楚后端接口的注解和期望的请求格式,比什么都重要。
4.3 处理真实场景中的请求头:携带 Token 和自定义头
实际项目里,几乎所有需要登录态的接口都要求你在请求头里带上 Token(通常是一个由后端签发的身份凭证字符串)。实战代码里可以这样加:
javascript复制xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
X-Requested-With 这个头在不少后端框架里会被用来判断请求是不是 AJAX 请求。有些后端对非 AJAX 请求会有不同的处理策略,加上这个头可以规避一部分拦截校验问题。
当然,这里我要提一句:把 Token 安全地存储和携带其实是个安全相关的课题,实际项目中还需要考虑过期续期、防止 XSS 窃取等一系列问题,这些就不展开说了,但新人至少要知道“请求头可以塞自定义内容”这个能力,它也是你日常调试接口时的重要抓手。
5. 新人最容易踩的坑:问题排查思路和面试要点
5.1 响应状态码速查表
实际开发中,见到状态码第一反应应该是判断请求是不是真的像表面那样“坏掉了”。我把常见的状态码整理成一张表,方便你排查问题:
| 状态码 | 含义 | 常见原因 | 排查方向 |
|---|---|---|---|
| 200 | 请求成功 | 正常 | 无需处理 |
| 301 | 永久重定向 | 接口地址变更、HTTP 跳 HTTPS | 检查请求 URL 是否需要更新 |
| 304 | 未修改走缓存 | 浏览器直接使用缓存 | 不是错误,无需处理 |
| 400 | 请求参数错误 | 参数格式、请求体编码不对 | 检查 Content-Type 和发送的数据格式 |
| 401 | 未认证 | 没带 Token 或 Token 失效 | 检查请求头 Authorization |
| 403 | 无权限 | 登录了但没权限访问该资源 | 检查账号权限或后端配置 |
| 404 | 接口不存在 | URL 路径写错 | 检查接口地址和后端路由 |
| 500 | 服务器内部错误 | 后端代码跑异常 | 抓后端日志,请求本身通常没问题 |
| 502 | 网关错误 | 后端服务挂了或代理配置错误 | 检查服务是否启动、负载均衡是否正常 |
我遇到过一个印象深刻的案例:前端请求一个列表接口,返回 500,后端说接口单独测试没问题,前端也看了 URL 没写错,排查半天发现是请求头里带了某个特殊字符导致后端解析崩溃了。所以说,状态码只是排查入口,真正的原因往往藏在请求头、参数体这类细节里。
5.2 三大高频问题:乱码、缓存、参数接收不到
乱码问题是 AJAX 新手最早遭遇的现实打击之一。GET 请求里的中文参数乱码,多半是 URL 编码没做或者做错了,解决办法就是前面说的 encodeURIComponent。响应内容乱码,通常是服务器返回的内容编码和页面声明不一致。正确的做法是:页面里 <meta charset="UTF-8"> 声明一遍,同时在服务器端响应头也设置好 Content-Type: application/json; charset=utf-8,两边都对齐了基本就没问题。如果后端拿不到你发的中文,先看看是不是请求体没有经过 encodeURIComponent 编码,或者 JSON 字符串里的中文没有被正确转义。
缓存问题常见于 GET 请求。浏览器为了优化速度,对 GET 请求的响应会做缓存。当你修改了后端数据,重新请求同一个 URL,可能拿到的还是旧数据。调试时怎么规避?最简单的办法是在请求 URL 后面加一个不重复的参数,比如时间戳或者随机数:https://api.example.com/list?timestamp=1690000000000。但因为每次都要改造 URL 比较麻烦,我建议你在封装工具时内置这个逻辑,就一步到位了。
参数接收不到的问题其实在 4.2 里已经重点讲过了,核心永远是对齐 Content-Type。我再补一个容易被忽略的场景:表单类型的数据里如果某个字段值是数组,直接调用 toString() 会变成逗号拼接的字符串,很多后端不认。这种时候最好把数组用 JSON 序列化之后再传,或者和后端约定一个分隔符。总而言之,任何时间出现“后端拿不到参数”的报错,先抓请求报文看格式,别急着怀疑后端代码。
5.3 跨域问题是怎么回事
跨域是新手必遇且最容易在网上搜到毒方案的难题。我先说清楚本质:浏览器的同源策略是一种安全机制,它默认禁止页面脚本读取不同源(协议、域名、端口任何一个不同)的接口数据。这个限制是浏览器主动施加的,不是服务器不允许你访问,是浏览器不允许你的 JS 代码读取跨域响应。
正确的解决思路主要有三种:
第一种,后端在响应头里配置 CORS。比如后端在接口响应头里设置 Access-Control-Allow-Origin: https://你的域名,浏览器看到这个头且校验通过,就不会拦截了。这是最标准、最推荐的做法。
第二种,开发环境下使用代理服务。前端把请求发给本地代理服务器,再由代理转发到真正的后端。生产环境通过 Nginx 反代同源转发,效果一模一样。
第三种,JSONP,这是特定历史条件下的一种“取巧”方案,利用 <script> 标签不受同源策略约束的特性来加载数据。它只支持 GET 请求,且需要后端配合返回特定格式的 JS,现在用得不多了,理解原理即可。
这里我要特别强调一句:跨域问题必须通过正规的 CORS 配置或服务端代理来解决,永远不要试图绕过浏览器的安全限制。规范的做法既安全又可靠,绕来绕去只会埋下更多坑。
5.4 面试高频题:AJAX 相关问题清单
我把这两年面试前端候选人时反复会问到的 AJAX 相关问题整理了一下,大家可以拿来自测:
- 什么是 AJAX,它解决了什么问题?——核心考点:无刷新交互、异步通信。
XMLHttpRequest的readyState有哪几种状态,分别代表什么?——五种状态,能完整说出来并解释 3 和 4 的区别。- GET 和 POST 有什么区别,AJAX 中怎么选择?——功能语义、参数传递位置、缓存策略、幂等性几个维度。
Content-Type有哪些常见值,后端分别怎么接收?——application/json对应@RequestBody,application/x-www-form-urlencoded对应@RequestParam。- 为什么异步请求不会阻塞页面,同步请求会?——JS 单线程 + 事件循环机制。
- 如何处理请求超时和网络异常?——
timeout+ontimeout+onerror。 - 什么是同源策略,怎么解决跨域?——CORS、代理、JSONP 三种方式,能说清原理。
- 如何用 Promise 封装一个 AJAX 请求?——这就是本文第 3 章的内容。
- 前端上传大文件时怎么显示进度?——
xhr.upload.onprogress。 - 如果连续点击按钮导致并发请求,怎么处理最后一次点击的响应?——
abort()取消旧请求或者用请求序号标记。
这些问题看着细碎,其实都是在考你有没有真正理解浏览器请求的底层机制。答得上来就说明你是真的写过、踩过坑,而不是背了一堆接口文档。
5.5 我的调试工具推荐
最后分享几个我平时排查 AJAX 问题必用的工具和技巧,新人可以现在就练起来。
浏览器开发者工具的 Network 面板是排查网络请求的第一战场。打开它,点一下出问题的请求,你能看到请求头、请求体、响应体、状态码、耗时。我排查问题时的顺序永远是:先看请求有没有发出去,再看状态码是多少,然后看请求头和请求体格式对不对,最后看响应里到底返回了什么。这个顺序能覆盖九成以上的问题。
控制台里的 console.log 也别浪费。在 XHR 的 onreadystatechange 里打印每个阶段的状态和响应内容,能让你清晰看到请求走到哪一步挂掉的。如果你在封装的库里面打印这些日志,排查效率会提升非常多。
另外,如果你用 VS Code 写前端,装一个 REST Client 插件,调试后端接口时直接在编辑器里发请求,不用一直切换页面点来点去,谁用谁知道。
写在最后
我从零手写 AJAX 那段时间,最大的收获不是“会写一个请求”,而是理解了现代前端各种网络库背后的设计哲学。axios 确实好用,但如果你不知道它内部如何序列化参数、如何处理超时、如何统一错误,一旦项目里出现诡异的“请求发了但数据不对”,你会完全无从下手。
我现在带团队的时候,遇到新人来问我网络请求相关的 bug,我一般先问一句:你先把这次请求的 URL、请求头、请求体、状态码这四样东西贴出来。能老老实实贴出来的人,通常问题就已经解决一半了。这些基本功,没有任何框架能帮你省掉。
如果这篇内容对你有帮助,建议你照着文章里的代码自己敲一遍,跑通 GET、POST,再把封装部分的代码改造成你喜欢的样子。只有亲手踩过一两个坑,这些东西才真正成为你的。
