嘿,开发界的萌新们,或者说是刚从前端坑里爬出来没多久的同学们,大家好呀!
今天咱们不聊虚的,就聊聊前端开发里最基础、也是最让人头秃的一个环节——网络请求。
你是不是也有过这种经历:明明代码写对了,接口也没报错,但数据就是不出来?或者调试的时候,浏览器控制台飘红,你一脸懵逼,完全不知道是哪里出了问题?
别担心,这不仅是你的问题,这是绝大多数前端同学必经的“渡劫”之路。在2024年的今天,虽然XMLHttpRequest已经被时代抛在脑后,但我们依然有fetch和axios这两个“大魔王”要对付。
这篇文章,我就把自己这些年踩过的坑、熬过的夜、喝过的咖啡,全部浓缩成这篇干货,带你彻底搞懂AJAX请求的来龙去脉。咱们就像聊天一样,把这事儿掰开了、揉碎了讲清楚。
一、 先别急着写代码,聊聊“HTTP请求”这个幕后黑手
首先,咱们得明白,你在浏览器里点一个按钮,或者页面一加载,背后发生了什么?
这就好比你去餐馆吃饭。
- 你(前端代码) 是顾客。
- 服务员(网络请求) 是你和厨房之间的桥梁。
- 厨房(后端服务器/API) 负责做菜。
- 菜(数据) 做好了,服务员端给你,你再端上桌(渲染页面)。
以前,服务员是个“老古董”,叫XMLHttpRequest(简称XHR)。这哥们儿有点古板,操作起来麻烦得要命,写一堆代码才能完成一个简单的“我要数据”的请求。
现在,我们有了两个更聪明的服务员:fetch 和 axios。
为什么我们还要学这个?
因为网络请求是前端与后端沟通的唯一桥梁。无论你做的是Vue、React还是Angular,只要你需要从服务器获取数据、提交表单、上传图片,你就离不开它。
所以,别嫌它烦,把它当成你的“老朋友”,好好相处。
二、 Fetch:浏览器亲儿子,原生却有点“野”
fetch是HTML5规范的一部分,由浏览器原生提供。这意味着你不需要安装任何第三方库,直接用就行。
1. 基本用法:简单粗暴
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('出错了:', error));
看,是不是比以前的XHR简洁多了?链式调用,看着就舒服。
2. 新手第一大坑:它不会“失败”!
这是fetch最容易被忽视、也最让人头疼的地方。
你知道吗? fetch默认只有在网络错误(比如断网、域名拼错)时才会reject,返回一个错误。如果服务器返回了404、500这些状态码,fetch依然会认为请求成功了,然后进入.then(),而不是.catch()。
这就像你去餐厅点菜,菜上错了(404),服务员还笑着说“您点的菜好了”,你都没察觉。
避坑指南:手动检查状态码
fetch('https://api.example.com/data')
.then(response => {
// 关键一步:手动检查 response.ok
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('出错了:', error));
记住这句话:fetch的成功不等于业务逻辑的成功。
3. 第二个坑:默认不携带Cookie
在很多项目中,我们需要登录状态(Session/Cookie)来验证用户身份。但fetch默认不会发送Cookie。
避坑指南:加上credentials选项
fetch('https://api.example.com/data', {
credentials: 'include' // 或者 'same-origin'
})
// ... 后续处理
'include':无论跨域与否,都携带Cookie。'same-origin':同源请求时携带Cookie(推荐)。'omit':不携带Cookie(默认值)。
4. 第三个坑:没有内置的请求超时控制
fetch本身没有timeout属性。如果你想实现超时,得借助AbortController,这对新手来说有点复杂。
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
console.log(data);
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.log('请求超时');
} else {
console.error('出错了:', error);
}
});
你看,就这么一个超时功能,代码量就上去了。这就是为什么很多人转投axios怀抱的原因。
三、 Axios:第三方库的优雅与强大
axios是一个基于Promise的HTTP库,可以用在浏览器和Node.js中。它不是浏览器原生的,需要安装:
npm install axios
# 或者
yarn add axios
# 或者
pnpm add axios
1. 基本用法:简洁优雅
import axios from 'axios';
axios.get('https://api.example.com/data')
.then(response => console.log(response.data))
.catch(error => console.error('出错了:', error));
是不是比fetch还简单?而且,axios会自动解析JSON,你直接拿到response.data,不用像fetch那样手动response.json()。
2. 新手第二大坑:响应结构不同
fetch返回的是Response对象,你需要手动提取数据。
axios返回的是包含data、status、headers等字段的对象,数据在response.data里。
避坑指南:记住response.data
很多新手刚用axios,会习惯性地写response.json(),结果报错“response.json is not a function”。记住,axios已经帮你解析好了,直接用response.data即可。
3. 第三个坑:拦截器的配置误区
axios最大的亮点是拦截器(Interceptors),可以在请求或响应发送/接收之前进行拦截处理。比如统一添加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);
});
// 响应拦截器
axios.interceptors.response.use(response => {
return response;
}, error => {
// 统一处理HTTP错误
if (error.response.status === 401) {
// 未授权,跳转到登录页
window.location.href = '/login';
}
return Promise.reject(error);
});
避坑指南:拦截器里的Promise.reject
在拦截器的错误处理函数中,必须返回Promise.reject(error),否则错误会被吞掉,外层.catch()就收不到了。这是新手常犯的错误。
4. 第四个坑:默认超时与取消请求
axios内置了timeout和CancelToken(新版本推荐用AbortController,但axios也兼容),用起来比fetch方便得多。
// 设置超时
axios.get('https://api.example.com/data', {
timeout: 5000 // 5秒
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已取消');
} else {
console.error('出错了:', error);
}
});
// 取消请求
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('https://api.example.com/data', {
cancelToken: source.token
});
// 在某个时机取消请求
source.cancel('请求已取消');
5. 第五个坑:类型安全(TypeScript用户注意)
如果你用TypeScript,axios的类型定义是内置的,但有时会遇到泛型推断不友好的问题。
避坑指南:善用泛型
interface UserData {
id: number;
name: string;
email: string;
}
axios.get<UserData>('https://api.example.com/user/1')
.then(response => {
console.log(response.data.name); // TypeScript能正确推断出name是string
});
这样写,代码提示和类型检查都会更友好。
四、 Fetch vs Axios:到底选谁?
好,现在你俩可能懵了:到底用哪个?
咱不扯虚的,直接上对比表格,让你一目了然。
| 特性 | Fetch | Axios |
|---|---|---|
| 原生支持 | ✅ 浏览器原生 | ❌ 需安装第三方库 |
| 自动JSON解析 | ❌ 需手动response.json() |
✅ 自动解析,直接取data |
| 错误处理 | ❌ 4xx/5xx不reject,需手动检查 | ✅ 所有HTTP错误都会reject |
| 请求拦截器 | ❌ 无 | ✅ 支持 |
| 响应拦截器 | ❌ 无 | ✅ 支持 |
| 超时控制 | ❌ 需手动实现 | ✅ 内置timeout选项 |
| 取消请求 | ❌ 需AbortController |
✅ 内置CancelToken(推荐AbortController) |
| Cookie携带 | ❌ 默认不携带,需手动配置 | ✅ 默认携带(同源) |
| 文件大小 | ✅ 无额外体积 | ❌ 增加约10KB(gzip后更小) |
| 浏览器兼容性 | ✅ 现代浏览器 | ✅ 需polyfill支持IE |
怎么选?听我一句劝:
如果你是新手,或者项目比较小:
- 直接用
fetch。原生、无依赖、学习成本低,能帮你理解HTTP请求的本质。 - 但务必记住那两个坑:手动检查状态码和携带Cookie。
- 直接用
如果你是中大型项目,或者用Vue/React框架:
- 直接用
axios。拦截器、超时控制、错误处理这些“高级功能”能帮你省很多事,代码也更整洁。 - 尤其是在企业级开发中,
axios几乎是标配。
- 直接用
如果你追求极致性能,且不需要太多高级功能:
- 可以考虑
fetch,或者更轻量的库如ky。
- 可以考虑
如果你在维护老项目:
- 可能还在用
XMLHttpRequest,别慌,慢慢过渡到fetch或axios。
- 可能还在用
五、 实战案例:手把手带你写一个完整请求
光说不练假把式。现在,我来带你写一个完整的、健壮的请求模块,覆盖之前提到的所有坑。
场景:用户登录
假设我们要实现一个用户登录功能,需要:
- 发送POST请求到登录接口。
- 携带用户名和密码。
- 自动添加Token到后续请求。
- 统一处理错误(401跳转登录页,500提示服务器错误)。
- 设置超时。
- 支持取消请求。
方案一:用Fetch实现
// api.js
const BASE_URL = 'https://api.example.com';
// 统一请求封装
async function request(url, options = {}) {
const defaultOptions = {
credentials: 'include', // 携带Cookie
headers: {
'Content-Type': 'application/json'
}
};
// 合并默认选项和传入选项
const config = { ...defaultOptions, ...options };
// 如果有Token,添加到Header
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10秒超时
try {
const response = await fetch(BASE_URL + url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
// 手动检查状态码
if (!response.ok) {
if (response.status === 401) {
// 未授权,清除Token并跳转
localStorage.removeItem('token');
window.location.href = '/login';
throw new Error('未授权,请重新登录');
} else if (response.status === 500) {
throw new Error('服务器错误,请稍后重试');
} else {
throw new Error(`HTTP错误: ${response.status}`);
}
}
// 手动解析JSON
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
}
// 导出GET和POST方法
export const get = (url, options) => request(url, { ...options, method: 'GET' });
export const post = (url, data, options) => request(url, { ...options, method: 'POST', body: JSON.stringify(data) });
使用示例:
// login.js
import { post } from './api';
async function login(username, password) {
try {
const result = await post('/login', { username, password });
console.log('登录成功:', result);
localStorage.setItem('token', result.token);
} catch (error) {
console.error('登录失败:', error.message);
}
}
login('myuser', 'mypassword');
方案二:用Axios实现
// api.js
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000, // 10秒超时
withCredentials: true // 携带Cookie
});
// 请求拦截器
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 => {
return response.data; // 直接返回data,不用每处都写response.data
}, error => {
if (error.response) {
switch (error.response.status) {
case 401:
localStorage.removeItem('token');
window.location.href = '/login';
break;
case 500:
break;
default:
break;
}
}
return Promise.reject(error);
});
export default instance;
使用示例:
// login.js
import api from './api';
async function login(username, password) {
try {
const result = await api.post('/login', { username, password });
console.log('登录成功:', result);
localStorage.setItem('token', result.token);
} catch (error) {
console.error('登录失败:', error.message);
}
}
login('myuser', 'mypassword');
对比一下:
fetch版本需要手动处理超时、错误码、JSON解析,代码量大,但逻辑清晰。axios版本通过拦截器统一处理,代码简洁,复用性强。
你看,这就是两者的区别。fetch更像是一块生铁,你需要自己打磨;axios像是半成品,你拿来就能用。
六、 那些容易被忽视的“隐形坑”
除了上面说的大坑,还有一些小坑,虽然不是致命,但处理不好会很烦人。
1. 跨域问题(CORS)
这是前端开发的“老朋友”了。当你从一个域名访问另一个域名的API时,浏览器会拦截请求,除非服务器配置了CORS头。
避坑指南:
- 开发环境:用Vite、Webpack等工具的代理配置,绕过浏览器跨域限制。
// Vite配置 export default defineConfig({ server: { proxy: { '/api': 'http://localhost:3000' } } }); - 生产环境:让后端配置CORS头,或者用Nginx反向代理。
2. 大文件上传
fetch和axios都支持FormData,但处理大文件时需要注意进度回调。
避坑指南:用axios的onUploadProgress
axios.post('/upload', formData, {
onUploadProgress: progressEvent => {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度: ${percent}%`);
}
});
fetch没有内置的上传进度回调,需要自己监听Body的写入流,比较复杂。
3. 重试机制
网络不稳定时,请求可能失败。有时我们需要自动重试。
避坑指南:简单重试逻辑
”`javascript // 用axios封装重试 async function requestWithRetry(url, options, retries = 3) { for (let i = 0; i < retries; i++) {
try {
return await axios(url, options);
} catch (error) {
if (i === retries -
