ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端HTTP请求库axios核心配置与拦截器实战指南

前端HTTP请求库axios核心配置与拦截器实战指南

这个axios库,说实话是我日常开发中几乎每天都在打交道的东西。今天这篇是前端交互系列的第三篇,前面聊了基础的前后端通信方式,这次就专门把axios拎出来彻底讲透——从它解决了什么问题,到拦截器怎么设计,再到实际项目里怎么封装、怎么避开那些坑,一篇讲完。

如果你是刚接触前后端交互不久的新手,或者已经在项目里用axios但总觉得自己只是“会用个get和post”,这篇内容就是给你准备的。我会把配置项、拦截器、取消请求、防重复提交这些核心场景都过一遍,代码直接可以复制去用。

1. 为什么是 axios?先聊聊它解决了什么问题

1.1 从原生XHR到fetch的痛点

很多人刚学前端的时候,用的第一个发请求的方式是XMLHttpRequest,就是那个写起来非常啰嗦的对象。创建一个实例、监听状态变化、判断readyState、处理响应,一套流程下来代码又长又难维护。后来fetch出现以后,原生支持Promise,写法清爽了不少,但它有几个实际问题:第一,老版本浏览器的兼容性需要额外处理;第二,fetch对请求中断的支持不够友好;第三,也是最关键的,它没有拦截器机制,想在请求发出前统一加个token或在响应回来后统一处理错误,你得自己在业务代码里到处写。

我在实际项目里体验最深的是这样一个场景:某天后端同事说“所有接口的错误码结构要调整”,用fetch的话,你项目里有三十个页面调了接口,每个页面都得找到请求的地方去改。但如果用了axios的响应拦截器,只需要改一个文件里的一个函数。这个对比特别直观,也是团队协作中axios最值钱的地方。

1.2 axios到底强在哪儿

axios是一个基于Promise的HTTP请求库,底层在浏览器里用的是XMLHttpRequest,在Node.js环境里用的是内置的http模块。这意味着同一套代码在浏览器和Node端都能跑,做服务端渲染、做自动化测试脚本,都可以直接复用你写好的请求封装。

它的核心优势我记得很清楚:

  • 支持Promise,async/await写起来非常顺手
  • 提供拦截器,请求发出前、响应回来后都有统一处理的入口
  • 支持请求取消,用户切换页面或者重复点击时能主动中断
  • 自动转换JSON数据,请求时把对象序列化,响应时把字符串解析成对象
  • 支持跨域的携带凭证配置,withCredentials一行搞定
  • 内置超时设置,timeout参数可以直接控制请求多久算失败

我拿个生活化的类比帮你们理解:把前后端交互比作寄快递的话,axios相当于一个成熟的快递服务商。你自己去邮局寄(XHR)手续麻烦,用跑腿小哥(fetch)虽然快但遇到问题得自己协调,而axios这种服务商会告诉你“你只要把包裹给我,填好单子,剩下打包、配送、异常处理都统一帮你搞定”。

2. 核心配置与API,这些细节别搞混

2.1 基本请求方法与参数位置

axios最基础的用法很简单,但你得搞清楚params和data的区别,我见过太多人在这两个参数上报错了。params是用来拼在URL查询字符串里的,一般用于GET请求;data是放在请求体里的,一般用于POST、PUT这类请求。后端接口设计如果约定“查询类接口用GET,数据提交类接口用POST”,那你前端就别乱套。

import axios from 'axios'; // GET请求,参数拼在url后面 axios.get('/api/user/list', { params: { page: 1, pageSize: 10 } }).then(res => { console.log(res.data); }); // POST请求,参数放在请求体 axios.post('/api/user/update', { id: 1001, name: '张三', age: 28 }).then(res => { console.log(res.data); });

这里注意一下响应对象的结构,axios会把返回结果包在一个对象里,里面包含data(后端返回的数据)、status(HTTP状态码)、statusText(状态文本)、headers(响应头)、config(这次的请求配置)、request(原生请求对象)。大多数场景下你只需要取res.data,这里的data是后端返回的数据本体。

2.2 创建实例与全局配置

项目里很少有只用axios默认配置的情况,比如不同模块的接口域名可能不一样,超时时间也可能不同。这时候用axios.create创建独立的实例,比直接在全局配置上改要灵活得多。

