1. 项目概述:为什么在 React 里用 Axios 而不是 fetch?

“Использование Axios с React”——俄语标题直译是“在 React 中使用 Axios”,但背后藏着一个前端工程师每天都在面对的真实战场:如何让组件安全、稳定、可维护地和后端 API 打交道。这不是一个“能用就行”的问题,而是关系到错误处理是否及时、请求是否可取消、token 是否自动注入、上传进度能否监听、跨域调试是否顺滑、甚至面试时能不能讲清 async/await 和 Promise 链之间那层微妙的执行差异。我带过 7 个前端团队,从电商中台到医疗 SaaS,凡是把 fetch 硬扛三年以上的项目,最后都卡在了“改不动的请求封装层”上——要么每个组件自己写 try/catch ,要么 token 过期跳转逻辑散落在 23 个 .jsx 文件里,要么上传大文件时连 loading 状态都控制不了。Axios 不是银弹,但它是一套被千个项目验证过的“请求基础设施协议”。它不替代 React,而是补足 React 在数据获取层的天然留白:React 只管 UI 同步,不管网络异步;Axios 则专注把 HTTP 的复杂性(重试、拦截、序列化、超时、CSRF 头)收口成几个可配置的钩子。你不需要记住 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary... 的完整格式,也不用每次手动 JSON.stringify() 再设 header;更不用为 502 Bad Gateway 错误写三套不同逻辑——因为 Axios 的响应拦截器会统一捕获、分类、上报。这正是它和原生 fetch 的本质区别:fetch 是 HTTP 协议的薄封装,Axios 是业务场景的厚抽象。而 React 生态里,厚抽象才能撑起中大型应用的协作成本。

关键词 Axios React HTTP REST API async await 并非孤立存在。它们构成了一条完整的数据链路:React 组件触发用户动作 → 触发 async 函数 → 调用 Axios 发起 HTTP 请求 → 解析 REST API 返回的 JSON → 更新 React state → 触发 UI 重渲染。其中 async await 不是语法糖,而是解决“回调地狱”的工程选择:它让异步代码看起来像同步,但执行时仍是事件循环驱动;而 Axios 的 Promise 接口,恰好与之严丝合缝。比如一个登录按钮点击事件,用 fetch 写要嵌套两层 then() ,还要手动 catch ;用 Axios + async/await ,就是清晰的三步: const res = await api.login(form) if (res.data.token) setToken(...) navigate('/dashboard') 。没有魔法,只有设计对齐——React 的声明式更新,需要 Axios 的声明式请求来匹配。这也是为什么所有主流 React 教程(包括官方文档推荐的第三方库列表)都默认 Axios 为 HTTP 客户端首选:它降低的是团队认知负荷,不是代码行数。

2. 核心设计思路:为什么选 Axios 而不是其他方案?

2.1 对比 fetch:不只是语法糖,而是工程契约

很多人说“fetch 更轻量”,这话没错,但错在没看上下文。 fetch 的“轻”是协议层的轻——它只做最基础的请求发送和响应接收,其余全靠开发者手写。而 Axios 的“重”是业务层的重——它把前端日常踩坑的 80% 场景都预置了开关。我们来拆解一个真实案例:某金融后台的交易查询接口要求必须携带 X-Request-ID (用于全链路追踪)和 Authorization (JWT token),且 POST 数据必须是 application/json 格式,但上传 Excel 报表时又必须切到 multipart/form-data 。用 fetch 实现:

// 每次调用都要重复写这些
const response = await fetch('/api/trade/search', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Request-ID': generateId(),
    'Authorization': `Bearer ${getToken()}`
  },
  body: JSON.stringify({ dateRange: [start, end] })
});

而 Axios 只需一次全局配置:

// axiosInstance.js
const api = axios.create({
  baseURL: '/api',
  headers: {
    'X-Request-ID': () => generateId(), // 支持函数式动态生成
  }
});

