AJAX 请求方法完全指南 GET 与 POST 的选择技巧实际应用场景与常见错误排查
最近有个刚入行的小伙伴问我,说做项目的时候每次发请求都纠结半天:到底用 GET 还是 POST?有时候明明知道要用 POST,结果忘了加请求头,服务器直接返回 405 Method Not Allowed,调试了半天才发现是小细节没注意到。今天咱们就来好好聊聊 AJAX 请求这件事,把 GET 和 POST 的前前后后、左左右右都掰扯清楚。
先搞明白:AJAX 到底是什么
AJAX(Asynchronous JavaScript and XML)这个名字虽然带着”XML”,但现在的实际应用场景里,大部分时候我们用的都是 JSON 数据。它的核心思想很简单:页面不用刷新,就能和服务器交换数据。
你可以把它想象成你在餐厅吃饭,不需要每次点菜都跑回柜台重新排队,而是叫服务员过来记录你的需求,厨房做好了直接端过来就行。网页也是这个道理,用户在该干嘛干嘛,后台悄悄跟服务器打交道。
下面这张图能直观地看出传统方式和 AJAX 方式的区别:
传统方式:
用户点击按钮 → 页面跳转/刷新 → 服务器返回新页面 → 用户看到结果
AJAX 方式:
用户点击按钮 → JavaScript 发请求 → 服务器返回数据 → JavaScript 更新页面局部
在写代码之前,咱们先搞清楚浏览器原生提供的两种发请求的方式:XMLHttpRequest 和 Fetch API。前者是老前辈,兼容性无敌;后者是新秀,语法更简洁,而且支持 Promise。
GET 请求:最简单也最容易用错
基本用法
GET 请求是最常用的 HTTP 方法,它的设计初衷就是从服务器获取数据。用 XMLHttpRequest 写一个 GET 请求长这样:
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
// 初始化请求,指定方法和 URL
xhr.open('GET', 'https://api.example.com/users?page=1&limit=10');
// 设置请求头(GET 请求一般不需要特殊的请求头)
xhr.setRequestHeader('Accept', 'application/json');
// 监听响应
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log('获取到的数据:', data);
// 在这里更新页面
} else {
console.error('请求失败,状态码:', xhr.status);
}
};
xhr.onerror = function() {
console.error('网络错误');
};
// 发送请求
xhr.send();
用 Fetch API 写就简洁多了:
fetch('https://api.example.com/users?page=1&limit=10', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer your_token_here'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('获取到的数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
GET 请求的几个关键特性
- 数据放在 URL 里:参数以 query string 的形式跟在 URL 后面,比如
?key1=value1&key2=value2 - 有长度限制:大多数浏览器对 URL 长度有限制,通常在 2000 个字符左右(IE 甚至更少,只有 1024 字符)
- 会被缓存:浏览器会对 GET 请求的结果进行缓存,相同 URL 的请求可能直接返回缓存结果
- 会留下记录:URL 会出现在浏览器历史、服务器日志、代理服务器日志里
什么时候该用 GET
- 查询数据:比如搜索、分页、筛选列表
- 幂等操作:多次请求结果应该是一样的,不会修改服务器状态
- 数据量小:参数简单,不会超过 URL 长度限制
- 需要分享链接:因为参数在 URL 里,方便复制和分享
举个例子,做一个搜索功能,用 GET 就很合适:
// 搜索用户
function searchUsers(keyword) {
fetch(`/api/users/search?q=${encodeURIComponent(keyword)}`)
.then(res => res.json())
.then(data => {
// 渲染搜索结果
renderSearchResults(data.users);
});
}
这样用户甚至可以把搜索结果的 URL 发给别人,别人打开就能看到同样的搜索结果。
POST 请求:更强大也更复杂
基本用法
POST 请求用来向服务器提交数据,比如创建资源、提交表单、上传文件等。数据放在请求体(Request Body)里,而不是 URL 里。
// 用 XMLHttpRequest 发送 POST 请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/users');
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.setRequestHeader('Accept', 'application/json');
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
console.log('创建成功,返回的数据:', data);
} else {
console.error('请求失败,状态码:', xhr.status);
}
};
xhr.onerror = function() {
console.error('网络错误');
};
// 请求体数据
const userData = {
name: '张三',
email: 'zhangsan@example.com',
age: 28
};
xhr.send(JSON.stringify(userData));
用 Fetch API:
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json',
'Authorization': 'Bearer your_token_here'
},
body: JSON.stringify({
name: '张三',
email: 'zhangsan@example.com',
age: 28
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('创建成功:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
POST 请求的几个关键特性
- 数据在请求体里:不会暴露在 URL 中,相对更安全
- 没有长度限制:理论上可以发送任意大小的数据(实际受服务器配置限制)
- 不会被缓存:POST 请求默认不会被浏览器缓存
- 不会留下记录:参数不会出现在浏览器历史或服务器日志的 URL 中
什么时候该用 POST
- 创建资源:比如注册用户、发表文章
- 修改敏感数据:比如修改密码、转账
- 数据量大:参数很多或者包含大段文本、文件上传
- 非幂等操作:多次请求会产生不同结果
举个实际点的例子,比如做一个注册功能:
async function registerUser(userData) {
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: userData.username,
email: userData.email,
password: userData.password
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '注册失败');
}
const result = await response.json();
console.log('注册成功:', result);
return result;
} catch (error) {
console.error('注册出错:', error);
throw error;
}
}
这里用 POST 而不是 GET,原因很明显:密码放 URL 里太危险了,而且注册是一个会修改服务器状态的操作。
GET vs POST:到底该怎么选
这个问题问得最多,但实际上答案并没有那么绝对。我来给你梳理几个判断维度:
1. 操作语义
这是最根本的判断依据。HTTP 协议本身对 GET 和 POST 有明确的语义定义:
| 维度 | GET | POST |
|---|---|---|
| 目的 | 获取数据 | 提交数据 |
| 安全性 | 幂等(多次结果一致) | 非幂等(多次可能产生不同结果) |
| 副作用 | 不应修改服务器状态 | 可以修改服务器状态 |
所以简单说:只读操作用 GET,写操作用 POST。这不只是惯例,而是 RESTful 设计的基本原则。
2. 数据量大小
如果你的请求要传很多参数,或者参数里包含大段文本(比如富文本编辑器里的内容),用 POST 更合适。GET 请求的 URL 长度受限,而且太长了也不好看。
// 这个用 GET 就不合适了,参数太长
const longContent = '这是一段很长的文本内容,可能有好几KB甚至更多...';
// 用 POST 发送
fetch('/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '我的文章',
content: longContent,
tags: ['技术', '前端', 'AJAX']
})
});
3. 安全性考虑
GET 请求的参数在 URL 里,意味着:
- 会出现在浏览器历史记录里
- 会出现在服务器日志里
- 可能被referrer头传递给第三方网站
- 容易被截获(虽然 HTTPS 可以缓解这个问题)
所以敏感数据千万不要用 GET。比如密码、身份证号码、手机号这些,一律用 POST,而且请求体也要加密或者用 HTTPS。
4. 缓存需求
如果你希望结果可以被缓存(比如搜索结果、列表数据),GET 是更好的选择,因为浏览器和 CDN 都能自动缓存 GET 请求的结果。POST 请求默认不会被缓存。
// 搜索建议可以用 GET,方便缓存
fetch('/api/suggestions?q=' + encodeURIComponent(query))
.then(res => res.json())
.then(data => {
renderSuggestions(data);
});
实际项目中的常见场景
下面这些都是我实际项目中经常遇到的,你看看有没有共鸣:
场景一:分页列表
// GET 请求,参数清晰,可缓存
fetch(`/api/products?page=${page}&limit=${limit}&category=${category}`)
.then(res => res.json())
.then(data => renderProductList(data));
场景二:用户登录
// POST 请求,密码不能暴露在 URL 里
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: username,
password: password
})
});
场景三:上传头像
// POST 请求,携带文件数据
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
fetch('/api/users/avatar', {
method: 'POST',
body: formData
// 注意:用 FormData 时不要手动设置 Content-Type,浏览器会自动设置
});
场景四:删除文章
// 这里有个争议点:DELETE 还是 POST?
// RESTful 风格推荐用 DELETE,但很多团队因为兼容性问题用 POST
fetch('/api/articles/123', {
method: 'DELETE'
// 或者 method: 'POST',header 里加 X-HTTP-Method-Override: DELETE
});
常见错误排查
这部分是重点,很多坑我都踩过,分享出来希望能帮你少走弯路。
错误一:405 Method Not Allowed
这个错误最常见的原因就是用错了请求方法。比如服务器接口设计成只接受 POST,你偏偏用了 GET,或者反过来。
排查方法:
- 仔细看接口文档,确认期望的方法
- 检查代码里的
method参数 - 如果是前后端分离项目,检查 CORS 预检请求(OPTIONS)是否通过
// 错误示例:接口要求 POST,用了 GET
fetch('/api/users', {
method: 'GET', // 错误!应该是 POST
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '李四' }) // GET 不应该有 body
});
错误二:400 Bad Request
参数不对、格式错误、缺少必填项都可能触发这个错误。
常见原因:
- JSON 格式错误
- 缺少必填字段
- 字段类型不对(比如数字传了字符串)
- 请求头不对
排查方法:
- 检查浏览器开发者工具里的 Network 面板,看请求体和响应体
- 逐字段校验参数
- 看响应里的错误信息(如果有的话)
// 错误示例:参数格式不对
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '张三',
age: '28' // 错误!age 应该是数字,不是字符串
})
});
错误三:CORS 错误
跨域问题是前端开发者的噩梦之一。最典型的报错是:
Access to fetch at 'https://api.example.com' from origin 'https://your-site.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
根本原因:浏览器的同源策略。简单说,你的前端页面和 API 服务器不在同一个源(协议+域名+端口),浏览器默认不允许跨域请求。
解决方案有两种思路:
思路一:后端配置 CORS 响应头
// Node.js + Express 示例
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://your-site.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200); // 预检请求直接返回
}
next();
});
思路二:前端配置代理(开发环境)
// Vite 配置示例
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
}
};
错误四:数据没有发送成功
这种情况经常发生在用 FormData 上传文件的时候。
// 错误示例:手动设置了 Content-Type
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/api/upload', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data' // 错误!浏览器会自动设置,包含 boundary
},
body: formData
});
浏览器在发送 multipart/form-data 请求时,会自动在 Content-Type 里加上 boundary 参数,手动设置反而会破坏这个机制。正确写法是不设置 Content-Type:
// 正确写法
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/api/upload', {
method: 'POST',
body: formData // 浏览器会自动设置正确的 Content-Type
});
错误五:请求挂起,一直没有响应
可能的原因:
- 服务器没收到请求(网络问题、防火墙、代理问题)
- 服务器处理太慢,超时了
- 客户端代码有问题,请求没发出去
排查方法:
- 打开浏览器的开发者工具,看 Network 面板
- 检查请求是否已经发出,状态是什么
- 如果是超时问题,设置合理的 timeout
// 设置超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
fetch('/api/slow-endpoint', {
method: 'GET',
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.error('请求超时了');
} else {
console.error('请求失败:', error);
}
});
错误六:中文乱码
这个问题的根源通常是编码不一致。服务器返回的是 UTF-8,但浏览器当成 GBK 解析了。
解决方法:
- 确保服务器返回的响应头里有
Content-Type: application/json; charset=utf-8 - 或者在请求头里明确指定 Accept-Charset
fetch('/api/data', {
headers: {
'Accept': 'application/json; charset=utf-8'
}
});
进阶技巧:封装一个通用的请求函数
实际项目中,我们不会每次都写那么长的代码。通常会把请求逻辑封装起来,复用起来更方便。
// request.js
class HttpClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.defaults = {
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
}
// 通用的请求方法
async request(method, url, options = {}) {
const config = {
...this.defaults,
...options,
method: method.toUpperCase(),
url: this.baseURL + url
};
// 处理请求体
if (config.body && !(config.body instanceof FormData)) {
config.body = JSON.stringify(config.body);
}
// 处理超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
try {
const response = await fetch(config.url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new HttpError(response.status, errorData.message || '请求失败', errorData);
}
// 204 No Content 没有返回体
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// 便捷方法
get(url, options) {
return this.request('GET', url, options);
}
post(url, data, options) {
return this.request('POST', url, { ...options, body: data });
}
put(url, data, options) {
return this.request('PUT', url, { ...options, body: data });
}
delete(url, options) {
return this.request('DELETE', url, options);
}
}
// 自定义错误类
class HttpError extends Error {
constructor(status, message, data) {
super(message);
this.name = 'HttpError';
this.status = status;
this.data = data;
}
}
// 使用示例
const api = new HttpClient('https://api.example.com');
// GET 请求
api.get('/users?page=1')
.then(users => console.log(users))
.catch(error => console.error(error));
// POST 请求
api.post('/users', {
name: '王五',
email: 'wangwu@example.com'
})
.then(user => console.log(user))
.catch(error => console.error(error));
// 文件上传
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
api.post('/users/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 30000 // 上传超时时间长一点
})
.then(result => console.log(result))
.catch(error => console.error(error));
这样封装之后,代码整洁很多,也方便统一处理错误、添加请求拦截器、管理 token 等。
几个容易被忽视的小细节
1. GET 请求也可以有请求体,但很多服务器会忽略它
虽然 HTTP 规范没有明确禁止 GET 请求带 body,但大多数服务器会忽略 GET 请求的请求体。所以如果你发现 GET 请求发了数据但服务器收不到,别奇怪,这是正常行为。
2. POST 请求的 Content-Type 决定数据格式
// 发送 JSON
headers: { 'Content-Type': 'application/json' }
// 发送表单数据
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
// 上传文件
// 不要手动设置 Content-Type,让浏览器自动处理
headers: { 'Content-Type': 'multipart/form-data' } // 错误!
3. 预检请求(Preflight)是 CORS 的一部分
当请求满足以下条件之一时,浏览器会先发送一个 OPTIONS 请求(预检请求),得到服务器允许后再发真实请求:
- 使用了 GET、POST、HEAD 以外的方法
- 设置了自定义请求头
- Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一
这就是为什么有时候你会看到 Network 面板里有两条请求,第一条是 OPTIONS,第二条才是你的真实请求。
4. 请求取消很重要
在现代单页应用里,用户可能快速切换页面或触发多个请求。如果前一个请求还没回来,后一个请求就发了,可能会出问题。用 AbortController 可以取消请求:
let currentController = null;
function searchUsers(query) {
// 取消上一个请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
fetch(`/api/users/search?q=${encodeURIComponent(query)}`, {
signal: currentController.signal
})
.then(res => res.json())
.then(data => renderResults(data))
.catch(error => {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
}
});
}
最后说两句
AJAX 请求这件事,说到底就是理解 HTTP 协议的基本原理,然后根据场景选择合适的方法。GET 和 POST 没有谁比谁高级,关键是用对地方。
实际开发中,我最常犯的错误就是:
- 该用 POST 的时候用了 GET,结果敏感数据暴露在 URL 里
- 忘记处理请求超时的情况
- 上传文件时手动设置 Content-Type
希望这篇文章能帮你避开这些坑。如果还有问题,欢迎在评论区交流,或者直接在开发者工具里调试,看看请求和响应的具体细节,比看十篇文章都管用。
记住, debugging 的时候别慌,Network 面板是你的好朋友,它会把一切都展示给你看。