// 创建一个带默认配置的实例 const request = axios.create({ baseURL: 'https://api.example.com', timeout: 15000, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); // 使用实例发请求 request.get('/activity/list', { params: { userId: 888 } });

baseURL的作用是,你在具体请求里写的url如果是相对路径,它会自动拼在baseURL后面。举个例子,baseURL设置成https://api.example.com,请求里写/activity/list,那最终地址就是https://api.example.com/activity/list。这比每次写全路径舒服多了,而且后端如果切换域名,只需要改这一个配置。

timeout这里我建议不要拍脑袋填,要结合后端接口实际响应时间来定。我之前填过3秒的超时时间,结果有个报表接口正常就要5秒,用户经常看到请求失败。后来我把这个接口单独建了一个实例,把超时放宽到15秒,才把问题解决。经验就是:全局超时可以设得保守一点,特殊接口单独覆盖。

3. 拦截器:最值得花时间理解的设计

3.1 请求拦截器实战

拦截器可以理解成请求生命周期中的“关卡”。请求拦截器会在请求发送出去之前执行,响应拦截器会在数据返回到业务代码之前执行。这两个关卡就是统一处理逻辑的最佳位置。

请求拦截器最常见的用途就是带上身份凭证。比如登录成功后后端发了一个token,之后每个接口需要在请求头里带Authorization字段。如果不用拦截器,你得在每个请求里手动加;用了拦截器,一行代码解决所有请求。

request.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); } );

这段代码里有个关键点:config是一个对象,你可以在里面修改headers、params、data等任何配置项,但最后必须return config,否则请求发不出去。很多初学者忘了这个return,排查半天不知道问题在哪。

拦截器里还可以做加时间戳防缓存、统一给特定接口加签名、上报埋点这些操作,但我的建议是别堆太多逻辑进去。我知道有个项目把权限判断、数据格式转换、埋点上报全都塞进了请求拦截器,结果每次发请求要跑一串逻辑,出问题了还不好定位。拦截器适合做轻量级的统一处理,重逻辑应该拆到各自的模块里。

3.2 响应拦截器实战

响应拦截器比请求拦截器更实用,因为后端返回的数据结构往往不是直接可用的,需要统一“解包”。大部分后端会返回类似这样的结构:{ code: 0, message: 'success', data: { ... } }。code为0表示成功,非0表示业务失败。

