嘿,写前端的朋友,咱们今天不聊虚的。我知道你肯定遇到过这种尴尬时刻:接口跑通了,但数据就是不对;或者明明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请求获取用户信息,用户改名了,你再请求,浏览器可能直接给你旧数据,而不是去问服务器。

解决办法

  1. 加时间戳:在URL后加 ?_t=Date.now(),强行让URL不同,绕过缓存。
  2. 设置Headers:在请求头里加上 Cache-Control: no-cachePragma: 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)。

解决办法

  1. Token验证:后端生成一个CSRF Token,放在HTML页面里,前端每次POST请求都带上这个Token(放在Header或Body里)。后端校验Token是否匹配。
  2. SameSite Cookie:设置Cookie的 SameSite 属性为 StrictLax,限制跨站携带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项目中,可以通过 webpackvite 配置代理:

// 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更清晰、更安全、更易于维护。下次再写请求时,不妨停下来想一想:我这个动作,到底是“看”、“建”、“改”还是“删”?选对了方法,后端会感谢你,你的代码也会更优雅。