嘿,写前端的朋友,咱们今天不聊虚的。我知道你肯定遇到过这种尴尬时刻:接口跑通了,但数据就是不对;或者明明POST请求发出去了,后端收到的却是空值;再或者,一个DELETE请求直接把整个列表给删了,用户当场崩溃。
这些都是AJAX请求方法没用好惹的祸。HTTP协议里的GET、POST、PUT、DELETE,不仅仅是四个单词,它们是RESTful架构的基石。今天咱们就把这四个“老熟人”扒开来看看,从原理到代码,再从坑里爬出来,保证你看完能稳稳地写对每一个请求。
先搞懂身份:HTTP方法到底是谁在说话?
在深入代码之前,咱们得先建立一种“直觉”。想象你在和一个后台管理员(服务器)打交道。
- GET 就像你去图书馆借书。你只是想看或者拿一本书,你不会修改书架上的任何书,也不会把书带回家(除非是电子版缓存)。它的核心是“幂等”——你去借十次书,书架还是那个书架,书还是那本书,结果一样。
- POST 就像你去邮局寄快递。你提交了一堆材料,目的是创建一个新的包裹。这动作会改变服务器状态,而且你不可能通过同一个请求寄出完全相同的两个包裹(因为时间戳不同)。
- PUT 就像你去办护照更新。你要替换掉现有的信息。你带上新的照片、新的地址,告诉管理员:“把这些全换了。”它也是幂等的——你交两次一样的资料,护照还是那张护照,状态没有额外变化。
- DELETE 就像你申请注销户口。这是一个明确的移除动作。一旦执行,数据就不见了(或者被标记为删除)。这显然不是幂等的,删了就是删了。
为什么分这么细?因为后端开发者靠这些方法来判断你的意图。如果你用GET去删除数据,后端可能直接拒绝你,因为这违反了安全规范——GET请求不应该产生副作用。
GET:最透明但也最脆弱的请求
GET请求是最常用的,用来获取资源。比如加载文章列表、搜索商品、获取用户信息。
它的样子
GET请求的参数会附加在URL后面,也就是QueryString。
// 一个简单的GET请求,获取文章列表
fetch('/api/articles?page=1&limit=10&tag=tech')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
对应的网络请求长这样:
GET /api/articles?page=1&limit=10&tag=tech HTTP/1.1
坑点一:参数长度限制
很多初学者不知道,URL是有长度限制的。虽然HTTP协议没明确规定,但浏览器和服务器往往有自己的上限。
- Chrome:大约32KB。
- IE:只有2KB左右。
- 服务器(如Nginx、Apache):通常也在几KB到几十KB之间。
如果你试图用一个GET请求发送一大段JSON配置,或者几百个ID,大概率会报错 414 URI Too Long。
解决办法:如果数据量大,果断改用POST。或者,把大数据放在请求体(Body)里,而不是URL里。
坑点二:缓存问题
GET请求默认会被浏览器缓存。这意味着,如果你用GET请求获取用户信息,用户改名了,你再请求,浏览器可能直接给你旧数据,而不是去问服务器。
解决办法:
- 加时间戳:在URL后加
?_t=Date.now(),强行让URL不同,绕过缓存。 - 设置Headers:在请求头里加上
Cache-Control: no-cache或Pragma: no-cache。
fetch('/api/user/profile', {
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
坑点三:安全敏感信息
绝对不要用GET传递密码、Token、身份证号等敏感信息。因为它们会暴露在URL里,会留在浏览器历史、服务器日志、甚至Referer头里。
POST:承载复杂数据的主力
POST用于创建资源,或者提交复杂数据。它的参数放在请求体(Body)里,URL看不出太多东西。
它的样子
// 创建一个新用户
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 告诉服务器:我给你的是JSON格式
},
body: JSON.stringify({
username: 'zhangsan',
email: 'zhangsan@example.com',
password: 'secret123'
})
})
.then(res => res.json())
.then(data => console.log('Created user:', data));
坑点一:Content-Type搞错
这是新手最容易踩的坑。后端如果是用Spring、Express、Node.js等现代框架,通常默认期望接收 application/json 格式的Body。
如果你没设这个Header,或者设成了 text/plain,后端可能解析不出你的参数,返回400 Bad Request。
// 错误示范:没设Content-Type,默认可能是text/plain
fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ username: 'zhangsan' })
// 后端收到的body可能是 "{ username: 'zhangsan' }" 这种字符串,解析失败
});
坑点二:表单提交 vs JSON提交
有时候你需要提交表单数据(比如上传文件,或者简单的登录表单),这时候Content-Type应该是 application/x-www-form-urlencoded 或者 multipart/form-data(如果有文件)。
// 提交表单数据
const formData = new URLSearchParams();
formData.append('username', 'zhangsan');
formData.append('password', 'secret123');
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString()
});
注意:如果用 JSON.stringify 包装,一定要设 application/json;如果用 URLSearchParams,要设 application/x-www-form-urlencoded。搞混了,后端收不到数据。
坑点三:CSRF(跨站请求伪造)
POST请求比GET更敏感,因为它会修改服务器数据。攻击者可以诱导用户点击一个恶意链接,或者访问一个恶意页面,触发用户的浏览器向你的服务器发送POST请求(因为浏览器会自动带上Cookie)。
解决办法:
- Token验证:后端生成一个CSRF Token,放在HTML页面里,前端每次POST请求都带上这个Token(放在Header或Body里)。后端校验Token是否匹配。
- SameSite Cookie:设置Cookie的
SameSite属性为Strict或Lax,限制跨站携带Cookie。
PUT:更新与幂等性
PUT用于全量更新资源。比如,更新用户的完整信息,你传过去的JSON应该包含所有字段,后端会用你提供的数据替换掉旧数据。
它的样子
// 更新用户信息(全量替换)
fetch('/api/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'zhangsan_updated',
email: 'new@example.com',
age: 30
// 注意:这里没有password,说明我们只更新这三项,其他可能由后端保留或设为默认
})
});
坑点一:PUT vs PATCH
很多开发者分不清这两个。
- PUT:全量替换。你要更新什么,就得把所有字段都传一遍。如果你漏传了一个字段,后端可能会把它清空。
- PATCH:部分更新。你只需要传你想改的字段,其他字段保持不变。
如果你的需求是“只修改用户的邮箱”,用PATCH更合适。用PUT的话,你得把用户名、密码、年龄等其他字段都带上,否则可能出错。
// 部分更新,用PATCH
fetch('/api/users/123', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'new@example.com' })
});
坑点二:幂等性误用
PUT是幂等的,意思是发10次同样的PUT请求,结果和发1次一样。这对于确保网络重试时的数据安全很重要。但如果你用PUT来做“增加”操作(比如增加积分),那就错了,因为第二次请求会覆盖第一次的结果,而不是累加。
DELETE:删除需谨慎
DELETE用于删除资源。比如删除一篇文章、删除一个用户。
它的样子
// 删除ID为123的用户
fetch('/api/users/123', {
method: 'DELETE'
})
.then(res => {
if (res.status === 200 || res.status === 204) {
console.log('Deleted successfully');
}
});
坑点一:DELETE请求最好不带Body
虽然HTTP协议允许DELETE请求带Body,但很多服务器(尤其是老版本的Nginx、Apache或某些框架)会忽略DELETE的Body,或者解析失败。
如果你非要在删除时传一些参数(比如“强制删除”、“删除评论”),建议把这些参数放在URL里,或者通过Query String传递。
// 不推荐:DELETE带Body
fetch('/api/posts/123', {
method: 'DELETE',
body: JSON.stringify({ force: true }) // 可能不被服务器处理
});
// 推荐:参数放URL
fetch('/api/posts/123?force=true', {
method: 'DELETE'
});
坑点二:软删除 vs 硬删除
DELETE请求通常意味着“删了”。但现代应用中,很多情况下我们用“软删除”——只是把数据库里的记录标记为 is_deleted=1,而不是真的物理删除。
前端调用DELETE接口时,要清楚后端是软删除还是硬删除。如果是硬删除,数据就再也找不回来了。所以,很多时候,前端删完数据后,不要只是从UI上移除,最好重新请求一下列表,确保状态一致。
实战:一个完整的CRUD示例
假设我们要做一个简单的“任务管理”应用,包含增删改查。
const API_BASE = '/api/tasks';
// 1. GET:获取所有任务
async function getTasks() {
try {
const response = await fetch(API_BASE);
if (!response.ok) throw new Error('Failed to fetch tasks');
return await response.json();
} catch (error) {
console.error(error);
return [];
}
}
// 2. POST:创建任务
async function createTask(title, description) {
try {
const response = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description })
});
if (!response.ok) throw new Error('Failed to create task');
return await response.json();
} catch (error) {
console.error(error);
return null;
}
}
// 3. PUT:更新任务(全量)
async function updateTask(id, title, description) {
try {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, title, description }) // 注意:带上id,因为是全量替换
});
if (!response.ok) throw new Error('Failed to update task');
return await response.json();
} catch (error) {
console.error(error);
return null;
}
}
// 4. DELETE:删除任务
async function deleteTask(id) {
try {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete task');
return true;
} catch (error) {
console.error(error);
return false;
}
}
// 使用示例
(async () => {
const tasks = await getTasks();
console.log('Current tasks:', tasks);
const newTask = await createTask('Buy milk', 'Go to the store');
console.log('Created task:', newTask);
const updatedTask = await updateTask(newTask.id, 'Buy organic milk', 'Go to the organic store');
console.log('Updated task:', updatedTask);
const deleted = await deleteTask(newTask.id);
console.log('Deleted:', deleted);
})();
高级避坑:那些容易被忽视的细节
1. 异步与错误处理
不要用 try-catch 包裹 fetch 然后假设所有错误都会抛出异常。fetch 只有在网络故障时才会 reject,HTTP错误状态码(如404、500)不会让fetch reject!
// 错误示范:以为404会进catch
fetch('/api/nonexistent')
.then(res => {
if (!res.ok) {
// 这里必须手动处理错误,否则会继续执行
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.catch(error => {
console.error(error);
});
2. 跨域问题(CORS)
开发时,前端和后端域名不同,浏览器会拦截请求。你需要后端配置CORS,或者前端用代理。
在Vue/React项目中,可以通过 webpack 或 vite 配置代理:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
3. 请求取消
如果用户快速切换页面,之前发出的请求可能还会回来,导致页面数据错乱。可以用 AbortController 取消请求。
let controller;
function fetchData() {
// 取消上一次请求
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request cancelled');
} else {
console.error('Fetch failed', error);
}
});
}
4. 重试机制
网络不稳定时,请求可能失败。可以给关键请求加上重试逻辑。
async function fetchWithRetry(url, options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// 如果是4xx错误,不重试
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
} catch (error) {
if (i === retries - 1) throw error;
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
总结:一张表记住精髓
| 方法 | 用途 | 幂等性 | 参数位置 | 典型场景 |
|---|---|---|---|---|
| GET | 获取资源 | 是 | URL Query | 查询列表、获取详情 |
| POST | 创建资源 | 否 | Body | 提交表单、创建数据 |
| PUT | 全量更新 | 是 | Body | 更新用户完整信息 |
| DELETE | 删除资源 | 否 | URL/无 | 删除数据 |
记住,HTTP方法的选择不仅仅是语法问题,更是语义问题。正确使用它们,能让你的API更清晰、更安全、更易于维护。下次再写请求时,不妨停下来想一想:我这个动作,到底是“看”、“建”、“改”还是“删”?选对了方法,后端会感谢你,你的代码也会更优雅。
