异步请求超时数据传不全 GET POST 方法选错怎么办 AJAX 请求实用指南
嗨,朋友!你是不是也遇到过这种抓狂的情况:页面上数据死活加载不出来,控制台报个超时错误;或者明明传了参数,后端却收不到;再或者 GET 请求参数传了中文直接乱码…… 别急,这篇文章就是把这些问题一次性说清楚,让你以后写 AJAX 请求不再踩坑。
一、先搞懂一件事:AJAX 到底是什么
AJAX 的全称是 Asynchronous JavaScript and XML,说白了就是:不用刷新整个页面,就能跟服务器偷偷交换数据。
你平时刷微博、看微信,那些不用跳转页面就能加载新内容,背后基本都是 AJAX 在干活。它最核心的工具就是 XMLHttpRequest(简称 XHR),现代浏览器也提供了更简洁的 fetch API。
// 最原始的 XHR 写法(了解一下就行,现在很少直接这么写)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
现在主流写法是用 fetch,干净得多:
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('请求失败了', error));
二、异步请求超时怎么办?
超时这个事,说白了就是服务器 response 回来的太慢,或者网络卡了。你有三种方式处理它:
方式一:给 fetch 设置超时(原生支持不太好,得用 AbortController)
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, {
...options,
signal: controller.signal
})
.then(response => {
clearTimeout(timeoutId);
return response;
})
.catch(error => {
if (error.name === 'AbortError') {
throw new Error('请求超时了,请稍后再试');
}
throw error;
});
}
// 使用
fetchWithTimeout('/api/slow-data', {}, 3000)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err.message));
方式二:用 axios(推荐!)
axios 内置了超时配置,写起来最舒服:
import axios from 'axios';
axios.get('/api/slow-data', {
timeout: 5000, // 5秒超时
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('成功拿到数据', response.data);
})
.catch(error => {
if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
console.error('请求超时了!');
} else {
console.error('其他错误', error.message);
}
});
方式三:XHR 的 timeout 属性(老项目还在用)
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.timeout = 5000; // 设置超时时间(毫秒)
xhr.ontimeout = function() {
console.error('请求超时!');
};
xhr.onerror = function() {
console.error('网络错误!');
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log('请求成功', xhr.responseText);
} else {
console.error('服务器返回错误状态码', xhr.status);
}
};
xhr.send();
小贴士:超时时间设置多少合适?一般 3~10 秒比较合理。太短了容易误判,太长了用户等得不耐烦。如果是图片加载、文件下载这种,可以适当给长一点。
三、数据传不全?99% 是这几个原因
原因一:Content-Type 没设对
这是最常见的问题!你的浏览器和服务器之间有个”协议”,就是 Content-Type。设错了,服务器就解析不了你的数据。
// ❌ 错误示范:用了 JSON 格式传数据,但没告诉服务器
fetch('/api/user', {
method: 'POST',
headers: {
// 忘记设 Content-Type 了!
},
body: JSON.stringify({ name: '张三', age: 25 })
});
// ✅ 正确写法:告诉服务器我传的是 JSON
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '张三', age: 25 })
});
原因二:表单数据用错格式
有时候你要传的是表单数据(比如登录、注册),不能直接用 JSON.stringify,要用 URLSearchParams:
// ✅ 传表单数据的标准写法
const formData = new URLSearchParams();
formData.append('username', '张三');
formData.append('password', '123456');
formData.append('remember', 'true');
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString()
})
.then(res => res.json())
.then(data => console.log('登录结果', data));
或者直接用 FormData 对象(适合上传文件):
// 上传文件的写法
const uploadForm = new FormData();
uploadForm.append('avatar', fileInput.files[0]);
uploadForm.append('userId', '12345');
fetch('/api/upload', {
method: 'POST',
body: uploadForm
// 注意:用 FormData 的时候不要手动设 Content-Type
// 浏览器会自动帮你加上 boundary 参数
})
.then(res => res.json())
.then(data => console.log(data));
原因三:参数嵌套太深,序列化出问题
// ❌ 直接 stringify 嵌套对象可能出问题
const user = {
name: '张三',
profile: {
age: 25,
address: {
city: '北京',
district: '朝阳'
}
}
};
// ✅ 用 qs 库或者手动展开
import qs from 'qs';
const encoded = qs.stringify({
name: '张三',
'profile.age': 25,
'profile.address.city': '北京'
});
// 或者用 axios 自带的序列化
axios.post('/api/user', user, {
headers: { 'Content-Type': 'application/json' }
});
原因四:中文乱码
// 后端如果不支持中文,传中文参数会乱码
// 解决方案:传之前 encode
const username = '张三';
const encoded = encodeURIComponent(username);
fetch(`/api/search?name=${encoded}`)
.then(res => res.json())
.then(data => console.log(data));
四、GET 和 POST,到底该选哪个?
这是一个经典问题,很多人搞混。记住这个原则:
| GET | POST | |
|---|---|---|
| 用途 | 查询数据(查) | 提交/修改数据(增删改) |
| 参数位置 | URL 后面(?key=value) | 请求体(body)里 |
| 参数长度 | 有限制(一般几千字符) | 基本无限制 |
| 安全性 | 参数可见,不安全 | 参数在 body 里,相对安全 |
| 缓存 | 可被缓存 | 默认不缓存 |
| 幂等性 | 幂等(多次结果一样) | 非幂等 |
GET 的正确用法
// 搜索用户
fetch('/api/users?name=张三&age=25&page=1')
.then(res => res.json())
.then(data => console.log(data));
// 或者用模板字符串更清晰
const searchParams = new URLSearchParams({
name: '张三',
age: 25,
page: 1,
pageSize: 10
});
fetch(`/api/users?${searchParams}`)
.then(res => res.json())
.then(data => console.log(data));
POST 的正确用法
// 创建用户
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
})
.then(res => res.json())
.then(data => console.log('创建成功', data));
一个容易踩的坑:用 GET 传大量数据
// ❌ 错误:GET 请求 body 没意义,很多服务器会忽略
fetch('/api/search', {
method: 'GET',
body: JSON.stringify({ keywords: 'AJAX', page: 1 })
});
// ✅ 正确:GET 请求参数放 URL 里
const params = new URLSearchParams({
keywords: 'AJAX',
page: 1
});
fetch(`/api/search?${params}`);
五、AJAX 请求的完整最佳实践
1. 封装一个自己的请求工具
别每次都写一遍 fetch,封装一个通用的:
// request.js
class Request {
constructor(baseURL = '', timeout = 10000) {
this.baseURL = baseURL;
this.timeout = timeout;
}
async request(method, url, data = null, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const config = {
method: method.toUpperCase(),
headers: {
'Content-Type': 'application/json',
...options.headers
},
signal: controller.signal,
...options
};
if (data && method.toLowerCase() !== 'get') {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(this.baseURL + url, config);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络连接');
}
throw error;
}
}
get(url, data, options) {
const params = new URLSearchParams(data).toString();
return this.request('get', params ? `${url}?${params}` : url, null, options);
}
post(url, data, options) {
return this.request('post', url, data, options);
}
put(url, data, options) {
return this.request('put', url, data, options);
}
delete(url, data, options) {
return this.request('delete', url, data, options);
}
}
// 导出单例
const request = new Request('https://api.example.com', 8000);
export default request;
使用方式超简单:
import request from './request';
// GET 请求
request.get('/users', { page: 1, pageSize: 10 })
.then(users => console.log(users))
.catch(err => console.error(err));
// POST 请求
request.post('/users', {
name: '张三',
email: 'zhangsan@example.com'
})
.then(result => console.log(result))
.catch(err => console.error(err));
2. 加个请求拦截器(类似 axios 的拦截器)
// 全局请求拦截:自动加 token
fetch('/api/user', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
// 或者统一处理
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [url, config] = args;
// 自动加 token
if (!config?.headers) config = { ...config, headers: {} };
config.headers = {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
...config.headers
};
return originalFetch.call(this, url, config);
};
3. 错误处理和重试机制
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return await response.json();
}
// 4xx 错误不重试
if (response.status >= 400 && response.status < 500) {
throw new Error(`客户端错误:${response.status}`);
}
throw new Error(`服务器错误:${response.status}`);
} catch (error) {
lastError = error;
console.warn(`请求失败,第 ${i + 1} 次重试`, error.message);
// 等待一段时间后重试(指数退避)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
throw lastError;
}
// 使用
fetchWithRetry('/api/some-data', { timeout: 5000 })
.then(data => console.log(data))
.catch(err => console.error('重试多次后仍失败', err));
六、实际项目中的常见坑和解决方案
坑一:跨域问题(CORS)
Access to fetch at 'http://api.example.com' from origin 'http://localhost:3000'
has been blocked by CORS policy
这个错误说明前后端域名不一样,浏览器安全策略拦截了请求。解决方法在后端,前端改不了:
// 后端 Node.js 示例(Express)
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000', // 允许的前端域名
credentials: true // 如果需要带 cookie
}));
坑二:大文件上传超时
// 上传大文件,超时时间要设长一点
const formData = new FormData();
formData.append('file', largeFile);
axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000, // 60秒超时
onUploadProgress: (progressEvent) => {
// 显示上传进度
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`上传进度:${percent}%`);
}
});
坑三:并发请求太多把服务器打崩了
// 用 Promise.all 同时发多个请求要小心
const [users, orders, products] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/products').then(r => r.json())
]);
// 如果请求很多,可以用并发控制
async function concurrentFetch(urls, concurrency = 3) {
const results = [];
let index = 0;
async function worker() {
while (index < urls.length) {
const url = urls[index++];
try {
const response = await fetch(url);
results.push(await response.json());
} catch (error) {
console.error(`请求 ${url} 失败`, error);
}
}
}
const workers = Array.from({ length: concurrency }, () => worker());
await Promise.all(workers);
return results;
}
七、快速排查清单
下次请求出问题,按这个顺序检查:
- [ ] 超时了吗? 检查
timeout设置,看网络是否正常 - [ ] Content-Type 对吗? JSON 请求要设
application/json - [ ] 参数序列化了吗? 表单数据用
URLSearchParams,不要直接传对象 - [ ] GET/POST 选对了吗? 查数据用 GET,改数据用 POST
- [ ] 中文编码了吗? 用
encodeURIComponent处理中文参数 - [ ] 跨域了吗? 检查后端是否配置了 CORS
- [ ] Token 带了吗? 检查 Authorization 头
- [ ] 后端收到数据了吗? 看后端日志,确认不是后端的问题
写到这里,你应该对 AJAX 请求有了比较全面的认识。其实这些坑都不是什么高深的问题,大部分都是粗心或者不了解细节导致的。多写几次,注意一下这些细节,以后写请求就不会再踩坑了。
如果你还有什么具体问题,欢迎在评论区留言,咱们一起讨论!
