Fetch API与Axios

张玥 2026年9月16日 阅读时间 40分钟
异步编程 Fetch API Axios Promise async/await
异步编程之Fetch API与Axios

现代前端应用几乎无时无刻不在与服务器通信:拉取列表、提交表单、上传文件、轮询状态、订阅消息……这一切的背后,都是异步编程在支撑。而发起 HTTP 请求的两大主力——原生 Fetch API 与社区经典 Axios,几乎是每一位前端工程师绕不开的必修课。本文将带你从异步编程的演进讲起,深入 Fetch 的请求配置、Response 解析、错误处理、取消与超时,再系统梳理 Axios 的实例、拦截器、错误对象与取消机制,最后通过横向对比与实战封装,帮助你彻底掌握两大请求方案,写出健壮、可维护、高性能的网络层代码。40 分钟沉浸式学习,从"会用"到"用对",再到"用精"。

异步编程演进:从回调到 async/await

理解 Fetch 与 Axios,必须先理解 JavaScript 的异步模型。浏览器是单线程的,网络请求天然异步,于是我们经历了回调函数、Promise、Generator、async/await 四个阶段。

/* 1. 回调地狱(Callback Hell) */
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);
  }
}
① 回调函数时代
嵌套深、错误处理分散、无法组合
② Promise 时代
链式调用、统一 catch、可组合(all / race)
③ async/await 时代
同步语义、try/catch、调试友好
④ 现代请求层
Fetch / Axios + async/await + 拦截器

每一次演进,都在解决上一次的痛点

Fetch API 快速入门

fetch() 是浏览器原生提供的全局函数,基于 Promise 设计,用于替代老旧的 XMLHttpRequest。它的基本调用非常简单。

/* 最简单的 GET 请求 */
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();
GET /api/users
POST /api/users
PUT /api/users/1
DELETE /api/users/1

fetch 默认使用 GET 方法

深入 Response 对象

fetch() 返回的是一个 Response 对象,它并不直接包含数据,而是包含响应元信息与一个可读流。理解它,是正确使用 Fetch 的关键。

/* Response 常用属性 */
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();  // 表单数据
Response 结构
├── status: 200
├── ok: true
├── headers: Headers
├── url: string
└── body: ReadableStream
body 是流,只能读取一次!重复调用 res.json() 会抛出 "body stream already read"。

Fetch 请求配置全解析

fetch(url, options) 的第二个参数是一个配置对象,掌握它才能发挥 Fetch 的全部能力。

const res = await fetch('/api/data', {
  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
});
method
请求方法
headers
请求头
body
请求体
credentials
携带 Cookie
cache
缓存策略
signal
取消信号

跨域携带 Cookie 必须同时设置 credentials 与服务端 CORS 响应头

错误处理:Fetch 最大的"坑"

与很多人直觉相反,fetch 在遇到 404、500 时不会 reject。只有当网络中断、DNS 失败、请求被拦截时才会 reject。因此必须手动判断 res.ok。

/* ❌ 错误示范:404 也会进入 then */
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();
}
会 reject 的情况
断网、DNS 解析失败、CORS 预检失败、请求被 abort
不会 reject 的情况
404、500、401、403 —— 全部 resolve,需检查 res.ok
统一封装建议
在封装层将 !res.ok 转换为 reject,让调用方只写一套 catch

请求取消与超时控制

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));
}
请求生命周期
发起 abort()
• signal.aborted 可查询状态
• 一个 signal 只能中止一次
• 多个请求可共用一个 controller
组件卸载时 abort,可有效避免内存泄漏与状态错乱

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)
Axios 响应对象
├── data: 服务端返回的数据
├── status: HTTP 状态码
├── statusText: 状态描述
├── headers: 响应头
├── config: 请求配置
└── request: 原生 XHR / http 对象
自动 JSON 拦截器 超时 取消

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) => {}
}
用户服务
/api/user
订单服务
/api/order
支付服务
/api/pay
文件服务
/api/file

每个微服务一个实例,互不干扰

拦截器:请求与响应的统一处理

拦截器是 Axios 最强大的特性。它允许你在请求发出前、响应到达后插入通用逻辑,例如注入 Token、统一错误提示、加载状态管理、日志埋点等。

/* 请求拦截器:注入 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);
发起请求
↓
请求拦截器(注入 Token / 加载态)
↓
服务器 / 网络
↓
响应拦截器(解包 / 统一报错)
↓
业务代码拿到 data

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();
error.response
4xx / 5xx,含状态码与响应体
error.request
请求已发出但无响应(超时、断网)
error.message
配置错误或未知异常

先判断 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 注入、错误处理、超时全部在拦截器与实例中完成
Fetch 优势
• 浏览器原生,零依赖
• 体积为 0
• 基于 Promise 标准
• 支持 Stream、Service Worker
• 更贴近底层,可控性强
Axios 优势
• 自动 JSON 转换
• 拦截器机制
• 内置 timeout
• 上传/下载进度
• 错误对象信息完整
• 浏览器 + Node 同构
非 2xx 自动 rejectFetch ✗ / Axios ✓
请求/响应拦截器Fetch ✗ / Axios ✓
超时配置Fetch 需手写 / Axios 内置
取消请求AbortController / AbortController

实战:封装一个企业级请求库

无论选择哪种方案,最终都应该封装成统一的请求层,让业务代码只关心"拿数据",而不是"怎么请求"。

/* request.js —— 基于 Axios 的完整封装 */
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;
封装带来的收益
✅ 业务层代码减少 50% 以上
✅ Token、错误提示、Loading 统一处理
✅ 重复请求自动取消
✅ 接口返回结构统一为 data
✅ 便于切换底层实现(Fetch ↔ Axios)
分层思想:底层请求 → 拦截器 → 业务 API → 组件

并发请求与性能优化

页面初始化时往往需要同时请求多个接口。合理使用 Promise 组合方法,可以大幅缩短首屏时间。

/* Promise.all:全部成功才成功 */
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);
}
串行 vs 并行(3 个各 300ms 的请求)
串行:900ms
并行:300ms
Promise.all allSettled race any

最佳实践与总结

  • 始终检查响应状态: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); // 指数退避
    }
  }
}