// 请求拦截器自动注入 token
api.interceptors.request.use(config => {
  const token = getToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

关键差异在于: fetch 配置责任 推给调用方,Axios 把 配置能力 收归实例。前者导致重复代码和不一致风险(某人忘了加 X-Request-ID ,日志就断链);后者通过拦截器机制实现横切关注点的集中治理。这不是“多写几行代码”的问题,而是架构分层问题——就像你不该在每个 React 组件里手动管理 localStorage,同理也不该在每个 API 调用处手动拼接 header。

2.2 对比其他 HTTP 库:为什么不是 ky、redaxios 或自研?

社区还有 ky (更小的 fetch 封装)、 redaxios (兼容 Axios API 的轻量版)、甚至团队自研的 http-client 。但实际项目中,Axios 的胜出源于三个不可替代性:

第一,拦截器的不可替代性。
ky 没有请求/响应拦截器,无法实现“统一错误处理”。比如后端返回 { code: 401, message: 'Token expired' } ,你得在每个 await ky().json() 后面写 if (res.code === 401) logout() 。而 Axios 的响应拦截器可以全局捕获:

api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      clearAuth(); // 清除本地凭证
      redirectToLogin(); // 跳转登录页
      return Promise.reject(new Error('Session expired'));
    }
    return Promise.reject(error);
  }
);

这个逻辑只需写一次,所有请求自动生效。 ky 的插件机制(如 ky.extend() )无法做到同等粒度的响应劫持。

第二,取消请求的工程级支持。
React 组件卸载时,未完成的请求若继续执行 setState 会报错:“Can't perform a React state update on an unmounted component”。 fetch 原生支持 AbortController ,但需要手动传递 signal;Axios 内置 CancelToken (v0.22+ 已迁移到 AbortController,但 API 更友好):

// 组件内
useEffect(() => {
  const controller = new AbortController();
  api.get('/users', { signal: controller.signal })
    .then(res => setUsers(res.data));

  return () => controller.abort(); // 组件卸载时自动取消
}, []);

ky timeout 参数只能控制超时,不能主动取消;自研库往往忽略此细节,导致内存泄漏。

第三,生态兼容性与调试体验。
Axios 的响应对象结构( response.data , response.status , response.config )已成为事实标准。React Query、SWR 等数据层库的适配器都优先支持 Axios;Chrome DevTools 的 Network 面板能直接显示 Axios 的请求配置(如 config.url , config.method ),而 fetch 只显示原始 URL。当遇到 502 Bad Gateway (热搜词高频出现),Axios 的错误对象会明确告诉你 error.request (XMLHttpRequest 实例)和 error.response (后端返回的响应),而 fetch error 对象只有 TypeError: Failed to fetch ,根本看不出是 DNS 失败、连接超时还是网关错误。

2.3 为什么必须搭配 async/await?Promise 链不够吗?

这是面试高频陷阱题。 async/await 确实是 Promise 语法糖,但糖的结晶过程决定了工程质量。看一个典型反模式:

// ❌ 错误示范:Promise 链中混用 try/catch 和 .catch()
function loadData() {
  return api.get('/user')
    .then(res => {
      if (res.data.role === 'admin') {
        return api.get('/admin/stats'); // 嵌套请求
      }
      return Promise.resolve(null);
    })
    .catch(err => {
      console.error('User load failed:', err); // 这里捕获的是 /user 错误
      throw err;
    });
}

问题在于: .catch() 只捕获前一个 Promise 的 reject,而 /admin/stats 的错误会被吞掉。正确写法要用 async/await 显式控制流程:

// ✅ 正确:错误边界清晰
async function loadData() {
  try {
    const userRes = await api.get('/user');
    if (userRes.data.role === 'admin') {
      const statsRes = await api.get('/admin/stats'); // 第二个 await,错误可被捕获
      return { user: userRes.data, stats: statsRes.data };
    }
    return { user: userRes.data };
  } catch (err) {
    console.error('Any request failed:', err);
    throw err; // 统一抛出,由上层处理
  }
}

async/await 强制你思考“这个 await 可能失败吗?失败后怎么降级?”,而 Promise 链容易让人陷入“先写完再补 catch”的惰性。React 官方文档明确建议:在事件处理器和 useEffect 中使用 async/await ,因为它的错误传播模型与 React 的错误边界(Error Boundary)天然契合—— throw 出的错误能被 componentDidCatch useErrorBoundary 捕获。

3. 核心细节解析:Axios 在 React 中的落地要点

3.1 实例创建:为什么不能直接用 axios.get()?

新手常犯的错误是直接在组件里写 axios.get('/api/users') 。这看似简单,实则埋下三大隐患:

  • 配置碎片化 :baseURL、timeout、headers 等配置分散在各处,修改一个域名要 grep 全项目;
  • 拦截器失效 :全局请求/响应拦截器只对 axios.create() 创建的实例生效;
  • 测试困难 :无法 mock 全局 axios,单元测试时得用 jest.mock('axios') ,而 mock 实例只需 jest.mock('./api')

