AJAX请求方法详解:GET/POST/PUT/DELETE在前端开发中的应用与实战避坑
作为一个在这行摸爬滚打多年的前端,说实话,AJAX这东西我天天都在用,但每次遇到奇怪的问题还是会停下来想想——是不是请求方式用错了?参数格式对不对?服务端到底在搞什么鬼?今天咱们就好好聊聊这四种请求方式,顺便把那些让人头疼的报错案例也翻一翻。
为什么我们总在重复造轮子?
先说个场景:你正在做一个后台管理系统,表单提交、数据列表、修改信息、删除条目……每一个操作背后都是一次AJAX请求。很多人学的第一个请求就是$.ajax,后来jQuery不流行了,换成了fetch,再后来有了axios,工具变了好几个,但底层逻辑其实一直没变——我们只是在跟服务器”对话”,而GET、POST、PUT、DELETE就是四种不同的说话方式。
GET:最老实的请求,也是最常见的请求
它的性格特点
GET请求就像你进店买东西问价——”这个多少钱?”你只是询问信息,不会改变任何东西。所以GET有一个铁律:不应该用来修改数据。它适合获取数据,参数放在URL里,可读性高,也方便缓存。
代码实战
// 方式一:原生 fetch(现代浏览器推荐)
fetch('/api/users?page=1&size=20&sort=name')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('获取用户列表成功', data);
renderUserList(data.list);
})
.catch(error => {
console.error('GET请求失败:', error);
});
// 方式二:axios(封装好,社区用得最多)
import axios from 'axios';
const getUserList = async () => {
try {
const response = await axios.get('/api/users', {
params: {
page: 1,
size: 20,
sort: 'name'
},
timeout: 5000, // 5秒超时
headers: {
'Authorization': `Bearer ${token}` // 携带token
}
});
return response.data;
} catch (error) {
if (error.response) {
// 服务器返回了错误状态码(4xx, 5xx)
console.error('服务器错误:', error.response.status, error.response.data);
} else if (error.request) {
// 请求发出去了,但没收到响应(网络问题)
console.error('网络异常,请检查连接:', error.request);
} else {
// 请求配置阶段就出错了
console.error('请求配置错误:', error.message);
}
throw error;
}
};
常见报错及解决
报错一:404 Not Found
GET /api/v2/users 404
这种情况十有八九是接口地址写错了。我当初第一次工作时,把/api/user写成了/api/users,查了两小时bug,最后发现是多了个s。建议养成习惯:先把接口文档打开,或者让后端把接口跑通给你测试数据,再写前端。
报错二:401 Unauthorized
GET /api/me 401
这个很常见,尤其是做了token认证的。解决思路:检查token是否存在、是否过期。可以在axios拦截器里统一处理:
// 请求拦截器
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器统一处理401
axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// token过期,跳转登录页
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
报错三:跨域问题(CORS)
Access to fetch at 'http://api.example.com/users' from origin
'http://localhost:3000' has been blocked by CORS policy
这个问题前端看起来没法解决,但有几个 workaround。最直接的方式是让后端配置CORS头:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
如果你们公司有统一的网关或者Nginx,可以在反向代理层处理:
location /api/ {
proxy_pass http://backend-server;
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
}
开发阶段也可以直接用Vue/React项目的proxy配置,简单粗暴:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://backend-server',
changeOrigin: true
}
}
}
};
POST:发送数据的”正式”请求
它和GET的本质区别
POST用来创建资源或者发送敏感数据。参数放在请求体里,不暴露在URL中,适合传输大量数据。但要注意,POST本身并不是完全安全的——HTTP明文传输下,数据照样能被抓包看到,所以HTTPS才是王道。
代码实战
// 创建新用户
const createUser = async (userData) => {
try {
const response = await axios.post('/api/users', userData, {
headers: {
'Content-Type': 'application/json'
}
});
// 201 Created 表示资源创建成功
console.log('用户创建成功,ID:', response.data.id);
return response.data;
} catch (error) {
console.error('创建用户失败:', error.response?.data);
throw error;
}
};
// 调用示例
const newUser = await createUser({
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000',
role: 'admin'
});
常见报错及解决
报错一:400 Bad Request
POST /api/users 400
{
"error": "Email is required",
"fields": ["email"]
}
后端返回400,说明请求参数有问题。这时候别急着问后端,先看自己的请求体格式对不对。一个经典错误是把数据直接传给url参数而不是body:
// ❌ 错误写法:把数据放在url上
axios.post('/api/users?name=张三&email=zhangsan@example.com');
// ✅ 正确写法:数据放在请求体
axios.post('/api/users', {
name: '张三',
email: 'zhangsan@example.com'
});
还有一个坑是Content-Type。如果你发的是JSON格式,但没告诉服务端,它可能按form-data去解析,结果解析失败:
// 确保设置了正确的Content-Type
axios.post('/api/users', { name: '张三' }, {
headers: { 'Content-Type': 'application/json' }
});
// 或者用FormData(适合文件上传)
const formData = new FormData();
formData.append('name', '张三');
formData.append('avatar', fileInput.files[0]);
axios.post('/api/users', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
报错二:重复提交问题
这个错误很隐蔽。用户点两次按钮,发了两个POST请求,结果库里出现了两条重复数据。解决方案:
// 方案一:按钮防抖
let isSubmitting = false;
const handleSubmit = async () => {
if (isSubmitting) return;
isSubmitting = true;
try {
await createUser(formData);
showMessage('提交成功');
} catch (error) {
showMessage('提交失败,请重试');
} finally {
isSubmitting = false;
}
};
// 方案二:提交令牌(Token)
// 后端给前端一个一次性token,提交时携带,后端验证后作废
const response = await axios.get('/api/token');
const submitToken = response.data.token;
await axios.post('/api/users', formData, {
headers: { 'X-Submit-Token': submitToken }
});
PUT:更新已有资源的”完整替换”
PUT vs PATCH:搞清楚了不吃亏
这两个经常混用,但它们的语义不一样。PUT是全量更新——你传什么就是什么,不传的字段会被清空(或者置为null)。PATCH是部分更新——只修改你传的字段。
举例子:用户信息有name、email、phone三个字段。
PUT: { "name": "李四" }
结果: name="李四", email=null, phone=null
PATCH: { "name": "李四" }
结果: name="李四", email保持原值, phone保持原值
实际开发中,大部分场景用PATCH更合适。但如果接口设计是PUT,你就要把完整数据传过去。
代码实战
// 更新用户(PUT - 全量更新)
const updateUser = async (userId, fullData) => {
try {
const response = await axios.put(`/api/users/${userId}`, fullData);
return response.data;
} catch (error) {
if (error.response?.status === 404) {
throw new Error('用户不存在');
}
if (error.response?.status === 400) {
throw new Error('请求参数错误: ' + JSON.stringify(error.response.data));
}
throw error;
}
};
// 部分更新(PATCH)
const patchUser = async (userId, updates) => {
try {
const response = await axios.patch(`/api/users/${userId}`, updates);
return response.data;
} catch (error) {
console.error('更新失败:', error);
throw error;
}
};
// 实际使用
await updateUser('123', {
name: '王五',
email: 'wangwu@example.com',
phone: '13900139000'
// 这三个字段都会更新,缺少的会被清空
});
await patchUser('123', {
name: '王五'
// 只有name会更新,其他字段保持不变
});
常见报错及解决
报错一:405 Method Not Allowed
PUT /api/users/123 405
这个错误说明服务端不支持PUT方法。可能情况:
- 接口只支持PATCH,不支持PUT
- URL路径写错了
- 路由配置有问题
建议先看接口文档确认,再检查请求方式。
报错二:422 Unprocessable Entity
PUT /api/users/123 422
{
"errors": {
"email": ["格式不正确"],
"phone": ["长度必须是11位"]
}
}
422表示请求格式是对的,但数据验证失败了。这时候要仔细检查每个字段:
// 前端预校验,减少422的概率
const validateUser = (data) => {
const errors = {};
if (!data.name || data.name.length < 2) {
errors.name = '姓名至少2个字符';
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
errors.email = '邮箱格式不正确';
}
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(data.phone)) {
errors.phone = '手机号格式不正确';
}
return errors;
};
// 提交前校验
const errors = validateUser(formData);
if (Object.keys(errors).length > 0) {
showError(errors);
return;
}
await updateUser(userId, formData);
DELETE:删除操作的”谨慎执行”
为什么说DELETE要格外小心
DELETE请求理论上应该幂等——不管删多少次,结果一样。但实际业务中,很多时候用的是”软删除”(只是标记状态,不真的删数据),这时候DELETE请求可能需要额外参数:
// 硬删除:直接从数据库删除
axios.delete('/api/users/123');
// 软删除:标记为已删除
axios.delete('/api/users/123', {
data: { softDelete: true } // 有些接口设计这样
});
// 或者通过PUT更新状态
axios.put('/api/users/123', { status: 'deleted' });
代码实战
// 删除用户
const deleteUser = async (userId) => {
try {
const response = await axios.delete(`/api/users/${userId}`);
// 200 或 204 都表示成功
return response.status === 204 ? { success: true } : response.data;
} catch (error) {
if (error.response?.status === 403) {
throw new Error('没有权限删除该用户');
}
if (error.response?.status === 404) {
throw new Error('用户不存在');
}
throw error;
}
};
// 批量删除
const batchDeleteUsers = async (userIds) => {
try {
const response = await axios.delete('/api/users/batch', {
data: { ids: userIds }
});
return response.data;
} catch (error) {
console.error('批量删除失败:', error);
throw error;
}
};
// 带确认的删除
const safeDelete = async (userId, userName) => {
// 二次确认
const confirmed = window.confirm(`确定要删除用户「${userName}」吗?此操作不可恢复。`);
if (!confirmed) return;
try {
await deleteUser(userId);
// 删除成功后更新列表
removeUserFromList(userId);
showSuccess('删除成功');
} catch (error) {
showSuccess('删除失败,请稍后重试');
}
};
常见报错及解决
报错一:403 Forbidden
DELETE /api/users/456 403
最常见的原因:当前用户没有删除权限。比如普通用户想删另一个用户的数据。解决方式:
- 检查当前用户的角色权限
- 确认token是否有效
- 联系后端确认接口权限设计
// 权限校验前置检查
const canDelete = (currentUser, targetUser) => {
// 只能删自己
if (currentUser.id === targetUser.id) return true;
// admin可以删任何人
if (currentUser.role === 'admin') return true;
return false;
};
// 调用前判断
if (!canDelete(currentUser, targetUser)) {
showError('你没有权限删除该用户');
return;
}
await deleteUser(targetUser.id);
报错二:删除后页面状态没更新
这个不是接口报错,是前端逻辑问题。请求成功了,但列表里那条数据还在。解决方法:
// 删除成功后的回调
const deleteUser = async (userId) => {
await axios.delete(`/api/users/${userId}`);
// 方式一:重新请求列表
// await refreshUserList();
// 方式二:从本地列表移除(推荐,更快)
userStore.users = userStore.users.filter(u => u.id !== userId);
};
请求方式的幂等性:一个容易被忽视的概念
搞清楚这四个方法的区别,幂等性是一个关键点。简单来说:
- 幂等:同一个请求执行一次和执行多次,结果相同
- GET:天然幂等
- PUT:幂等(重复put同一个数据,结果一样)
- DELETE:幂等(删一次和删多次,结果都是没有这条数据)
- POST:不幂等(重复提交会创建多个资源)
这个区别在实际业务中很重要。比如支付场景,如果你用POST创建订单,网络抖动导致重复请求,就会产生重复订单。这时候要么前端加防重提交,要么后端做幂等性校验。
实际项目中的最佳实践
封装一个统一的请求模块
// request.js
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器:统一处理token
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 开发环境打印请求日志
if (import.meta.env.DEV) {
console.log(`[${config.method.toUpperCase()}]`, config.url, config.data);
}
return config;
},
error => Promise.reject(error)
);
// 响应拦截器:统一错误处理
request.interceptors.response.use(
response => {
// 根据实际接口约定处理
const { code, data, message } = response.data;
if (code === 0) {
return data;
}
return Promise.reject(new Error(message || '请求失败'));
},
error => {
const status = error.response?.status;
switch (status) {
case 400:
showToast('请求参数错误');
break;
case 401:
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
showToast('没有操作权限');
break;
case 404:
showToast('请求的资源不存在');
break;
case 422:
showToast('数据校验失败');
break;
case 500:
showToast('服务器内部错误');
break;
default:
showToast('网络异常,请稍后重试');
}
return Promise.reject(error);
}
);
export default request;
// api/users.js
import request from './request';
export const getUserList = (params) =>
request.get('/users', { params });
export const getUser = (id) =>
request.get(`/users/${id}`);
export const createUser = (data) =>
request.post('/users', data);
export const updateUser = (id, data) =>
request.put(`/users/${id}`, data);
export const patchUser = (id, data) =>
request.patch(`/users/${id}`, data);
export const deleteUser = (id) =>
request.delete(`/users/${id}`);
export const batchDeleteUsers = (ids) =>
request.delete('/users/batch', { data: { ids } });
类型安全(TypeScript用户必备)
// types/user.ts
export interface User {
id: string;
name: string;
email: string;
phone: string;
role: 'admin' | 'user';
status: 'active' | 'inactive';
createdAt: string;
updatedAt: string;
}
export interface CreateUserDTO {
name: string;
email: string;
phone: string;
role?: 'admin' | 'user';
}
export interface UpdateUserDTO {
name?: string;
email?: string;
phone?: string;
role?: 'admin' | 'user';
status?: 'active' | 'inactive';
}
总结:记住几个关键区别
说到底,GET/POST/PUT/DELETE其实就是四种不同目的的”对话方式”:
| 方法 | 用途 | 参数位置 | 幂等 | 缓存 |
|---|---|---|---|---|
| GET | 获取数据 | URL | 是 | 可缓存 |
| POST | 创建资源 | 请求体 | 否 | 不缓存 |
| PUT | 全量更新 | 请求体 | 是 | 不缓存 |
| DELETE | 删除资源 | URL/请求体 | 是 | 不缓存 |
新手最容易踩的坑就三个:把POST当GET用(参数放URL里)、搞不清PUT和PATCH的区别、以及忘记处理网络异常。把这些搞清楚了,AJAX这块就算入门了。
最后说句实在话,工具变了但原理不变。jQuery时代我们用$.ajax,现在用fetch或axios,本质都是HTTP请求。多看看浏览器Network面板,多理解每个状态码的含义,遇到问题不慌,一步步排查,这几种请求方式很快就能用得游刃有余。
