你是不是也经历过这样的时刻:控制台突然蹦出一坨红色的 Network Error 或者 Failed to fetch,你盯着屏幕发呆,头发一把一把地掉,最后发现只是忘了加个 Content-Type,或者是后端跨域没配好 CORS。
别慌,作为一个在代码坑里摸爬滚打多年的“老狗”,今天咱们不聊虚的,直接上干货。我把我之前踩过的、见过的、以及亲手调试过的 Fetch 和 Axios 的坑,全给你盘出来。这篇文章不教你从头学 HTTP,只教你怎么写出不会崩的请求代码。
一、先别急着写代码,搞清楚你的“武器”是谁
在入坑之前,你得知道 Fetch 和 Axios 到底有啥区别。很多人觉得“都能发请求,有啥好争的”,但其实它们性格差别挺大。
Fetch:浏览器亲儿子,原生但有点“冷”
Fetch 是 W3C 的标准,现代浏览器自带,不用装任何包。它的优点很明显:轻量、原生、没有第三方依赖。
但它的缺点也很致命,至少在我看来,它是“反直觉”的:
- Promise reject 的条件太苛刻:只有网络故障或 CORS 报错时,Fetch 才会 reject。如果服务器返回
404或500,Fetch 依然会 resolve,你需要手动判断response.ok。 - 没有超时控制:这是 Fetch 最大的硬伤。你没法直接在请求里写
timeout: 3000,得靠AbortController手动实现,代码量直接翻倍。 - 默认不带 Cookie:除非你显式设置
credentials: 'include',否则跨域请求时,Cookie 和鉴权信息全丢。 - JSON 还得手动 parse:不像 Axios 直接返回 data,Fetch 返回的是 Response 对象,你得自己调
.json()或.text()。
Axios:封装好的“保姆级”工具
Axios 是一个基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js。它把 Fetch 的很多痛点都解决掉了:
- 自动 JSON 转换:返回的已经是解析好的数据,不用手动
.json()。 - 请求/响应拦截器:你可以统一处理 token 注入、全局错误提示,这功能太香了。
- 自动转换请求和响应数据:你传 JSON,它自动序列化;你返回 JSON,它自动反序列化。
- 自带超时和取消请求:
timeout: 3000直接写,CancelToken或AbortController支持取消。 - 客户端防护:防止 CSRF 攻击。
我的建议:如果你在做大型项目,尤其是企业级后台、移动端 H5,无脑选 Axios。Fetch 适合那种简单脚本、微前端子应用、或者不想引入第三方依赖的场景。
二、Fetch 实战:那些让你抓狂的坑
坑1:你以为 404 是错误,但 Fetch 不这么认为
fetch('/api/user/123')
.then(res => {
console.log(res.status); // 输出 404
console.log(res.ok); // 输出 false
return res.json(); // 如果直接 return,下面会报错,因为 404 可能没有 body
})
.then(data => console.log(data))
.catch(err => console.error('网络错误', err)); // 这里根本不会触发!
解读:很多新手以为 .catch() 能捕获所有错误,包括服务器返回的错误状态码。错了!只有真正的网络错误(如断网、DNS 解析失败、CORS 拦截)才会进 .catch()。404、500 这些,都在 .then() 里,你必须手动判断 res.ok 或 res.status。
正确写法:
fetch('/api/user/123')
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => console.log(data))
.catch(err => console.error('请求失败', err));
坑2:跨域请求,Cookie 不带了
你以为设置了 withCredentials: true 就行?错了,后端也得配合。
前端:
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include' // 关键:带上 Cookie
})
后端(Node.js Express 示例):
app.use(cors({
origin: 'https://your-frontend.com', // 不能写 '*',否则 Cookie 不会携带
credentials: true
}));
如果你前端写了 credentials: 'include',但后端 CORS 配置是 Access-Control-Allow-Origin: *,浏览器会直接拦截,报 CORS 错误,而且不会返回任何响应体,你只会看到 Network 面板里状态是 (failed)。
坑3:没有超时?那我们用 AbortController 自己造一个
const controller = new AbortController();
const { signal } = controller;
// 设置 5 秒超时
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch('https://api.example.com/slow-endpoint', { signal })
.then(res => {
clearTimeout(timeoutId);
return res.json();
})
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.error('请求超时了');
} else {
console.error('其他错误', err);
}
});
解读:这是 Fetch 目前唯一的超时解决方案。代码有点啰嗦,但能用。如果你不想写这么多,建议还是换 Axios。
三、Axios 实战:看似简单,实则暗藏玄机
Axios 虽然好用,但也不是银弹。以下是我踩过的几个深坑。
坑1:Post 请求参数格式不对,后端收不到
很多新手这样写:
axios.post('/api/login', {
username: 'admin',
password: '123456'
});
然后后端报 undefined。为什么?因为 Axios 默认把请求体序列化为 JSON,但很多后端接口(尤其是老项目)期望的是 application/x-www-form-urlencoded 格式,也就是表单提交。
解决:
// 方案1:告诉后端我是 JSON(推荐,前后端约定好)
axios.post('/api/login', {
username: 'admin',
password: '123456'
}, {
headers: {
'Content-Type': 'application/json'
}
});
// 方案2:用 URLSearchParams 模拟表单提交
const params = new URLSearchParams();
params.append('username', 'admin');
params.append('password', '123456');
axios.post('/api/login', params);
教小朋友理解:这就好比你去寄快递,你填的是“电子表格式”(JSON),但收件方只收“纸质表格”(Form Data)。你得按对方的格式填,不然他们看不懂。
坑2:拦截器里 return 错了,导致整个请求链断裂
很多人喜欢用拦截器统一加 Token:
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 忘记 return config 了!
}, error => {
return Promise.reject(error);
});
后果:请求发出去了,但 Headers 里没带 Token,后端返回 401。你以为是自己 Token 过期了,查了半天,结果是因为你没 return config。
正确写法:
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config; // 别忘了 return!
}, error => {
return Promise.reject(error);
});
坑3:上传文件时,Content-Type 被自动设置了,导致上传失败
当你用 Axios 上传文件时,如果你不手动设置 Content-Type,Axios 会自动设为 multipart/form-data,这是对的。但如果你错误地手动设置了 Content-Type: multipart/form-data,浏览器会丢失 boundary 参数,导致后端解析失败。
正确写法:
const formData = new FormData();
formData.append('file', file);
formData.append('name', 'test.txt');
axios.post('/api/upload', formData, {
headers: {
// 不要手动设置 Content-Type,让浏览器自动设置,包括 boundary
// 'Content-Type': 'multipart/form-data' // 删掉这行!
}
});
教小朋友理解:这就像你寄一个包裹,快递员(浏览器)会自动贴一个包含地址信息的标签(boundary)。如果你自己手写这个标签,还写错了,快递员就不知道怎么送了。
四、跨域问题:终极Boss,必须面对
无论是 Fetch 还是 Axios,跨域都是绕不开的话题。先说结论:跨域是浏览器限制的,不是服务器限制的。服务器完全可以跨域发请求,是浏览器为了保护用户安全,阻止了前端 JS 读取跨域响应。
1. 浏览器是怎么判断跨域的?
只要协议、域名、端口任意一个不同,就是跨域。
http://a.com和https://a.com→ 跨域(协议不同)http://a.com和http://b.com→ 跨域(域名不同)http://a.com:80和http://a.com:8080→ 跨域(端口不同)
2. 常见的跨域解决方案
方案A:后端配 CORS(最推荐)
后端在响应头里加上:
Access-Control-Allow-Origin: https://your-frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
注意:如果后端支持 credentials: true,那 Access-Control-Allow-Origin 不能写 *,必须写具体的域名。
方案B:前端开发时配代理(Vite/Webpack)
如果你是本地开发,可以配一个 devServer 代理,把 /api 请求转发到后端。
Vite 配置:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
解读:这样你在前端请求 /api/login 时,浏览器以为是在同源请求,代理服务器会帮你转发到 https://api.example.com/login。生产环境还是得靠后端 CORS。
方案C:JSONP(古老但有效,只支持 GET)
JSONP 利用 <script> 标签不受跨域限制的特性,通过回调函数接收数据。
// 前端
function handleData(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = `https://api.example.com/data?callback=handleData`;
document.body.appendChild(script);
// 后端(Node.js)
app.get('/data', (req, res) => {
const callback = req.query.callback;
const data = { name: 'John' };
res.send(`${callback}(${JSON.stringify(data)})`);
});
现状:JSONP 现在基本被抛弃了,只有极少数老接口还在用。新项目别考虑。
五、错误处理:让程序优雅地报错
很多开发者写完请求后,就等着成功了,一旦失败,页面直接白屏或者崩溃。这是很不专业的。
1. 统一错误处理(Axios 示例)
// axiosInstance.js
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000
});
// 请求拦截器:加 Token
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一处理错误
instance.interceptors.response.use(
response => {
// 假设后端统一返回 { code: 200, data: {...} }
const { code, data, message } = response.data;
if (code === 200) {
return data;
} else {
// 业务错误
return Promise.reject(new Error(message || '未知错误'));
}
},
error => {
if (error.response) {
// 服务器返回了错误状态码
switch (error.response.status) {
case 401:
// 未授权,跳转登录
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 403:
console.error('权限不足');
break;
case 404:
console.error('资源不存在');
break;
case 500:
console.error('服务器内部错误');
break;
default:
console.error('其他错误', error.response.status);
}
} else if (error.request) {
// 请求发出去了,但没有收到响应(网络错误、超时)
console.error('网络错误,请检查网络连接');
} else {
// 请求配置出错
console.error('请求配置错误', error.message);
}
return Promise.reject(error);
}
);
export default instance;
使用:
// 在你调用接口的地方
import instance from './axiosInstance';
instance.get('/user/123')
.then(data => {
console.log('成功', data);
})
.catch(err => {
// 错误已经在拦截器里处理了,这里不需要再处理
console.log('兜底处理', err);
});
2. Fetch 的错误处理(更繁琐)
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
// 手动判断状态码
if (response.status === 401) {
throw new Error('Unauthorized');
} else if (response.status === 404) {
throw new Error('Not Found');
} else {
throw new Error(`HTTP ${response.status}`);
}
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
console.error('网络错误,请检查连接');
} else {
console.error('请求失败', error.message);
}
throw error; // 向上抛出,让调用者处理
}
}
六、性能优化:别让请求拖慢你的应用
1. 请求取消:防止内存泄漏和重复请求
用户在输入框快速搜索时,如果每次按键都发请求,会造成大量冗余请求。
Axios 取消请求:
import axios from 'axios';
let cancelToken;
function search(keyword) {
// 取消上一次未完成的请求
if (cancelToken) {
cancelToken.cancel('取消上一次请求');
}
cancelToken = axios.CancelToken.source();
axios.get(`/api/search?q=${keyword}`, {
cancelToken: cancelToken.token
})
.then(res => {
console.log(res.data);
})
.catch(err => {
if (axios.isCancel(err)) {
console.log('请求已取消', err.message);
} else {
console.error('搜索失败', err);
}
});
}
Fetch 取消请求:
let controller;
function search(keyword) {
// 取消上一次请求
if (controller) {
controller.abort();
}
controller = new AbortController();
const { signal } = controller;
fetch(`/api/search?q=${keyword}`, { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('搜索失败', err);
}
});
}
2. 请求防抖:减少无效请求
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
search(e.target.value);
}, 300));
解读:用户输入时,300ms 内不会发请求,只有用户停止输入 300ms 后,才发一次请求。这样能大大减少服务器压力。
七、常见陷阱总结表
| 问题 | Fetch | Axios | 解决方案 |
|---|