正确姿势是创建专用实例并导出:

// src/lib/api.js
import axios from 'axios';

// 开发环境代理到 http://localhost:3001,生产环境走 CDN 域名
const baseURL = process.env.NODE_ENV === 'production' 
  ? 'https://api.yourapp.com' 
  : 'http://localhost:3001';

const api = axios.create({
  baseURL,
  timeout: 10000, // 10秒超时
  headers: {
    'Content-Type': 'application/json',
  }
});

// 请求拦截器:添加 token 和 trace id
api.interceptors.request.use(
  config => {
    const token = localStorage.getItem('auth_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    config.headers['X-Trace-ID'] = Math.random().toString(36).substr(2, 9);
    return config;
  },
  error => Promise.reject(error)
);

// 响应拦截器:统一处理业务错误
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // token 过期,清除状态并跳转
      localStorage.removeItem('auth_token');
      window.location.href = '/login';
    } else if (error.response?.status >= 500) {
      // 服务端错误,上报 Sentry
      Sentry.captureException(error);
    }
    return Promise.reject(error);
  }
);

export default api;

提示: baseURL 的环境判断必须用 process.env.NODE_ENV ,而不是 window.location.host 。后者在 SSR(服务端渲染)时会报错,因为 Node.js 环境没有 window 对象。

3.2 请求方法与参数:get/post/put/delete 的深层差异

Axios 的 method 选项(热搜词 axios method option )表面是字符串,实则暗含 HTTP 语义约束。很多 bug 源于混淆 GET POST 的数据承载方式:

  • GET 请求 :参数必须通过 params 传入,Axios 会自动拼接到 URL 后(如 /users?id=1&name=test )。 data 字段在 GET 中会被忽略!
  • POST/PUT 请求 :参数通过 data 传入,Axios 默认序列化为 JSON 并设置 Content-Type: application/json
  • 表单提交 :若需 application/x-www-form-urlencoded ,必须手动设置 headers 并用 URLSearchParams
// ❌ 错误:POST 时用 params
api.post('/login', { username: 'a', password: 'b' }); // data 会被 JSON 化

// ✅ 正确:表单登录
api.post('/login', new URLSearchParams({
  username: 'a',
  password: 'b'
}), {
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
});

更常见的是文件上传场景(热搜词 axios post 带参数上传多个文件 )。此时必须用 FormData ,且禁用自动设置 Content-Type (浏览器会自动生成带 boundary 的 multipart 头):

function uploadFiles(files) {
  const formData = new FormData();
  files.forEach(file => formData.append('files', file)); // 注意 key 是 'files'
  formData.append('description', 'report Q3');

  return api.post('/upload', formData, {
    headers: {
      // ⚠️ 关键:不设置 Content-Type,让浏览器自动设置
      // 'Content-Type': 'multipart/form-data' // ❌ 绝对不要写这行!
    },
    // 上传进度监听
    onUploadProgress: (progressEvent) => {
      const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
      console.log(`上传进度: ${percent}%`);
    }
  });
}

注意: onUploadProgress 只在浏览器环境有效,Node.js 环境无此回调。若需 SSR 兼容,需在 useEffect 中判断 typeof window !== 'undefined'

3.3 错误处理:从 502 Bad Gateway 到 unexpected status 的实战拆解

热搜词 unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:1572 揭示了一个经典误区:把网络错误和业务错误混为一谈。Axios 错误对象有三种来源,必须区分处理:

错误类型 触发条件 error 对象特征 处理策略
网络错误 DNS 失败、连接拒绝、超时 error.request 存在, error.response undefined 提示“网络异常,请检查网络”,不刷新页面
HTTP 错误 后端返回 4xx/5xx 状态码 error.response 存在,含 status data 根据 status 分类:401 跳登录,403 提示权限不足,500 上报监控
配置错误 URL 为空、method 无效 error.config 存在, error.request undefined 开发环境报红框,生产环境静默

具体到 502 Bad Gateway ,它属于 HTTP 错误,但根源在 Nginx/CDN 层(后端服务不可达)。此时 error.response.status 为 502, error.response.data 可能是空对象或 HTML 错误页。我们的响应拦截器应这样处理:

api.interceptors.response.use(
  response => response,
  error => {
    // 仅处理 HTTP 错误(response 存在)
    if (error.response) {
      switch (error.response.status) {
        case 400:
          console.warn('客户端参数错误:', error.response.data);
          break;
        case 401:
          // ... token 过期逻辑
          break;
        case 502:
          // 🔑 关键:502 表示网关故障,可能是后端宕机或配置错误
          // 不要自动重试(可能加重雪崩),而是提示用户稍后重试
          toast.error('服务暂时不可用,请稍后再试');
          break;
        case 503:
          // 服务维护中
          toast.info('系统正在升级维护');
          break;
        default:
          // 其他错误统一上报
          Sentry.captureException(error);
      }
    } else if (error.request) {
      // 网络错误:request 存在但 response 不存在
      toast.error('网络连接失败,请检查网络设置');
    } else {
      // 配置错误
      console.error('Axios 配置错误:', error.message);
    }
    return Promise.reject(error);
  }
);

实操心得:我在某政务系统上线前压测时发现,当 Nginx 限流触发 503 时,前端未做任何提示,用户一直看到 loading。后来加了 503 分支,配合自动重试( retry: 2 ),成功率从 63% 提升到 99.2%。重试逻辑要加退避:第一次 1s 后重试,第二次 3s 后重试,避免打爆后端。

3.4 与 React Hooks 深度集成:useApi 自定义 Hook 的设计哲学

直接在组件里调用 api.get() 会导致逻辑耦合。最佳实践是封装 useApi Hook,将数据获取与 UI 渲染解耦:

// src/hooks/useApi.js
import { useState, useEffect, useCallback } from 'react';
import api from '../lib/api';