request.interceptors.response.use( response => { const res = response.data; if (res.code === 0) { return res.data; } // 业务失败,统一提示 alert(res.message || '请求失败'); return Promise.reject(new Error(res.message || '请求失败')); }, error => { // 404、500、超时这些HTTP层面的错误走这里 const status = error.response?.status; if (status === 401) { // 登录状态失效,跳转登录页 localStorage.removeItem('token'); window.location.href = '/login'; } else { alert(error.message || '网络异常'); } return Promise.reject(error); } );

这样做了以后,业务代码里就清爽多了,不用每个接口都去判断code。比如一个获取用户信息的接口,调用处可以直接这样写:

const userInfo = await request.get('/api/user/info'); console.log(userInfo.name);

注意,拦截器里我把res.data直接返回了,所以上面拿到的userInfo就是后端data字段里的内容,不是整个响应对象。

这种“统一解包”的做法有个前提:后端接口的结构约定是一致的。如果有的接口直接返回数组,有的返回对象,有的code在data里,那解包逻辑就得写兼容分支。我建议这类事情要尽早跟后端对齐约定,别等到前端代码里到处写兼容再后悔。

4. 实际项目中的应用场景与封装方案

4.1 统一的API层封装

在实际项目中,不要在每个组件里直接调axios发请求,而是做一个API层,把接口地址和调用方式集中管理。好处是接口路径统一维护、复用方便、后端改路径只需改一处。

我常用的目录结构是:

src/ api/ request.js // axios实例与拦截器配置 user.js // 用户模块接口 order.js // 订单模块接口

request.js负责创建实例、配置拦截器,然后导出这个实例。user.js和order.js里写具体的接口函数:

import request from './request'; // 获取用户信息 export function getUserInfo() { return request.get('/api/user/info'); } // 修改用户资料 export function updateUser(data) { return request.put('/api/user/update', data); }

组件里这样调用:

import { getUserInfo } from '@/api/user'; const submit = async () => { const data = await getUserInfo(); console.log(data); };

这套方案我用了很久,体验下来有几个明显的好处:第一,接口改动时只动api目录下的文件;第二,接口列表一目了然,方便review;第三,做单元测试时可以轻松mock这一层。

4.2 请求取消与防重复提交

先说说请求取消。axios里取消一个请求是用AbortController或者CancelToken(新版本推荐前者)。适用的场景很有意思——用户在某个页面加载列表数据,突然切走了,这会让旧页面的请求白跑一遍,甚至出现“在旧页面弹了报错提示”的尴尬情况。

import axios from 'axios'; const controller = new AbortController(); axios.get('/api/long-task', { signal: controller.signal }).then(res => { console.log(res); }).catch(err => { if (axios.isCancel(err)) { console.log('请求已取消'); } else { console.error(err); } }); // 用户离开页面时取消请求 controller.abort();

再说防重复提交。用户在提交表单时可能会因为网络慢而多次点击提交按钮,导致后端收到多条相同的数据。前端可以在请求发出前加一个“标记”,请求完成或失败后移除标记。实现方式不复杂,但要注意并发场景下的正确性。

const pendingRequests = new Map(); function addPending(config) { const key = `${config.method} ${config.url}`; const controller = new AbortController(); config.signal = controller.signal; if (!pendingRequests.has(key)) { pendingRequests.set(key, controller); } } function removePending(config) { const key = `${config.method} ${config.url}`; if (pendingRequests.has(key)) { pendingRequests.get(key).abort(); pendingRequests.delete(key); } }

这个思路是:相同method跟url的请求在途时,后面重复发起的请求会被拦截。不过要小心一个坑——如果不同的请求参数不同但url相同,用url做唯一标识会误伤。更严谨的做法是把参数也拼进key里面,但逻辑会复杂一些,团队里要根据实际情况取舍。

5. 常见问题与排查技巧

5.1 跨域、超时、401这些高频问题

跨域问题是前端用axios最常见的报错之一。出现跨域的原因是浏览器的同源策略,它要求请求的域名、端口、协议必须一致。解决办法有几种:后端配置CORS(推荐),前端用代理转发(开发环境常用),或者用JSONP(但现在用得少了,而且只支持GET)。

开发环境用代理的话,路径设置要注意。比如Vite或webpack的devServer可以配置proxy,这样你在axios里请求的相对路径会转发到目标服务。不要把target直接写在baseURL里,否则代理就不生效了。

// vite.config.js 中的配置片段 server: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true } } }

这时候axios的baseURL写成'/api'就行了,浏览器看到的请求是发到本地的,实际上被代理转发到了目标服务器。

超时问题,除了前面提到的调整timeout以外,还要留意后端的设计。如果后端是同步计算,耗时过长没响应,前端超时了但后端还在跑,这会导致两边状态不一致。更合理的做法是:后端把耗时任务改造成异步,先立刻返回“任务已接收”,前端再通过轮询或WebSocket获取结果。

401的坑在于有些接口在未登录状态下也会返回数据,有些接口则不会,所以不能一刀切处理所有401。我的做法是先跟后端确认哪些接口是“公开接口”(不需要带token也能访问),再在拦截器里排除这些接口的401提示。

5.2 冷门但关键的小坑

这些坑是我在实际项目中一次次踩过才记住的。

第一个是重复发送的请求。axios底层用的是XHR,本身没有自动去重的功能。如果你在循环里调接口,或者组件在短时间内被同时触发多次,请求会被发送多次。除了防重复提交,还要注意组件卸载后setState的报错——“Can't perform a React state update on an unmounted component”。这种报错通常不是axios的问题,而是你在请求返回后调用了setState,但组件已经卸载了。

解决方法是:在组件卸载时取消请求,或者用标志位判断——组件还挂载着才更新状态。这跟“请求取消”是一套思路。

第二个是文件上传时的Content-Type。上传文件一般用FormData,不要手动设置Content-Type为multipart/form-data,因为这样会让浏览器少一些边界参数,反而是错的。让axios自己设置更好。但如果你用了别的库或者自己拼请求,就必须注意这一点。

const formData = new FormData(); formData.append('file', fileInput.files[0]); axios.post('/api/upload', formData, { // 这里不要手动设置 Content-Type }).then(res => { console.log(res.data); });

第三个是下载文件时要设置responseType为blob。如果后端返回的是文件流,你没有设置这个参数,拿到的data会是一堆乱码或者被解析成JSON失败。设置responseType之后,再接一个Blob对象创建下载链接:

axios.get('/api/export', { responseType: 'blob' }).then(res => { const blob = new Blob([res.data]); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = '导出数据.xlsx'; a.click(); URL.revokeObjectURL(url); });

下载文件名如果后端在响应头里通过Content-Disposition返回了,前端可以解析出来,但不同的浏览器对这个响应头的暴露策略不太一样,跨域时可能读不到。稳妥的做法是让后端直接返回文件名给前端。

第四个是拦截器里的错误处理要小心不要二次弹窗。比如响应拦截器里已经对401做了跳转登录页的处理,业务代码的catch里又写了一套401处理,结果就会弹两次提示或者执行两次跳转。我的经验是:HTTP层级的错误在拦截器统一处理完以后,业务代码的catch里只处理业务逻辑错误,两边分工明确。

5.3 排查思路速查表

我把遇到的高频问题整理成一张表,排查的时候可以对照着看:

现象可能原因排查方向
请求一直pending跨域问题、后端未响应、代理配置错误看浏览器Network面板,有没有CORS报错
响应返回415Content-Type设置不对检查headers里的Content-Type,JSON和表单数据别混用
响应返回401token缺失或过期检查请求头有没有带Authorization,token是否过期
数据是字符串但被当成对象后端返回了字符串,Content-Type不对看响应头的Content-Type,确保是application/json
请求发出去但没有响应数据后端抛异常但没返回结构化错误让后端同事打印日志,用Postman验证接口本身是否正常
页面代码没报错但数据不更新响应拦截器把业务错误吞了检查拦截器是不是把Promise.reject掉了,或者resolve了错误数据

这张表不能覆盖所有情况,但它是一个很好的起点。遇到问题先别急着改代码,先看Network面板确认请求有没有发出去、状态码是多少、响应体是什么,再定位是前端、后端还是网络链路的问题。

6. 顺手再推荐几个配套工具

实际开发中,光靠axios本身还不够,我常用的几个配套能力给大家做个参考。

第一个是调试工具。浏览器自带的开发者工具Network面板是排查问题的第一站,安装一个接口调试扩展(比如常见的API调试工具)可以更方便地模拟请求、保存请求历史、生成代码片段。我个人习惯是在浏览器里先验证接口能通,再回前端代码里排查问题,这样能快速区分“接口问题”和“代码问题”。

第二个是请求日志埋点。在某些业务场景下,你可能需要记录每个请求的耗时、成功失败情况。这可以在拦截器里做:请求发出去时记录开始时间,响应回来后计算耗时,上报到日志系统。这样做出的效果,产品反馈“最近接口变慢了”时,你能拿出数据确认到底是哪个接口慢了,而不是靠猜。

request.interceptors.request.use(config => { config.metadata = { startTime: Date.now() }; return config; }); request.interceptors.response.use( response => { const elapsed = Date.now() - response.config.metadata.startTime; console.log(`请求耗时: ${elapsed}ms`); return response; }, error => { console.error('请求失败', error.config?.url); return Promise.reject(error); } );

第三个是环境变量管理。不同环境(开发、测试、生产)的API地址不一样,不要把地址写死在代码里。可以放在环境变量文件里,比如.env.development、.env.production,然后通过import.meta.env或process.env读取。这样打包不同环境时,不需要改代码,只需要选择对应配置即可。

我实际操作中还有个小习惯:在request.js里写一个getBaseURL函数,根据当前环境返回对应域名,这比在多个文件里散落判断要好维护得多。

7. 结尾分享点个人体会

最后说几句实在话。axios这个库看起来简单,但真正用好它,需要你对整个前后端交互的链路有清晰认识——从请求的发出、数据的传递、异常的处理,到安全性的考虑,每一步都有讲究。我在项目里见过不少同事,axios用得“能跑”但谈不上“好用”,遇到问题就靠alert弹窗调试,一旦碰到跨域、token失效、并发冲突这些场景就抓瞎。

我的建议是,别急着把代码写复杂。先把拦截器这块吃透,再把请求取消和防重复提交这两个能力掌握,你的前端交互能力就已经超过大部分人了。剩下的经验,都是在真实的业务场景里踩坑积累出来的。这篇就当是给你铺了一条路,路怎么走,还得看你自己。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进