说实话,很多刚入行的前端同学甚至工作了两三年的老手,在搞RESTful接口的时候,依然会把GET和POST混着用,或者把PUT和PATCH傻傻分不清。这不仅是代码写得丑的问题,更是理解HTTP协议本质的缺失。今天咱们不整那些教科书式的定义,我以一个过来人的身份,聊聊这些请求方法到底该怎么用,以及在实际开发中那些让人头秃的坑。
一、先别急着写代码,搞懂HTTP方法的“性格”
HTTP协议里的这四个方法(GET, POST, PUT, DELETE)不是随便定义的,它们各自有明确的语义。理解了这个,你写代码自然就不容易出错。
GET:最老实的查询员
GET方法从字面意思就是“获取”。它的特点是幂等且安全。
- 幂等:意味着你调用1次和调用10次,结果完全一样,不会改变服务器状态。
- 安全:GET请求不应该对服务器数据产生任何副作用。
在实际开发中,GET通常用于:
- 获取用户列表
- 查询商品详情
- 搜索文章
错误用法案例:有一次我在看一个老项目的代码,发现有人用GET请求来删除数据库记录。
// 这是典型的错误用法
fetch('/api/users/delete/123', {
method: 'GET'
});
这样做不仅违反了RESTful规范,还会带来严重的安全隐患。想象一下,如果攻击者构造一个带有删除链接的图片,当页面加载时就会悄悄执行删除操作。这就是所谓的“GET请求被滥用导致的安全漏洞”。
正确用法:
// 正确的GET请求 - 获取用户信息
fetch('/api/users/123', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(response => response.json())
.then(data => console.log(data));
二、POST:最复杂的“多面手”
POST方法在HTTP协议中的定义是“创建资源”,但现实开发中,它被用在了各种场景。
POST的特点:
- 不幂等(多次调用可能产生多个资源)
- 可以携带大量数据
- 请求体数据对服务器可见
常见使用场景:
- 创建新用户
- 提交表单数据
- 上传文件
真实开发中的错误用法:
很多开发者把POST当成“万能提交方法”,什么数据都用POST。比如有个项目,查询商品库存竟然也用POST:
// 错误的用法 - 查询操作用POST
fetch('/api/products/inventory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId: '12345',
warehouse: 'CN01'
})
});
这样做的后果是:浏览器不会缓存这个请求,CDN也无法缓存,而且不符合语义。正确的做法应该用GET,把查询参数放在URL上:
// 正确的用法 - 查询操作用GET
fetch('/api/products/inventory?productId=12345&warehouse=CN01', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
三、PUT:最容易被误解的“更新者”
PUT方法在RESTful API中用于完整更新一个资源。
关键区别:
- PUT:替换整个资源(幂等)
- PATCH:部分更新资源(不幂等)
实际开发案例:
假设有一个用户资料接口,PUT和PATCH的区别就很明显了:
// PUT请求 - 完整更新用户信息
// 所有字段都是必须的,缺失的字段会被清空
fetch('/api/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 123,
name: '张三',
email: 'zhangsan@example.com',
phone: '13800138000', // 如果这个字段缺失,会被清空
address: '北京市朝阳区'
})
});
// PATCH请求 - 部分更新
// 只更新提供的字段
fetch('/api/users/123', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'newemail@example.com'
})
});
错误用法警示:
我见过很多开发者把PUT当成POST用,提交部分数据就期望服务器更新对应的字段。结果用户发现,改个邮箱地址,电话和地址都被清空了,一脸懵逼。
四、DELETE:最直接的“删除键”
DELETE方法用于删除指定资源。
特点:
- 幂等(删除一次和删除多次结果相同)
- 通常不需要请求体
正确用法:
// 删除用户
fetch('/api/users/123', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + token
}
});
错误用法:
// 错误:把删除做成软删除,还用了POST
fetch('/api/users/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 123,
softDelete: true,
reason: '用户主动注销'
})
});
软删除是业务逻辑,应该用PUT更新状态,而不是用POST假装删除:
// 正确的软删除方式
fetch('/api/users/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 123,
status: 'deleted',
deletedAt: new Date().toISOString(),
deletedReason: '用户主动注销'
})
});
五、真实开发中的综合场景分析
让我讲一个实际的项目案例。我们团队开发一个电商后台系统,涉及商品管理模块。
场景1:商品查询
// 搜索商品 - 使用GET,参数放在URL
fetch('/api/products?category=electronics&page=1&limit=20&sort=price:asc', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + userToken
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
renderProductList(data.products);
updatePagination(data.pagination);
})
.catch(error => {
console.error('Fetch error:', error);
showErrorToast('加载商品列表失败,请重试');
});
场景2:创建商品
// 创建商品 - 使用POST
async function createProduct(productData) {
try {
const response = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken
},
body: JSON.stringify({
name: productData.name,
description: productData.description,
price: productData.price,
category: productData.category,
stock: productData.stock,
images: productData.images
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '创建商品失败');
}
const result = await response.json();
return result;
} catch (error) {
console.error('Create product error:', error);
throw error;
}
}
场景3:更新商品库存
// 更新库存 - 使用PATCH(部分更新)
async function updateProductStock(productId, stockDelta) {
try {
const response = await fetch(`/api/products/${productId}/stock`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken
},
body: JSON.stringify({
delta: stockDelta, // 正数增加,负数减少
operator: currentUser.id,
reason: '库存调整'
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
return await response.json();
} catch (error) {
console.error('Update stock error:', error);
throw error;
}
}
场景4:删除商品
// 删除商品 - 使用DELETE
async function deleteProduct(productId) {
try {
// 先确认
if (!confirm('确定要删除这个商品吗?此操作不可恢复。')) {
return false;
}
const response = await fetch(`/api/products/${productId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + userToken
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message);
}
return true;
} catch (error) {
console.error('Delete product error:', error);
showErrorToast(error.message);
return false;
}
}
六、调试技巧:当请求出问题怎么办
1. 浏览器开发者工具调试
打开Chrome DevTools(F12),切换到Network标签,勾选Preserve log(保留日志),这样在页面跳转后也能看到之前的请求。
查看请求详情:
- Headers:查看请求头,确认Content-Type是否正确
- Payload/Request Body:查看发送的数据格式
- Response:查看响应内容和状态码
- Timing:分析请求耗时,定位性能问题
2. 常见错误及排查方法
错误1:CORS跨域问题
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
解决方案:
- 后端配置CORS头
- 开发环境使用代理(如webpack devServer proxy)
- 生产环境配置反向代理
// webpack配置示例
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
secure: false
}
}
}
};
错误2:415 Unsupported Media Type
HTTP Error 415 (Unsupported Media Type)
这通常是因为Content-Type不匹配。检查请求头:
// 确保Content-Type正确
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 检查这里
},
body: JSON.stringify(data)
});
错误3:400 Bad Request
HTTP Error 400 (Bad Request)
检查请求参数格式:
// 打印请求数据调试
const requestBody = {
name: 'Product Name',
price: 99.99,
category: 'electronics'
};
console.log('Request body:', JSON.stringify(requestBody));
console.log('Content-Type:', 'application/json');
fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(requestBody)
});
错误4:500 Internal Server Error
HTTP Error 500 (Internal Server Error)
这时候需要看后端日志。前端可以这样捕获:
fetch('/api/data', {
method: 'GET'
})
.then(response => {
if (!response.ok) {
// 500错误时,尝试读取错误信息
return response.text().then(text => {
throw new Error(`Server error: ${text}`);
});
}
return response.json();
})
.catch(error => {
console.error('Request failed:', error);
// 记录错误日志
logError(error, {
url: '/api/data',
method: 'GET',
timestamp: new Date().toISOString()
});
});
3. 使用fetch的完整示例(包含错误处理)
class ApiClient {
constructor(baseURL, token) {
this.baseURL = baseURL;
this.token = token;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const defaultHeaders = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
'X-Request-ID': this.generateRequestId()
};
const config = {
...options,
headers: {
...defaultHeaders,
...options.headers
}
};
try {
const startTime = performance.now();
console.log(`[${config.method}] ${url}`, config.body ? config.body : '');
const response = await fetch(url, config);
const duration = performance.now() - startTime;
console.log(`Response: ${response.status} (${duration.toFixed(2)}ms)`);
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
// 尝试解析错误响应
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (e) {
// 非JSON错误响应,保持原错误信息
}
throw new ApiError(errorMessage, response.status, config);
}
// 204 No Content没有响应体
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// 网络错误
throw new ApiError('Network error: ' + error.message, 0, config);
}
}
get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data)
});
}
patch(endpoint, data) {
return this.request(endpoint, {
method: 'PATCH',
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
}
generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
class ApiError extends Error {
constructor(message, statusCode, requestConfig) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.requestConfig = requestConfig;
}
}
// 使用示例
const api = new ApiClient('https://api.example.com', userToken);
// GET请求
api.get('/products?page=1&limit=20')
.then(products => console.log(products))
.catch(error => console.error(error));
// POST请求
api.post('/products', {
name: 'New Product',
price: 99.99
})
.then(product => console.log(product))
.catch(error => console.error(error));
// PUT请求
api.put('/products/123', {
id: 123,
name: 'Updated Product',
price: 109.99
})
.then(product => console.log(product))
.catch(error => console.error(error));
// PATCH请求
api.patch('/products/123', {
price: 89.99
})
.then(product => console.log(product))
.catch(error => console.error(error));
// DELETE请求
api.delete('/products/123')
.then(() => console.log('Deleted successfully'))
.catch(error => console.error(error));
七、总结:记住这些原则
- GET:只读操作,参数放URL,不要有副作用
- POST:创建资源,数据放请求体
- PUT:完整更新,幂等操作
- PATCH:部分更新,灵活操作
- DELETE:删除资源,简单直接
记住,选择正确的HTTP方法不仅是为了代码规范,更是为了让你的API更容易理解、维护和调试。当你的API遵循RESTful原则时,其他开发者(包括未来的你)读你的代码会轻松很多。
最后送大家一句话:好的API设计,是让调用者一看就知道该用什么方法、传什么参数、期望什么结果。 这才是真正专业的表现。