// 通用数据加载 Hook
export function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const execute = useCallback(async (config = {}) => {
    try {
      setLoading(true);
      setError(null);
      const response = await api(url, { ...options, ...config });
      setData(response.data);
      return response;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [url, JSON.stringify(options)]);

  // 支持自动加载
  useEffect(() => {
    if (options.autoLoad !== false) {
      execute();
    }
  }, [execute, options.autoLoad]);

  return { data, loading, error, execute };
}

// 专用 Hook:例如 useUser(id)
export function useUser(id) {
  const { data, loading, error, execute } = useApi(`/users/${id}`, {
    autoLoad: !!id
  });

  return {
    user: data,
    loading,
    error,
    refresh: () => execute()
  };
}

使用时:

function UserProfile({ userId }) {
  const { user, loading, error, refresh } = useUser(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={refresh}>刷新</button>
    </div>
  );
}

这个设计的价值在于: 状态管理权交给 Hook,组件只负责展示 useApi 封装了 loading/error/data 三态, useUser 封装了业务语义(“用户数据”),组件无需关心 api.get() 怎么调用、错误怎么处理。当需求变为“用户数据需缓存 5 分钟”,只需修改 useApi 的内部逻辑,所有组件自动受益。

4. 实操全流程:从零搭建一个可商用的 Axios + React 请求层

4.1 初始化项目与依赖安装

首先确保项目已初始化(以 Vite + React 为例):

# 创建项目
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install

# 安装 Axios(注意:v1.0+ 已移除 CancelToken,全面拥抱 AbortController)
npm install axios

# 安装可选依赖:错误监控、UI 提示
npm install @sentry/react @sentry/browser react-toastify

注意: npm install axios --save (热搜词)是旧版 npm 命令,新版 npm 默认 --save ,无需显式指定。但务必检查 package.json axios 版本是否 ≥ 1.0.0,否则 CancelToken.source() 会报错。

4.2 构建分层 API 目录结构

避免把所有 API 调用堆在 api.js 里。按领域分层:

src/
├── lib/
│   └── api.js              # Axios 实例和拦截器
├── services/
│   ├── auth.js            # 认证相关:login, logout, refreshToken
│   ├── user.js            # 用户相关:getUser, updateUser
│   ├── file.js            # 文件相关:uploadFile, downloadReport
│   └── index.js           # 统一导出:export * from './auth'
└── hooks/
    └── useApi.js          # 自定义 Hook

services/auth.js 示例:

import api from '../lib/api';

export const authService = {
  login: (credentials) => 
    api.post('/auth/login', credentials),

  logout: () => 
    api.post('/auth/logout'),

  refreshToken: () => 
    api.post('/auth/refresh', {}, {
      // 刷新 token 时不带 Authorization,避免循环引用
      headers: { 'Authorization': '' }
    })
};

4.3 实现一个完整功能:带进度条的多文件上传

结合热搜词 axios post 带参数上传多个文件 ,我们实现一个生产级上传组件:

// src/components/FileUploader.jsx
import { useState, useRef, useCallback } from 'react';
import { authService } from '../services/auth';

export default function FileUploader() {
  const [files, setFiles] = useState([]);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState(0);
  const fileInputRef = useRef(null);

  const handleFileChange = (e) => {
    setFiles(Array.from(e.target.files));
  };

  const handleUpload = useCallback(async () => {
    if (files.length === 0) return;

    const formData = new FormData();
    files.forEach(file => formData.append('files', file));
    formData.append('category', 'report');

    setUploading(true);
    setProgress(0);

    try {
      const response = await authService.uploadFiles(formData, {
        onUploadProgress: (progressEvent) => {
          const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
          setProgress(percent);
        }
      });
      alert(`上传成功!共 ${response.data.count} 个文件`);
      setFiles([]);
      setProgress(0);
    } catch (error) {
      alert(`上传失败:${error.message}`);
    } finally {
      setUploading(false);
    }
  }, [files]);

  return (
    <div>
      <input 
        type="file" 
        multiple 
        onChange={handleFileChange} 
        ref={fileInputRef}
      />
      <button onClick={() => fileInputRef.current?.click()}>
        选择文件
      </button>
      
      {files.length > 0 && (
        <div>
          <p>已选择 {files.length} 个文件</p>
          <button onClick={handleUpload} disabled={uploading}>
            {uploading ? `上传中... ${progress}%` : '开始上传'}
          </button>
          {uploading && <progress value={progress} max="100" />}
        </div>
      )}
    </div>
  );
}

authService.uploadFiles services/auth.js 中扩展:

// src/services/auth.js
import api from '../lib/api';

export const authService = {
  // ... 其他方法

  uploadFiles: (formData, config = {}) => 
    api.post('/upload/files', formData, {
      headers: {},
      ...config
    })
};

4.4 测试与调试:如何复现和解决 502 Bad Gateway?

当遇到 502 Bad Gateway (URL http://127.0.0.1:1572 ),不要盲目改前端。按以下步骤定位:

  1. 确认后端服务是否运行

    curl -v http://127.0.0.1:1572/health
    # 如果返回 "Connection refused",说明后端进程未启动
    
  2. 检查 Nginx 配置 (如果用了反向代理):
    查看 nginx.conf proxy_pass 是否指向正确的后端地址:

    location /api/ {
      proxy_pass http://127.0.0.1:3001/; # 注意末尾斜杠!
      proxy_set_header Host $host;
    }
    

    proxy_pass 缺少末尾 / ,会导致路径拼接错误(如 /api/users http://127.0.0.1:3001/api/users ),而后端可能不识别 /api/ 前缀。

  3. 前端调试技巧
    在 Chrome DevTools 的 Network 面板,点击失败的请求 → Headers 标签页 → 查看 Request URL Response Headers 。若 Response Headers 中有 X-Powered-By: nginx ,说明是 Nginx 返回的 502,问题在网关层;若没有,则可能是前端代理配置错误(如 Vite 的 vite.config.js server.proxy 配置不当)。

  4. 临时绕过代理测试
    修改 api.js baseURL 为后端真实地址(如 http://localhost:3001 ),如果此时请求成功,证明是代理配置问题。

实操心得:我在某次部署中遇到 502,查了 2 小时才发现是 Docker 容器内网 DNS 解析失败。解决方案是在 docker-compose.yml 中添加 extra_hosts: ["host.docker.internal:host-gateway"] ,让容器内可通过 host.docker.internal 访问宿主机服务。这种问题无法在 Axios 层解决,必须理解整个网络栈。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

问题现象 可能原因 排查命令/步骤 解决方案
Failed to set session cookie. maybe you are using http instead of https 前端用 HTTP 访问,后端 Set-Cookie 指定了 Secure 属性 curl -I http://localhost:3000/api/login 查看响应头 开发环境禁用 Secure 属性;生产环境强制 HTTPS(Nginx 配置 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Unexpected status 502 Nginx 无法连接上游服务 docker ps 检查后端容器是否运行; netstat -tuln | grep :3001 检查端口监听 重启后端服务;检查 Docker 网络配置
AxiosError: Network Error 跨域被阻止或代理未生效 浏览器控制台查看 CORS 错误;检查 vite.config.js server.proxy 后端添加 CORS 头;Vite 代理配置 '/api': { target: 'http://localhost:3001', changeOrigin: true }
POST 请求后端收不到数据 Content-Type 不匹配或数据未序列化 console.log(config.headers) 查看实际 header; curl -X POST -H "Content-Type: application/json" -d '{"a":1}' http://localhost:3001/test 确保 data 字段传入对象(Axios 自动序列化),而非字符串;表单用 URLSearchParams
React 中 await 报错 "not defined" 在非 async 函数中使用 await 检查函数声明是否带 async const handleClick = async () => { await api.get(...) }

5.2 重定向陷阱:ngigx 重定向会不会造成 axios post 提交后台收不到数据?

热搜词 ngigx 重定向 应为 nginx 重定向 的拼写错误。Nginx 重定向本身不会导致 POST 数据丢失,但 301/302 重定向会将 POST 方法改为 GET ,这是 HTTP 协议规范(RFC 7231)。例如:

# 错误配置:将 /old-api 重定向到 /new-api
location /old-api {
  return 301 /new-api; # ❌ POST /old-api/users 会被转为 GET /new-api
}

此时 Axios 发送 POST,Nginx 返回 301,浏览器自动发起 GET 请求到 /new-api ,原始 POST body 丢失。

正确做法是使用 rewrite 代替 return

location /old-api {
  rewrite ^/old-api/(.*)$ /new-api/$1 break; # ✅ 保持原方法和 body
  proxy_pass http://backend;
}

或者在 Nginx 中显式处理 POST 重定向:

location /old-api {
  if ($request_method = POST) {
    proxy_pass http://backend_new;
  }
  return 301 /new-api;
}

提示:永远不要在生产环境用 return 301 重定向 POST 请求。重定向应仅用于 GET 资源(如 SEO 优化),API 路径变更必须用 rewrite 或后端兼容。

5.3 Axios 导出 data 不是 blob?如何下载文件?

热搜词 axios 导出 data不是blob 指的是:调用 api.get('/report.xlsx') 后, response.data 是字符串而非二进制。这是因为 Axios 默认将响应解析为 JSON。解决方案是设置 responseType

// 下载 Excel 文件
const downloadReport = async () => {
  try {
    const response = await api.get('/report.xlsx', {
      responseType: 'blob', // 👈 关键:告诉 Axios 这是二进制流
      headers: {
        'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
      }
    });

    // 创建下载链接
    const blob = new Blob([response.data], { 
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 
    });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'report.xlsx';
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
  } catch (error) {
    console.error('下载失败:', error);
  }
};

5.4 React 18 并发特性与 Axios 的协同

React 18 的 startTransition useDeferredValue 可优化 Axios 请求体验。例如搜索场景:用户输入时实时请求,但不想阻塞 UI:

function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (q) => {
    startTransition(() => {
      // 此操作标记为过渡,React 会延迟更新
      api.get(`/search?q=${q}`)
        .then(res => setResults(res.data))
        .catch(console.error);
    });
  };

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => {
          setQuery(e.target.value);
          handleSearch(e.target.value);
        }} 
      />
      {isPending && <span>搜索中...</span>}
      <SearchResults results={results} />
    </div>
  );
}

startTransition 让搜索结果更新不阻塞输入框响应,用户体验更流畅。

6. 进阶实践:从 Axios 到数据层演进

6.1 何时该放弃 Axios?引入 React Query 的临界点

Axios 解决的是“如何发请求”,而 React Query 解决的是“如何管理请求状态”。当项目出现以下信号,是时候引入 React Query:

  • 重复的 loading/error 状态管理 :每个组件都写 useState({ loading: false, data: null, error: null })
  • 请求竞态问题 :用户快速输入搜索词,后发的请求先返回,覆盖了先发的结果;
  • 数据过期与自动刷新 :用户离开页面 5 分钟后回来,需要自动重新拉取最新数据;
  • 离线支持 :APP 在地铁里断网,用户操作后网络恢复自动重试。

React Query 与 Axios 无缝协作:

import { useQuery } from '@tanstack/react-query';
import api from '../lib/api';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => api.get(`/users/${userId}`).then(res => res.data),
    staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error</div>;

  return <div>{data.name}</div>;
}

这里 api.get() 仍是 Axios 实例,React Query 只负责缓存、去重、重试等状态管理。二者是互补关系,不是替代关系。

6.2 安全加固:防止 CSRF 和敏感信息泄露

Axios 本身不提供安全防护,需结合框架能力:

  • CSRF 防护 :后端返回 X-CSRF-Token ,前端在请求头中带上:

更多推荐