端新人别慌:手把手教你用原生JS搞定AJAX请求(附避坑指南)
前端新人别慌:手把手教你用原生JS搞定AJAX请求(附避坑指南)
一、写在前面:AJAX这东西,真没那么玄乎
说实话,我当年第一次听到"AJAX"这个词的时候,脑子里浮现的是某种神秘的科幻武器,或者至少是像jQuery那样需要花钱买的插件。结果老师甩了一句"异步JavaScript和XML",我整个人都是懵的——这啥?异步我懂,XML我也见过,但它们加在一起怎么就突然高大上了?
后来自己撸了几个项目才明白,AJAX说白了就是"偷偷发请求"的艺术。你想啊,以前网页要获取新数据,得整个页面刷新,用户体验就跟翻书似的,哗啦一下全变了。有了AJAX,页面可以纹丝不动,背后的小弟(JavaScript)已经跑去找服务器要数据了,要到之后悄悄更新页面上的某一块,用户可能都没察觉发生了什么。
但这玩意儿看着简单,坑是真的多。我见过太多新人(包括我自己)在这些地方栽过跟头:跨域报错一脸懵逼、POST请求死活传不到数据、回调函数里的this指向突然变脸……所以今天这篇,我就当是跟你在微信群语音聊天,想到哪说到哪,把原生AJAX的里里外外、坑坑洼洼都唠清楚。代码会贴很多,注释也会写得很啰嗦——没办法,谁让我当年就是被一行没注释的代码坑到凌晨三点呢。
二、AJAX到底是个啥?别被 acronym 吓到
先破除一个迷信:AJAX不是某种新技术,也不是必须要学的框架。它就是个缩写——Asynchronous JavaScript and XML。虽然现在XML用得少了(JSON真香),但这个名字还是保留下来了。
核心就三个东西:
- 浏览器内置的 XMLHttpRequest 对象(简称XHR,这名字起得,还以为跟某个不良网站有关)
- JavaScript 的异步编程能力
- DOM 操作(拿到数据后总得显示出来吧)
整个过程就像点外卖:你(JavaScript)打开手机(创建XHR对象)→ 选餐厅填地址(open)→ 下单(send)→ 等骑手配送(异步等待)→ 收到餐更新订单状态(onload/onreadystatechange回调)。关键是,等外卖的时候你不用站在门口干等着,该刷剧刷剧,该打游戏打游戏——这就是异步的魅力。
不过啊,XHR这个API设计得确实有点反人类。同样是发请求,fetch API就清爽很多,但为啥我们还要学XHR?因为面试要考,而且有些老项目还在用,更重要的是——理解了XHR的底层,你用任何封装库都会心里有底。就像学开车先学手动挡,虽然自动挡更方便,但懂离合器的工作原理,关键时刻能救命。
三、起手式:new 一个 XMLHttpRequest 对象
好,开始写代码。最基础的版本长这样:
// 第一步:创建一个XHR对象
// 注意:IE老版本用的是 ActiveXObject("Microsoft.XMLHTTP"),现在2024年了,除非你在维护古董项目,否则不用管
const xhr = new XMLHttpRequest();
// 第二步:配置请求参数
// open(method, url, async, user, password)
// method: GET/POST/PUT/DELETE等,必须大写,虽然小写也能跑,但规范是大写
// url: 请求的地址,可以是绝对路径或相对路径
// async: 是否异步,true是异步(默认),false是同步(别用,会卡死页面)
xhr.open('GET', 'https://api.example.com/data', true);
// 第三步:发送请求
// GET请求send里面一般传null,POST请求传数据
xhr.send(null);
看起来简单对吧?但这里面的门道多了去了。
关于open方法的几点碎碎念:
-
method必须大写吗? 规范上是的,但浏览器其实很宽容,'get’和’GET’都能跑。不过为了保险,还是大写吧,万一哪天遇到个严格的浏览器呢。
-
URL要不要带协议? 如果你请求的是当前域的资源,可以写相对路径
/api/data;如果是跨域,必须写完整URL包括https://。这里有个坑:如果你写的是//api.example.com/data,浏览器会自动匹配当前页面的协议(http或https),这在混合内容场景下很有用。 -
第三个参数async,99%的情况你都应该传true或者不传(默认就是true)。如果你传false变成同步请求,JavaScript引擎会卡住直到服务器响应,页面在这期间完全冻结,用户啥都点不了。除非你在写Web Worker里的代码,否则别碰同步模式。
send方法的小秘密:
// GET请求
xhr.send(null); // 或者 xhr.send(),效果一样
// POST请求,传表单数据
const formData = 'name=张三&age=25';
xhr.send(formData);
// POST请求,传JSON
const jsonData = JSON.stringify({name: '张三', age: 25});
xhr.send(jsonData);
注意send接收的参数类型可以是 string、Document、Blob、ArrayBuffer、FormData 等。如果你传的是对象,记得先JSON.stringify(),否则服务器收到的是[object Object],别问我怎么知道的。
四、监听响应:onload vs onreadystatechange,该用哪个?
好了,请求发出去了,但服务器还没回话呢。怎么知道它啥时候回?有两个主要方案:
方案一:onload(推荐,现代浏览器支持)
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
// 请求成功完成时触发(状态码200-299都算成功)
xhr.onload = function() {
// this指向xhr对象
if (this.status === 200) {
console.log('成功拿到数据:', this.responseText);
// 解析JSON要小心,try-catch保平安
try {
const data = JSON.parse(this.responseText);
console.log('解析后的数据:', data);
} catch (e) {
console.error('JSON解析失败,原数据是:', this.responseText);
}
} else {
// 服务器返回了,但状态码不是200,比如404、500
console.error('服务器报错,状态码:', this.status, this.statusText);
}
};
// 网络错误时触发(比如断网、DNS解析失败)
xhr.onerror = function() {
console.error('请求失败,可能是网络问题');
};
// 请求超时触发(需要先设置timeout)
xhr.ontimeout = function() {
console.error('请求超时了老铁');
};
xhr.send();
方案二:onreadystatechange(兼容性好,能监控全过程)
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
xhr.onreadystatechange = function() {
// readyState的四个阶段:
// 0: UNSENT - 还没调用open()
// 1: OPENED - open()已调用
// 2: HEADERS_RECEIVED - send()已调用,已接收响应头
// 3: LOADING - 正在下载响应体
// 4: DONE - 全部搞定
if (this.readyState === 4) { // 请求完成
if (this.status === 200) {
console.log('搞定:', this.responseText);
} else {
console.error('出错了:', this.status);
}
}
};
xhr.send();
我该用哪个? 如果你不需要监控下载进度,直接用onload更清爽。但如果你想做个进度条,或者需要在发送过程中做点什么,onreadystatechange是必须的。
关于状态码的碎碎念:
很多人只判断status === 200,但HTTP状态码家族很大:
- 200 OK:标准成功
- 201 Created:创建成功(常用于POST)
- 204 No Content:成功但无返回内容
- 400 Bad Request:客户端请求参数错了
- 401 Unauthorized:没登录或token过期
- 403 Forbidden:没权限
- 404 Not Found:资源不存在
- 500 Internal Server Error:服务器炸了
所以更好的判断是:
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功区间
} else if (xhr.status >= 400 && xhr.status < 500) {
// 客户端错误,可能是参数传错了
} else if (xhr.status >= 500) {
// 服务器错误,找后端撕逼去
}
};
五、GET请求:拼参数是个技术活
GET请求简单,但URL参数拼起来容易出错。假设你要搜"前端开发",时间范围是最近一周:
// 错误示范,千万别这么干
const keyword = '前端开发';
const timeRange = '最近一周';
const url = 'https://api.example.com/search?keyword=' + keyword + '&time=' + timeRange;
// 结果:https://api.example.com/search?keyword=前端开发&time=最近一周
// 中文没编码,某些服务器会直接400错误,或者拿到乱码
正确做法是用encodeURIComponent,这玩意儿会把特殊字符(包括中文)转成URL安全的格式:
const params = {
keyword: '前端开发',
time: '最近一周',
page: 1
};
// 手动拼
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
const url = `https://api.example.com/search?${queryString}`;
console.log(url);
// 结果:https://api.example.com/search?keyword=%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91&time=%E6%9C%80%E8%BF%91%E4%B8%80%E5%91%A8&page=1
或者用URLSearchParams(现代浏览器支持,IE不行):
const params = new URLSearchParams({
keyword: '前端开发',
time: '最近一周'
});
const url = `https://api.example.com/search?${params.toString()}`;
GET请求的缓存陷阱:
浏览器会缓存GET请求,这意味着如果你连续发两次相同的GET请求,第二次可能直接拿缓存,根本没去服务器。这在开发环境很烦人,因为你改了后端代码,前端还是拿到旧数据。
解决方案:加个随机数或时间戳
// Cache Busting
const url = `https://api.example.com/data?t=${Date.now()}`;
// 或者
const url = `https://api.example.com/data?_=${Math.random()}`;
虽然丑,但管用。当然你也可以在请求头里加Cache-Control: no-cache,但有些浏览器不认,还是时间戳最靠谱。
六、POST请求:Content-Type是道坎
POST比GET复杂,因为涉及到请求体(body)的格式。最常见的坑就是忘记设置Content-Type,导致服务器收不到数据。
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/user', true);
// 坑1:没设Content-Type,服务器不知道你传的是啥格式
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
const data = 'name=张三&age=25';
xhr.send(data);
如果服务器期待的是表单格式(application/x-www-form-urlencoded),而你没有设置Content-Type,它可能解析不出来。如果期待的是JSON(application/json),你传表单格式也会失败。
常见Content-Type对照表:
- 表单格式(传统HTML form提交)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
const data = 'name=张三&age=25'; // 字符串格式,key=value用&连接
xhr.send(data);
- JSON格式(现代API主流)
xhr.setRequestHeader('Content-Type', 'application/json');
const data = JSON.stringify({name: '张三', age: 25});
xhr.send(data);
- multipart/form-data(上传文件)
// 这个不用手动设,FormData对象会自动设置,包括boundary
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', '张三');
xhr.send(formData);
// 注意:这时候千万别手动设Content-Type,让浏览器自动处理boundary
一个完整的POST-JSON示例:
function postJSON(url, data, callback) {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
callback(null, xhr.response);
} else {
callback(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => callback(new Error('网络请求失败'));
xhr.send(JSON.stringify(data));
}
// 使用
postJSON('https://api.example.com/user',
{ name: '李四', email: 'li@si.com' },
(err, result) => {
if (err) {
console.error('提交失败:', err);
return;
}
console.log('提交成功:', result);
}
);
关于POST数据大小的迷思:
网上很多人说GET有长度限制(URL太长服务器会拒绝),POST没限制。这话对也不对。实际上HTTP协议本身对URL长度没限制,但浏览器和服务器有(比如IE 2083字符,Apache 8192字符)。POST虽然body可以很大,但服务器也会设上限(比如PHP默认8M,Node.js的body-parser默认100kb)。所以别拿POST当无限容量用,传大文件还是用专门的文件上传方案。
七、处理响应:别只会responseText
拿到响应后,大多数人直接JSON.parse(xhr.responseText),但如果服务器返回的不是JSON呢?或者返回空字符串呢?
responseType属性:提前告诉浏览器你想要什么格式
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
// 可选值:''(默认,字符串)、'json'、'text'、'arraybuffer'、'blob'、'document'
xhr.responseType = 'json'; // 告诉浏览器:请自动帮我解析成JSON
xhr.onload = function() {
if (xhr.status === 200) {
// 如果responseType是'json',这里直接就是对象,不用parse
console.log(xhr.response); // 已经是JavaScript对象了
console.log(xhr.response.userName); // 直接访问属性
}
};
xhr.send();
responseType的妙用:下载文件
// 下载图片并显示
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/image.png', true);
xhr.responseType = 'blob'; // 二进制大对象
xhr.onload = function() {
if (xhr.status === 200) {
const blob = xhr.response;
const img = document.createElement('img');
img.src = URL.createObjectURL(blob); // 生成临时URL
document.body.appendChild(img);
}
};
xhr.send();
responseText vs response:
responseText:始终是字符串,不管服务器返回什么response:根据responseType自动转换后的结果。如果responseType是’‘或’text’,response和responseText一样;如果是’json’,response是解析后的对象。
空响应处理:
有时候服务器会返回204 No Content,或者返回空字符串。这时候JSON.parse会报错:
xhr.onload = function() {
if (xhr.status === 204 || !xhr.responseText) {
console.log('服务器没返回数据,但操作成功了');
return;
}
try {
const data = JSON.parse(xhr.responseText);
} catch (e) {
console.error('解析失败,原始响应:', xhr.responseText);
}
};
八、跨域:前端的永恒噩梦,但不该全让前端背锅
这是新人最头疼的问题。你在localhost:8080跑项目,请求api.example.com的数据,浏览器直接给你报错:
Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'http://localhost:8080' has been blocked by CORS policy...
首先搞清楚:这不是bug,是浏览器的安全机制(同源策略)。 浏览器不允许一个域的网页随意访问另一个域的资源,除非对方明确允许。
CORS(跨域资源共享)到底是谁配? 虽然报错出现在前端控制台,但解决方案主要是后端的。后端需要在响应头里加:
Access-Control-Allow-Origin: http://localhost:8080
// 或者通配符(生产环境慎用):
Access-Control-Allow-Origin: *
前端能做什么?
- 开发环境代理(最实用)
如果你用Webpack/Vite,配置个devServer.proxy,把请求转发到同源,浏览器就以为你在请求自己域的资源。
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
-
JSONP(老古董,现在很少用,仅支持GET)
利用<script>标签不受同源策略限制的特性,但只能GET,而且需要后端配合返回callback({data})格式的数据。 -
理解简单请求和预检请求(Preflight)
如果你发的请求满足以下条件,就是简单请求,直接发:
- 方法:GET/HEAD/POST
- Content-Type:仅限application/x-www-form-urlencoded、multipart/form-data、text/plain
- 请求头只有安全头部(Accept、Accept-Language等)
不满足条件的,比如Content-Type是application/json,或者自定义了Header(如X-Token),浏览器会先自动发个OPTIONS请求(预检),问服务器"我能不能这么干",服务器说可以,浏览器才会发真正的请求。
这也是为什么有时候你会看到控制台有两个请求,第一个是OPTIONS,第二个才是POST。如果OPTIONS返回403,后面的请求就不会发了。
withCredentials:带cookie的跨域
如果你的请求需要携带cookie(比如登录态),需要前后端配合:
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/user', true);
xhr.withCredentials = true; // 允许携带cookie
xhr.send();
后端也要响应:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8080 // 注意这里不能用*,必须指定具体域名
新手常见误区:
- “我在Postman里能跑通,为啥浏览器不行?”——因为Postman不受同源策略限制,它就是个HTTP客户端。
- “我把后端Allow-Origin设成*了,为啥还带cookie时报错?”——因为
*和withCredentials=true不能同时用,必须指定具体域名。
九、调试排错:当AJAX没反应时,你在想什么?
最崩溃的时刻:代码看着没问题,但控制台一片寂静,onload就是不触发。这时候别砸键盘,按这个 checklist 排查:
第一步:看Network面板
打开F12 -> Network -> XHR(或Fetch/XHR),看有没有你的请求:
- 完全没有请求记录:可能是JS报错阻止了执行,或者URL拼错了(比如undefined拼进URL里)
- 请求是红色的:看状态码,4xx是客户端问题,5xx是服务器问题
- 请求是灰色的(pending):一直挂着没响应,可能是服务器卡了,或者没收到(跨域被拦截了不会出现在这,但网络断开会pending)
第二步:看Console面板的CORS错误
如果有红色的CORS policy报错,那就是跨域问题,按上一节解决。
第三步:检查回调函数是否真的绑上了
// 错误示范:先send后绑定事件
xhr.send();
xhr.onload = function() {}; // 请求可能已经完成了,监听没绑上
// 正确示范:先绑定再send
xhr.onload = function() {};
xhr.send();
第四步:检查this指向
xhr.onload = function() {
console.log(this.responseText); // 这里的this是xhr对象,没问题
};
// 但如果你用箭头函数...
xhr.onload = () => {
console.log(this.responseText); // 箭头函数没有自己的this,this指向外层,可能是window,undefined
};
第五步:检查异步问题
let result;
xhr.onload = function() {
result = xhr.responseText;
};
console.log(result); // 这里一定是undefined,因为请求是异步的,还没执行完
如果你需要等请求完成再执行代码,得用回调或者Promise。
第六步:GET请求缓存作祟
明明后端改了数据,前端拿到的还是旧的?可能是浏览器缓存了GET请求。在URL后加?t=${Date.now()}试试。
一个万能调试技巧:
在onload第一行加debugger;或者console.log('响应回来了', xhr),至少能确认回调有没有执行。
十、实用技巧:让原生AJAX不那么难用
虽然XHR设计得有点 archaic,但我们可以通过封装让它现代化一点。
技巧1:Promise封装,告别回调地狱
function request(method, url, data = null, headers = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
// 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('网络请求失败'));
xhr.ontimeout = () => reject(new Error('请求超时'));
xhr.send(data);
});
}
// 使用
async function getUser() {
try {
const data = await request('GET', 'https://api.example.com/user');
console.log(data);
} catch (err) {
console.error('获取失败:', err);
}
}
技巧2:加个loading提示,提升体验
function requestWithLoading(method, url, data) {
// 显示loading
const loading = document.createElement('div');
loading.textContent = '加载中...';
loading.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:rgba(0,0,0,0.7);color:white;border-radius:8px;z-index:9999;';
document.body.appendChild(loading);
return request(method, url, data).finally(() => {
// 不管成功失败都隐藏loading
loading.remove();
});
}
技巧3:自动处理JSON
function ajax(url, options = {}) {
const { method = 'GET', data = null, responseType = 'json' } = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.responseType = responseType;
// 如果是对象且不是FormData,自动转JSON
if (data && typeof data === 'object' && !(data instanceof FormData)) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send(data);
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.statusText);
}
};
xhr.onerror = () => reject('Network Error');
});
}
// 使用超简单
ajax('https://api.example.com/user', { method: 'GET' })
.then(data => console.log(data))
.catch(err => console.error(err));
ajax('https://api.example.com/user', {
method: 'POST',
data: { name: '王五' }
});
技巧4:超时设置
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://slow-api.com/data', true);
xhr.timeout = 5000; // 5秒超时
xhr.ontimeout = function() {
console.error('请求超时,请检查网络或稍后重试');
};
xhr.send();
技巧5:进度监控(上传/下载)
// 下载进度
xhr.onprogress = function(event) {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`下载进度:${percent}%`);
// 可以在这里更新进度条
document.getElementById('progress').style.width = percent + '%';
}
};
// 上传进度(用于文件上传)
xhr.upload.onprogress = function(event) {
const percent = (event.loaded / event.total) * 100;
console.log(`上传进度:${percent}%`);
};
十一、别被现代框架吓住:原生AJAX才是你的内功
现在谁还手写XMLHttpRequest?Vue项目里axios.get(),React项目里fetch(),甚至直接用react-query或swr这种数据获取库。那学原生AJAX还有啥用?
首先,面试要考。 我就被问过"说说AJAX的实现原理"、“fetch和XHR有什么区别”。你要是说"我用axios的",面试官心里可能会扣分。
其次,理解底层才能用好封装。 比如你知道XHR有onprogress事件,用axios的时候就知道怎么配置onDownloadProgress;你知道跨域需要withCredentials,用fetch的时候就知道要设credentials: 'include'。
再者,极端环境能救命。 去年我维护一个老项目,跑在嵌入式设备里的webkit浏览器,版本老到fetch都不支持,axios又太大塞不进去,最后只能手写XHR。那时候要是不会原生,项目就黄了。
对比一下fetch和XHR:
fetch API更现代,基于Promise,写法清爽:
fetch('https://api.example.com/data')
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(data => console.log(data))
.catch(err => console.error(err));
但fetch也有坑:
- 默认不带cookie(需要手动设
credentials: 'include') - 404或500不会进catch,只有网络错误才会reject
- 不支持进度监控(虽然可以用ReadableStream实现,但麻烦)
- IE不支持(虽然IE已死,但某些内网环境还在用)
XHR虽然老,但功能齐全、兼容性好、控制精细。就像手动挡汽车,虽然现在都开自动挡,但懂离合器和档位的工作原理,关键时刻能救命。
十二、最后唠叨两句
写这篇的时候,我想起了自己第一次用AJAX的场景。那是2016年,我要做一个简单的天气查询页面。照着网上的教程敲代码,结果一直报404,查了两个小时才发现URL拼错了一个字母。那时候觉得AJAX真难,前端真难。
但现在回头看,AJAX其实是个很朴素的技术——创建对象、配置参数、发送、等回调。它的难点不在API本身,而在异步编程的思维转换,以及HTTP协议的各种细节(状态码、Content-Type、跨域等)。
如果你现在还在纠结,别怕,多写几个完整的例子,多踩几个坑,自然就熟了。就像学骑自行车,理论知识看再多,不如摔两跤来得实在。下次再有人问你"为啥不用jQuery的$.ajax?",你可以微微一笑:“我在练手动挡。”
毕竟,懂底层的前端,走路都带风。



&spm=1001.2101.3001.5002&articleId=157773017&d=1&t=3&u=1c41722fcb8e4fa7bc966f4470ae26c0)
2067

被折叠的 条评论
为什么被折叠?



