现代前端应用几乎无时无刻不在与服务器通信:拉取列表、提交表单、上传文件、轮询状态、订阅消息……这一切的背后,都是异步编程在支撑。而发起 HTTP 请求的两大主力——原生 Fetch API 与社区经典 Axios,几乎是每一位前端工程师绕不开的必修课。本文将带你从异步编程的演进讲起,深入 Fetch 的请求配置、Response 解析、错误处理、取消与超时,再系统梳理 Axios 的实例、拦截器、错误对象与取消机制,最后通过横向对比与实战封装,帮助你彻底掌握两大请求方案,写出健壮、可维护、高性能的网络层代码。40 分钟沉浸式学习,从"会用"到"用对",再到"用精"。
异步编程演进:从回调到 async/await
理解 Fetch 与 Axios,必须先理解 JavaScript 的异步模型。浏览器是单线程的,网络请求天然异步,于是我们经历了回调函数、Promise、Generator、async/await 四个阶段。
getUser(function(user) {
getOrders(user.id, function(orders) {
getDetail(orders[0].id, function(detail) {
// 层层嵌套,难以维护
});
});
});
/* 2. Promise 链式调用 */
getUser()
.then(user => getOrders(user.id))
.then(orders => getDetail(orders[0].id))
.then(detail => console.log(detail))
.catch(err => console.error(err))
.finally(() => hideLoading());
/* 3. async/await:同步写法,异步执行 */
async function load() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
return await getDetail(orders[0].id);
} catch (err) {
console.error('加载失败', err);
}
}
每一次演进,都在解决上一次的痛点
Fetch API 快速入门
fetch() 是浏览器原生提供的全局函数,基于 Promise 设计,用于替代老旧的 XMLHttpRequest。它的基本调用非常简单。
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data));
/* async/await 写法(推荐) */
async function getUsers() {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('HTTP ' + res.status);
return await res.json();
}
/* POST 提交 JSON */
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '张玥', age: 28 })
});
const created = await res.json();
fetch 默认使用 GET 方法
深入 Response 对象
fetch() 返回的是一个 Response 对象,它并不直接包含数据,而是包含响应元信息与一个可读流。理解它,是正确使用 Fetch 的关键。
res.ok // true 表示状态码 200-299
res.status // 200 / 404 / 500 ...
res.statusText // "OK" / "Not Found"
res.url // 最终请求地址(含重定向)
res.redirected // 是否发生过重定向
res.type // basic / cors / opaque
res.headers // Headers 实例
/* 读取响应头 */
res.headers.get('Content-Type');
res.headers.get('X-Total-Count');
/* 五种读取 body 的方式(只能选其一) */
await res.json(); // JSON → 对象
await res.text(); // 纯文本
await res.blob(); // 二进制(图片/文件)
await res.arrayBuffer(); // 原始二进制缓冲
await res.formData(); // 表单数据
├── ok: true
├── headers: Headers
├── url: string
└── body: ReadableStream
Fetch 请求配置全解析
fetch(url, options) 的第二个参数是一个配置对象,掌握它才能发挥 Fetch 的全部能力。
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify(payload),
mode: 'cors', // cors / no-cors / same-origin
credentials: 'include', // omit / same-origin / include
cache: 'no-cache', // default / no-store / reload ...
redirect: 'follow', // follow / error / manual
referrerPolicy: 'no-referrer',
integrity: 'sha384-...', // SRI 子资源完整性
keepalive: true, // 页面卸载后仍发送
signal: controller.signal
});
请求方法
请求头
请求体
携带 Cookie
缓存策略
取消信号
跨域携带 Cookie 必须同时设置 credentials 与服务端 CORS 响应头
错误处理:Fetch 最大的"坑"
与很多人直觉相反,fetch 在遇到 404、500 时不会 reject。只有当网络中断、DNS 失败、请求被拦截时才会 reject。因此必须手动判断 res.ok。
fetch('/api/not-exist')
.then(res => res.json())
.then(data => console.log('成功', data))
.catch(err => console.log('失败', err));
/* ✅ 正确做法:显式检查 ok */
async function request(url, options) {
let res;
try {
res = await fetch(url, options);
} catch (err) {
// 网络层错误:断网、DNS、CORS 被拒
throw new Error('网络异常,请检查连接');
}
if (!res.ok) {
// HTTP 层错误:4xx / 5xx
const message = await res.text();
throw new Error(`HTTP ${res.status}: ${message}`);
}
const type = res.headers.get('content-type');
return type?.includes('application/json') ? res.json() : res.text();
}
请求取消与超时控制
Fetch 本身没有 timeout 配置项,也没有 cancel 方法。现代方案统一使用 AbortController 来中止请求。
const controller = new AbortController();
fetch('/api/slow', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
controller.abort(); // 立即中止
/* 超时控制:AbortSignal.timeout(现代浏览器) */
const res = await fetch('/api/slow', {
signal: AbortSignal.timeout(5000)
});
/* 兼容写法:手动 setTimeout */
function fetchWithTimeout(url, options = {}, ms = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timer));
}
signal.aborted 可查询状态Axios 快速入门
Axios 是一个基于 Promise 的 HTTP 客户端,可在浏览器与 Node.js 中运行。它自动转换 JSON、支持拦截器、超时、取消、进度回调,是很多团队的首选。
// npm install axios
/* GET 请求 */
const { data } = await axios.get('/api/users', {
params: { page: 1, size: 20 } // 自动拼接到 URL
});
/* POST 请求:data 自动 JSON 序列化 */
const res = await axios.post('/api/users', {
name: '张玥',
age: 28
});
console.log(res.data, res.status, res.headers);
/* 一次配置,通用请求 */
const res = await axios({
url: '/api/users',
method: 'post',
baseURL: 'https://api.example.com',
timeout: 5000,
data: { name: '张玥' }
});
/* 简写方法一览 */
axios.get(url, config)
axios.post(url, data, config)
axios.put(url, data, config)
axios.patch(url, data, config)
axios.delete(url, config)
axios.head(url, config)
axios.options(url, config)
├── status: HTTP 状态码
├── statusText: 状态描述
├── headers: 响应头
├── config: 请求配置
└── request: 原生 XHR / http 对象
Axios 实例与全局配置
在真实项目中,我们通常不会直接使用全局 axios,而是通过 axios.create() 创建实例,为不同后端服务配置独立的 baseURL、超时与请求头。
const api = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
withCredentials: true // 跨域携带 Cookie
});
/* 修改默认配置 */
api.defaults.timeout = 15000;
api.defaults.headers.common['X-App'] = 'web';
/* 常用配置项 */
{
url: '/users',
method: 'get',
baseURL: 'https://api.example.com',
params: { id: 1 },
data: { name: '张玥' },
timeout: 5000,
responseType: 'json', // arraybuffer / blob / text / stream
transformRequest: [(data) => data],
transformResponse: [(data) => data],
validateStatus: (status) => status < 400,
maxRedirects: 5,
onUploadProgress: (e) => {},
onDownloadProgress: (e) => {}
}
每个微服务一个实例,互不干扰
拦截器:请求与响应的统一处理
拦截器是 Axios 最强大的特性。它允许你在请求发出前、响应到达后插入通用逻辑,例如注入 Token、统一错误提示、加载状态管理、日志埋点等。
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
config.metadata = { start: Date.now() };
return config;
},
(error) => Promise.reject(error)
);
/* 响应拦截器:解包数据 + 统一错误 */
api.interceptors.response.use(
(response) => {
const { data, status } = response;
if (status >= 200 && status < 300) return data;
return Promise.reject(new Error('业务异常'));
},
(error) => {
const status = error.response?.status;
if (status === 401) {
localStorage.removeItem('token');
location.href = '/login';
}
return Promise.reject(error);
}
);
/* 移除拦截器 */
const id = api.interceptors.request.use(fn);
api.interceptors.request.eject(id);
Axios 错误处理与取消机制
Axios 在 HTTP 状态码非 2xx 时会自动 reject,错误对象结构清晰,便于分类处理。同时它也提供了与 Fetch 一致的 AbortController 支持。
try {
await api.get('/api/users');
} catch (error) {
if (error.response) {
// 服务器返回了非 2xx 状态码
console.log(error.response.status);
console.log(error.response.data);
console.log(error.response.headers);
} else if (error.request) {
// 请求已发出但没有收到响应
console.log(error.request);
} else {
// 请求配置阶段出错
console.log('Error', error.message);
}
}
/* 判断错误类型 */
axios.isAxiosError(error); // 是否 Axios 错误
axios.isCancel(error); // 是否被取消
error.code === 'ECONNABORTED'; // 超时
error.code === 'ERR_NETWORK'; // 网络错误
/* AbortController 取消请求 */
const controller = new AbortController();
api.get('/api/slow', { signal: controller.signal })
.catch((err) => {
if (axios.isCancel(err)) console.log('已取消');
});
controller.abort();
先判断 response,再判断 request,最后兜底
Fetch 与 Axios 全面对比
两者各有优势,选择哪一个取决于项目规模、团队习惯与运行环境。下面从多个维度进行对比。
// —— Fetch ——
async function getUser(id) {
const res = await fetch(`/api/users/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
}
// —— Axios ——
async function getUser(id) {
const { data } = await api.get(`/users/${id}`);
return data;
}
// Token 注入、错误处理、超时全部在拦截器与实例中完成
• 体积为 0
• 基于 Promise 标准
• 支持 Stream、Service Worker
• 更贴近底层,可控性强
• 拦截器机制
• 内置 timeout
• 上传/下载进度
• 错误对象信息完整
• 浏览器 + Node 同构
实战:封装一个企业级请求库
无论选择哪种方案,最终都应该封装成统一的请求层,让业务代码只关心"拿数据",而不是"怎么请求"。
import axios from 'axios';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000,
withCredentials: true
});
/* 请求队列:用于重复请求取消 */
const pending = new Map();
service.interceptors.request.use((config) => {
const key = config.method + config.url + JSON.stringify(config.params);
if (pending.has(key)) {
pending.get(key).abort(); // 取消上一次相同请求
}
const controller = new AbortController();
config.signal = controller.signal;
pending.set(key, controller);
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
service.interceptors.response.use(
(response) => {
const key = response.config.method + response.config.url;
pending.delete(key);
const { code, data, message } = response.data;
if (code !== 0) {
Toast.error(message || '业务异常');
return Promise.reject(new Error(message));
}
return data;
},
(error) => {
if (axios.isCancel(error)) return Promise.reject(error);
const status = error.response?.status;
const map = {
400: '请求参数错误',
401: '登录已过期',
403: '没有权限',
404: '资源不存在',
500: '服务器开小差了'
};
Toast.error(map[status] || '网络异常');
if (status === 401) router.push('/login');
return Promise.reject(error);
}
);
export default service;
✅ Token、错误提示、Loading 统一处理
✅ 重复请求自动取消
✅ 接口返回结构统一为
data✅ 便于切换底层实现(Fetch ↔ Axios)
并发请求与性能优化
页面初始化时往往需要同时请求多个接口。合理使用 Promise 组合方法,可以大幅缩短首屏时间。
const [user, orders, notices] = await Promise.all([
api.get('/user'),
api.get('/orders'),
api.get('/notices')
]);
/* Promise.allSettled:互不影响,容错更强 */
const results = await Promise.allSettled([
api.get('/user'),
api.get('/recommend')
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.warn('失败:', r.reason);
});
/* Promise.race:谁快用谁(常用于超时竞速) */
const result = await Promise.race([
api.get('/primary-api'),
api.get('/backup-api')
]);
/* 限制并发数量 */
async function pool(tasks, limit = 5) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(task);
results.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
}
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
}
return Promise.all(results);
}
最佳实践与总结
- 始终检查响应状态:Fetch 用
res.ok,Axios 依赖自动 reject - 统一封装请求层:不要在组件里散落
fetch与axios调用 - 善用拦截器:Token 注入、错误提示、Loading 管理集中处理
- 处理取消与超时:组件卸载时 abort,避免内存泄漏与状态错乱
- 合理并发:能并行不串行,大批量任务注意限制并发数
- 区分错误类型:网络错误、HTTP 错误、业务错误分开处理
- 避免重复请求:相同参数请求进行去重与缓存
- 注意安全:Token 存储、HTTPS、CORS、SRI 完整性校验
- 监控与埋点:记录请求耗时、失败率,便于线上排查
/* 统一入口 → 统一配置 → 统一错误 → 统一出口 */
/* 一个健壮的请求函数应具备的能力 */
async function request(url, options = {}) {
const {
timeout = 10000,
retry = 2,
signal,
...rest
} = options;
for (let i = 0; i <= retry; i++) {
try {
return await doFetch(url, { ...rest, timeout, signal });
} catch (err) {
if (i === retry || !isRetryable(err)) throw err;
await sleep(2 ** i * 300); // 指数退避
}
}
}