嘿,朋友。如果你正在写前端代码,那“发请求”肯定是你每天干得最多的事。以前我们靠 XMLHttpRequest (XHR) 摸爬滚打,后来有了 jQuery 简化了这一切,现在呢?fetch 成了原生标配,axios 成了第三方库里的霸主。
但这里有个坑:很多人以为换了工具就万事大吉,结果线上 Bug 不断,或者代码难维护得像一团乱麻。 今天咱们不聊那些枯燥的定义,我就用这几年的实战经验,带你把 AJAX 的请求方式扒开揉碎了看,顺便把那些让人头大的性能和错误处理问题也一并解决掉。
先聊聊:为什么我们还在纠结“请求方法”?
你可能会问:“不就是 axios.get 吗?有什么好讲的?”
问题在于,场景变了。
几年前,我们主要做的是后端渲染为主的页面,请求简单,错误了刷新页面就行。但现在,无论是 Vue、React 还是 Svelte,前端应用越来越复杂,接口调用频繁、并发多、取消请求、拦截器、加载状态管理……这些需求让一个简单的“发请求”变成了系统工程。
选错了工具,或者用错了姿势,轻则用户体验卡顿,重则内存泄漏、页面崩溃。所以,搞清楚 fetch 和 axios 的本质区别,以及在不同场景下如何优雅地封装,才是正经事。
一、原生派:fetch API 详解
fetch 是浏览器原生的 API,基于 Promise。它的出现是为了替代那个古老又丑陋的 XMLHttpRequest。
1. 基本用法
fetch('/api/user/profile')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('User:', data);
})
.catch(error => {
console.error('Fetch error:', error);
});
注意这个细节: fetch 只有当网络故障(如 DNS 解析失败、断网)时才会 reject。如果服务器返回 404 或 500,fetch 依然会 resolve,并且你需要手动检查 response.ok 或者 response.status。这一点经常让新手踩坑。
2. 发送 POST 请求
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 如果你有 token,记得加在这里
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: '张三',
age: 25
})
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
3. 取消请求:AbortController
这是 fetch 相比旧 XHR 的一大进步。以前想取消请求?没门。现在可以用 AbortController。
const controller = new AbortController();
const signal = controller.signal;
const request = fetch('/api/long-data', { signal })
.then(res => res.json())
.then(data => console.log(data));
// 比如用户点击了“取消”按钮,或者组件卸载了
button.onclick = () => {
controller.abort();
};
request.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
为什么这很重要? 想象一下,用户在搜索框输入内容,每次按键都发请求。如果他输入“javascript”,我们会发出 10 多个请求。如果不取消之前的请求,最后一个响应的数据可能覆盖前面的,导致显示错乱,还浪费带宽。
二、第三方派:axios 的优势在哪?
axios 不是浏览器原生的,它需要引入。但它之所以能火,是因为它解决了很多 fetch 的痛点。
1. 自动 JSON 转换
用 fetch 时,你得手动 .json()。axios 会自动把响应体解析成 JSON。
axios.get('/api/user')
.then(response => {
console.log(response.data); // 直接拿到对象,不用 .json()
});
2. 请求/响应拦截器
这是 axios 最强大的功能之一。你可以在请求发出前或响应回来后,统一处理一些逻辑,比如加 Token、统一错误处理、显示 Loading 等。
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});
// 请求拦截器
instance.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
response => {
// 统一处理业务状态码
if (response.data.code !== 200) {
return Promise.reject(new Error(response.data.message));
}
return response.data;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// 跳登录页
window.location.href = '/login';
break;
case 403:
console.log('无权限');
break;
default:
console.log('其他错误');
}
}
return Promise.reject(error);
}
);
你看,有了拦截器,你的每个业务代码里就不需要重复写这些逻辑了。
3. 更好的错误处理
axios 会抛出 HTTP 状态码相关的错误,比如 404、500,它会直接 reject,不需要你手动判断 response.ok。
三、fetch vs axios:核心对比
| 特性 | fetch |
axios |
|---|---|---|
| 原生支持 | ✅ 浏览器内置 | ❌ 需引入 |
| 自动 JSON 解析 | ❌ 需手动 .json() |
✅ 自动 |
| 请求拦截 | ❌ 无原生支持 | ✅ 强大拦截器 |
| 响应拦截 | ❌ 无原生支持 | ✅ 强大拦截器 |
| 取消请求 | ✅ AbortController |
✅ CancelToken (旧) / AbortController (新) |
| 浏览器兼容性 | 现代浏览器 | 需要 Polyfill (IE 不支持) |
| 文件大小 | 0KB | ~13KB (gzipped) |
| 类型支持 | 需手动定义 | 原生 TypeScript 支持好 |
我的建议:
- 如果你在做轻量级项目,或者不想引入额外依赖,用
fetch。 - 如果你在做中大型应用,需要统一拦截、复杂错误处理、Token 管理,用
axios。 - 现代前端工程化(Vue/React 项目),几乎都用
axios,因为它的封装能力太强了。
四、实战:如何封装一个健壮的请求模块
不管用 fetch 还是 axios,直接在组件里写请求代码是大忌。代码会重复,错误处理会混乱。我们需要封装。
下面是一个基于 axios 的完整封装示例,你可以直接拿去用。
1. 创建 axios 实例
// utils/request.js
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // Vite 环境变量
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
request.interceptors.request.use(
config => {
// 添加 Token
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加请求时间戳,用于去重或日志
config.metadata = { startTime: Date.now() };
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => {
const { data } = response;
// 假设后端统一返回 { code: 200, data: ..., msg: 'success' }
if (data.code === 200) {
return data;
}
// 业务错误
throw new Error(data.msg || '请求失败');
},
error => {
const { response } = error;
if (response) {
switch (response.status) {
case 401:
// 未授权,跳转登录
localStorage.removeItem('auth_token');
window.location.href = '/login';
break;
case 403:
alert('您没有权限访问此资源');
break;
case 404:
console.error('接口不存在');
break;
case 500:
console.error('服务器错误');
break;
default:
console.error('网络错误', response.status);
}
} else if (error.code === 'ERR_CANCELED') {
console.log('请求已取消');
} else {
console.error('未知错误', error.message);
}
return Promise.reject(error);
}
);
export default request;
2. 封装 API 模块
// apis/user.js
import request from '@/utils/request';
export function getUserProfile() {
return request.get('/user/profile');
}
export function updateUserProfile(data) {
return request.put('/user/profile', data);
}
export function searchUsers(keyword) {
return request.get('/user/search', { params: { keyword } });
}
3. 在组件中使用
<!-- Vue 示例 -->
<script setup>
import { ref, onMounted } from 'vue';
import { getUserProfile, searchUsers } from '@/apis/user';
const user = ref(null);
const searchResult = ref([]);
onMounted(async () => {
try {
const res = await getUserProfile();
user.value = res.data;
} catch (error) {
// 拦截器已经处理了大部分错误,这里可以补充组件级的错误提示
console.log('获取用户信息失败');
}
});
async function handleSearch(keyword) {
const res = await searchUsers(keyword);
searchResult.value = res.data.list;
}
</script>
你看,组件里干净得像什么都没发生过。 这就是封装的价值。
五、常见问题与“坑”
1. 跨域问题(CORS)
前端开发最头疼的问题之一。记住:CORS 是后端配置的问题,前端只能想办法规避或提醒后端。
- 开发环境:使用 Vite/Webpack 的代理配置。
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
};
- 生产环境:联系后端配置 Nginx 反向代理,或在服务器端设置
Access-Control-Allow-Origin头。
2. 重复请求
用户快速点击按钮,或者搜索框快速输入,会导致多个请求并发。如果顺序乱了,数据显示就会错。
解决方案:请求去重
// 简单的请求缓存/去重示例
const pendingRequests = new Map();
request.interceptors.request.use(config => {
const key = config.url + JSON.stringify(config.params);
// 如果已有相同请求,取消它
if (pendingRequests.has(key)) {
const controller = pendingRequests.get(key);
controller.abort();
pendingRequests.delete(key);
}
// 创建新的 AbortController
const controller = new AbortController();
config.signal = controller.signal;
pendingRequests.set(key, controller);
return config;
});
request.interceptors.response.use(
response => {
const key = response.config.url + JSON.stringify(response.config.params);
pendingRequests.delete(key);
return response;
},
error => {
const key = error.config?.url + JSON.stringify(error.config?.params);
if (key && pendingRequests.has(key)) {
pendingRequests.delete(key);
}
return Promise.reject(error);
}
);
3. 文件上传
fetch 和 axios 都支持 FormData,但要注意 Content-Type。
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('title', '我的图片');
axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
// 不要手动设置 Content-Type,浏览器会自动加上 boundary
},
onUploadProgress: progressEvent => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`上传进度: ${percent}%`);
}
});
注意: axios 的 onUploadProgress 是原生支持的,fetch 没有这个功能,你需要自己实现或者用第三方库。
六、性能优化技巧
请求发得快,不代表用户体验好。以下是几个关键的优化点:
1. 请求合并(Debouncing)
搜索框是最典型的场景。用户输入“javascript”,会触发 10 次请求。我们用防抖(Debounce)来减少请求次数。
import { debounce } from 'lodash-es';
const searchUsers = debounce(async (keyword) => {
const res = await getUser(keyword);
result.value = res.data;
}, 300); // 300ms 内只发最后一次请求
2. 预加载(Prefetching)
在用户可能需要的地方,提前发出请求。比如,鼠标悬停在链接上时,预加载目标页面的数据。
// 简单预加载示例
const prefetchLink = document.querySelector('#preview-link');
prefetchLink.addEventListener('mouseover', () => {
fetch('/api/preload-data').then(res => res.json()).then(data => {
window.__preloadData = data;
});
});
3. 请求取消(Cancellation)
如前所述,使用 AbortController 取消不再需要的请求。这对于单页应用(SPA)的路由跳转非常有用。
// 在 Vue 的 onUnmounted 中取消请求
import { onUnmounted } from 'vue';
let controller;
function fetchData() {
controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => {
// 处理数据
});
}
onUnmounted(() => {
if (controller) {
controller.abort();
}
});
4. 缓存策略
对于不常变化的数据,使用缓存。可以简单地用内存缓存,也可以结合浏览器缓存(cache 属性)。
// 使用浏览器缓存
fetch('/api/static-data', {
cache: 'force-cache' // 优先使用缓存
});
// 或者自己实现内存缓存
const cache = new Map();
async function cachedFetch(url) {
if (cache.has(url)) {
const { data, timestamp } = cache.get(url);
if (Date.now() - timestamp < 5 * 60 * 1000) { // 5分钟缓存
return data;
}
}
const res = await fetch(url);
const data = await res.json();
cache.set(url, { data, timestamp: Date.now() });
return data;
}
5. 并行与串行
根据业务逻辑选择 Promise.all 还是串行请求。
// 并行:多个独立请求同时发出
const [user, orders, settings] = await Promise.all([
fetch('/api/user').then(res => res.json()),
fetch('/api/orders').then(res => res.json()),
fetch('/api/settings').then(res => res.json())
]);
// 串行:依赖前一个请求的结果
const user = await fetch('/api/user').then(res => res.json());
const profile = await fetch(`/api/profile/${user.id}`).then(res => res.json());
注意: 浏览器对同一域名的并发请求数量有限制(通常是 6 个)。太多并发请求会导致排队,反而降低性能。
七、给小朋友也能听懂的比喻
如果上面的技术术语让你头晕,咱们换个说法。
想象你在餐厅吃饭:
fetch就像是你自己站起来,走到服务员那里,说:“我要点菜。” 然后你等,等服务员回来,你自己检查菜对不对。如果服务员没回来(网络超时),你得自己判断怎么办。如果服务员说“做不了”(404),你得自己处理。axios就像是有个专属管家。你告诉管家:“帮我点菜,要加辣,还要记得带上我的会员卡。” 管家会统一处理所有细节:加 Token(会员卡)、处理错误(如果菜做不了,管家会告诉你原因)、自动把菜端到你面前(自动解析 JSON)。你只需要坐着吃就行。拦截器 就像管家的工作流程。每个请求出去前,管家都会检查一遍(加 Token);每个请求回来时,管家
