
一、先搞清楚:浏览器到底在干什么
你打开一个网页,发生了什么?
你的浏览器向一台远程服务器说:"给我那个页面。" 服务器把 HTML 扔回来。你点了张图片,浏览器又说:"给我那张图。" 服务器把图片字节扔过来。你提交了表单,浏览器说:"这是用户填的数据,存一下。" 服务器说:"存好了。"
整个过程就是一件事:发请求,等回应。
这就像你往邮局投信——你写好地址和内容,投进信箱,然后等回信。只不过这里邮递员是光速的,信箱是 TCP 连接,而"信"的格式是 HTTP 协议。
从 1991 年开始,浏览器就在干这件事。问题是:浏览器用什么"语言"让 JavaScript 去发这个请求?
二、前任:XMLHttpRequest(XHR)
2000 年左右,微软发明了一个东西叫 XMLHttpRequest。名字里带着"XML",但实际上什么格式都能传——JSON、文本、二进制都行。Google 用它在 Gmail 里实现了"无刷新更新",从此 Ajax 这个词火了。
它长这样:
// 经典 XHR 写法
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // 4 = 请求完成
if (xhr.status === 200) { // 200 = 成功
var users = JSON.parse(xhr.responseText);
console.log(users);
} else {
console.log('出错了: ' + xhr.status);
}
}
};
xhr.send();
你看这段代码——你得先 new 一个对象,然后 open,然后监听 onreadystatechange,然后在回调里判断 readyState === 4 和 status === 200,最后 send。
这东西的设计哲学是事件驱动的:状态从 0 变到 1,1 变到 2,2 变到 3,3 变到 4。你像在监听一台机器的仪表盘,每次指针动了你得自己判断"现在该干嘛了"。
它管用吗?管用。它丑吗?丑得让人想骂人。
问题在于:HTTP 请求本质上是一个异步操作——你发出去,等一会儿,结果回来。这是 Promise 的天然场景。但 XHR 诞生于 Promise 出现之前,所以它用了回调函数和状态机来表达这件事。这不是 XHR 的错,是时代的局限。
三、Fetch 登场:用 Promise 重新表达同一件事
2015 年,Fetch API 被正式纳入浏览器标准。核心思想一句话:
HTTP 请求是异步操作 → 用 Promise 表达它。
fetch('/api/users')
.then(res => res.json())
.then(users => console.log(users))
.catch(err => console.error(err));
一个函数调用,返回一个 Promise。你链式处理:先拿到响应对象,再把它解析成 JSON,然后处理数据。没有状态码要记,没有回调要嵌套。
用 async/await 写更直观:
async function getUsers() {
try {
const res = await fetch('/api/users');
const users = await res.json();
console.log(users);
} catch (err) {
console.error(err);
}
}
读起来就像同步代码——"等一下请求结果,等一下 JSON 解析,打印出来"。这就是 Fetch 的魅力。
四、实际用法:从简单到复杂
案例 1:GET 请求——获取数据
最简单的场景:从服务器拿一个用户列表。
async function fetchUsers() {
const res = await fetch('https://api.example.com/users');
const users = await res.json();
console.log(users);
// [{ id: 1, name: '前端王睿' }, { id: 2, name: '爱因斯坦' }, ...]
}
案例 2:带查询参数
URL 里拼参数就行,但手动拼容易出错:
// 不推荐:手动拼接
fetch('/api/users?name=前端王睿&age=18')
// 推荐:用 URLSearchParams
const params = new URLSearchParams({
name: '前端王睿',
age: 18
});
const res = await fetch(`/api/users?${params}`);
案例 3:POST 请求——发送 JSON 数据
async function createUser() {
const res = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '前端王睿',
age: 18,
title: '前端开发工程师'
})
});
const data = await res.json();
console.log('创建成功:', data);
}
案例 4:上传文件
async function uploadFile(fileInput) {
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
formData.append('userId', '42');
const res = await fetch('/api/upload', {
method: 'POST',
body: formData // 不要手动设 Content-Type!浏览器会自动设
});
console.log('上传成功:', await res.json());
}
注意:上传文件时千万不要手动设
Content-Type: multipart/form-data。浏览器需要自动生成 boundary 分隔符,你手动设了反而会出错。这是 Fetch 的一个"坑"——它不会帮你纠正这个错误。
案例 5:带认证 Token
const token = localStorage.getItem('auth_token');
const res = await fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
案例 6:设置超时(Fetch 原生没有!)
Fetch 没有内置超时功能。如果服务器卡住了,你的请求会永远等下去。你得用 AbortController:
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`请求超时(${timeout}ms)`);
}
throw err;
} finally {
clearTimeout(timeoutId);
}
}
// 使用
const data = await fetchWithTimeout('/api/slow-endpoint', 5000);
案例 7:处理 HTTP 错误状态码
这是 Fetch 最让人困惑的设计——404、500 不会触发 catch!
// ❌ 你以为这样能捕获 404 错误,实际上不能
try {
const res = await fetch('/api/missing'); // 服务器返回 404
const data = await res.json();
console.log(data);
} catch (err) {
console.log('出错了'); // 这里不会执行!
}
Fetch 认为:请求发出去了,服务器也回应了——"请求成功了"。至于服务器回应的内容是"404 Not Found",那是业务层面的成功,不是网络层面的失败。
所以你必须自己检查 res.ok:
// ✅ 正确做法
async function safeFetch(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
return res.json();
}
try {
const data = await safeFetch('/api/users');
console.log(data);
} catch (err) {
console.error(err.message); // "HTTP 404: Not Found"
}
这就像你问邮递员信送到了没有,他说送到了——但信的内容写着"房子着火了"。邮递员觉得他的任务完成了,因为信确实送到了。至于信的内容好不好,那不是他管的事。
五、那为什么大家还在用 axios?
好了,现在到了核心问题。Fetch 能用,为什么大家还在装 axios?
因为 Fetch 是积木,axios 是工具。 注意:axios 底层不是 Fetch,而是 XMLHttpRequest。两者是各自独立的封装路线,但 axios 解决的是同一类问题——原生 API 太简陋,开发者需要更多开箱即用的功能。
Fetch 的设计哲学是:我只提供最基础的 HTTP 操作能力。超时、拦截器、自动 JSON 转换、错误处理——这些"实际开发中你每天都要做的事",Fetch 不管。它管不管?不管。你自己写。
axios 把这些事情全帮你做了。让我逐个对比:
差异 1:错误处理
Fetch(需要手动检查):
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
axios(自动处理):
const { data } = await axios.get('/api/users');
// 状态码 >= 400 自动 throw,直接进 catch
差异 2:JSON 转换
Fetch(每次都要手动):
const res = await fetch(url);
const data = await res.json(); // 多一行
axios(自动解析):
const { data } = await axios.get(url); // 直接给你数据
差异 3:超时
Fetch(15 行代码):
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
// ...
} finally {
clearTimeout(timeoutId);
}
axios(一行):
const data = await axios.get(url, { timeout: 5000 });
差异 4:拦截器——真正的杀手锏
假设你有 50 个 API 调用,每个都需要带 token,每个都需要统一错误处理。
用 Fetch,你得自己封装一个 wrapper,或者在每个调用处重复写:
// 每个 API 文件都得这样写
const token = localStorage.getItem('token');
const res = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) { /* 处理错误 */ }
用 axios,你只需配置一次拦截器:
// 配置一次,全局生效
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(
res => res,
err => {
if (err.response.status === 401) {
// token 过期,自动跳转登录
window.location.href = '/login';
}
return Promise.reject(err);
}
);
之后你写的每一个 axios.get()、axios.post() 都自动带 token,自动处理 401。写一次,管全局。
差异 5:请求/响应进度
上传大文件时,你想显示进度条。
Fetch:几乎做不到(技术上可以用 ReadableStream,但极其复杂)。
axios:
await axios.post('/upload', formData, {
onUploadProgress: (event) => {
const percent = Math.round((event.loaded / event.total) * 100);
console.log(`上传进度: ${percent}%`);
}
});
六、一张表总结全部差异
| 特性 | Fetch | axios |
|---|---|---|
| 来源 | 浏览器原生 | 第三方库(~13KB) |
| 底层 | Promise | Promise |
| JSON 自动解析 | 不自动,需 res.json()
|
自动 |
| HTTP 错误状态码 | 不 throw,需手动检查 res.ok
|
自动 throw |
| 超时 | 需手动用 AbortController |
{ timeout: 5000 } 一行搞定 |
| 请求拦截器 | 没有 | 有 |
| 响应拦截器 | 没有 | 有 |
| 上传/下载进度 | 极难实现 | 内置 onUploadProgress
|
| 取消请求 | AbortController(现代浏览器) | 内置 |
| Node.js 支持 | Node 18+ 原生支持 | 通用 |
| 学习成本 | 低(基础简单,进阶复杂) | 低 |
七、实际场景:你应该用哪个?
我不会给你一个简单的答案——因为真正的答案取决于你在做什么。
场景 A:个人小项目 / 脚本
你写一个 Chrome 扩展,或者一个只有两三个 API 调用的小页面。
用 Fetch。 没必要为了两三个请求引入一个库。写个薄薄的 wrapper 就够了:
async function api(url, options = {}) {
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
20 行代码,覆盖 80% 的场景。
场景 B:正经的前端应用
你在做一个有几十个 API 调用的应用,需要统一加 token、统一错误处理、统一超时。
用 axios。 或者用基于 Fetch 封装的库(如 ky、wretch、ofetch)。
核心不是 Fetch 不好,而是你不想在每个 API 文件里都重复写那 5 行样板代码。拦截器帮你把这些重复逻辑集中到一处。
场景 C:需要上传进度条 / 大文件传输
用 axios。 Fetch 在进度回调方面几乎无能为力。
场景 D:包体积极其敏感
你在做移动端 H5,每个 KB 都要算。或者你在做一个 SDK。
用 Fetch。 axios.min.js 约 13KB,Fetch 是 0KB。13KB 在移动端可能意味着 200ms 的加载延迟。
场景 E:Node.js 服务端
你写一个同时跑在浏览器和 Node.js 的同构应用。
用 axios。 虽然 Node 18+ 原生支持 Fetch,但 axios 的 API 在两个环境完全一致,而 Fetch 在 Node 里没有 window,可能有一些细微差异。
八、如果你决定用 Fetch,这是一个能用的封装
class HttpClient {
constructor(baseURL = '', defaultOptions = {}) {
this.baseURL = baseURL;
this.defaultOptions = defaultOptions;
}
async request(url, options = {}) {
const finalUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
const finalOptions = {
...this.defaultOptions,
...options,
headers: {
'Content-Type': 'application/json',
...this.defaultOptions.headers,
...options.headers
}
};
// 超时处理
if (finalOptions.timeout) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), finalOptions.timeout);
finalOptions.signal = controller.signal;
delete finalOptions.timeout;
try {
return await this._doRequest(finalUrl, finalOptions);
} finally {
clearTimeout(timeoutId);
}
}
return this._doRequest(finalUrl, finalOptions);
}
async _doRequest(url, options) {
const res = await fetch(url, options);
if (!res.ok) {
const errorBody = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${res.statusText}\n${errorBody}`);
}
const contentType = res.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return res.json();
}
return res.text();
}
get(url, options) {
return this.request(url, { ...options, method: 'GET' });
}
post(url, data, options) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
put(url, data, options) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(url, options) {
return this.request(url, { ...options, method: 'DELETE' });
}
}
用法:
const api = new HttpClient('https://api.example.com', {
headers: { 'Authorization': 'Bearer my-token' }
});
// GET
const users = await api.get('/users');
// POST
const newUser = await api.post('/users', { name: '前端王睿' });
// 带超时
const data = await api.get('/slow-endpoint', { timeout: 5000 });
九、总结
Fetch 是浏览器给你的原生能力。它简单、轻量、无处不在。
axios 是社区基于实际痛点做的封装。它底层用 XMLHttpRequest,补上了原生 API 没帮你做的事——错误处理、超时、拦截器、自动 JSON 转换。这些功能 Fetch 也没有,XHR 也没有,axios 把它们全包了。
理解 Fetch,你才理解 axios 在帮你做什么。 不理解 Fetch 直接用 axios,那你只是在用你不懂的工具。
从第一性原理出发:HTTP 请求是"发请求、等回应"。Fetch 用 Promise 表达了这件事。axios 则在 XMLHttpRequest 的基础上做了封装,补上了实际开发中每天都要用的功能——错误处理、超时、拦截器、自动 JSON 转换。
这不是谁替代谁的问题。这是不同层次的抽象。
如果你连 Fetch 的底层行为都没搞清楚就用了 axios,你就是在骗自己。反之,如果你理解了 Fetch 还选择用 axios,那是你做了一个明智的决定。