axios从入门到封装:拦截器机制与Vue devServer代理实战
2026/9/9 8:53:54 网站建设 项目流程

刚入行那会儿,我被面试官问过一个问题:原生fetch已经这么好用了,为什么项目里还要用axios?当时我答得磕磕绊绊,后来自己在项目里把axios从零封装到生产环境,才算真正想明白这件事。axios这个库,表面上是“一个发请求的工具”,但深入进去你会发现,它真正的价值在于一套完整的拦截器机制和统一错误处理体系。这篇文章我就站在一个干过多年前端、踩过不少坑的角度,把axios从基础API到项目级封装,再到Vue开发环境下的devServer转发,一次讲透。不管你是刚接触前端的新手,还是想系统梳理基础知识的老手,这篇都适合你。

1. axios到底是什么,为什么前端项目里离不开它

1.1 先搞清楚axios的定位

axios是一个基于Promise的HTTP客户端,可以在浏览器和Node.js两端同时使用。浏览器端它底层封装的XMLHttpRequest,Node端走的是http模块,对外暴露的API却完全一致。这点非常关键——写一套代码,两端通用,这在当时是很多请求库不具备的优势。

它解决的核心问题可以归结为几个词:统一、可控、可扩展。

  • 统一:所有请求都走同一个库的同一套API,不会出现一个项目里既有fetch又有XMLHttpRequest的混乱局面。
  • 可控:请求拦截器、响应拦截器、全局配置、取消请求、超时控制,这些能力让你能在“发出请求前”和“拿到响应后”这两个关键节点上插入自己的逻辑。
  • 可扩展:通过实例化、配置项组合,可以轻松管理多域名请求、多套鉴权逻辑,甚至切换不同的后端服务。

说白了,不用axios,你用fetch自己封装,也完全可以实现类似能力,但你要自己造轮子的东西太多了:全局loading管理、token注入、错误码统一处理、重复请求取消、下载进度回调……每一样都要自己写,而且容易写出各种边界问题。axios把这些能力内置了,你只需要学会怎么用。

1.2 axios和原生fetch,到底该选哪个

这个问题在团队讨论里经常出现,尤其是Next.js项目里,很多人会用原生fetch封装一层拦截器。我的观点一直很明确:如果项目是React/Vue传统SPA,或者有频繁的拦截逻辑需求,优先选axios;如果项目是Next.js服务端组件场景,追求最小依赖,可以考虑fetch封装。但不能盲目选,要从几个维度看清两者的真实差距。

对比维度axios原生fetch
拦截器内置请求/响应拦截器,开箱即用无内置,需自己包装请求函数
超时控制timeout配置项,简单粗暴需借助AbortController手动实现
错误处理非2xx状态码自动进入catch分支只在网络异常时reject,4xx/5xx需要手动判断res.ok
请求取消支持CancelToken和AbortController只能AbortController
上传/下载进度支持onUploadProgress/onDownloadProgress不支持,需要ReadableStream处理
自动JSON转换自动JSON.stringify和JSON.parse需要手动调用res.json()
兼容性兼容老版本浏览器较新浏览器才完整支持

你注意看最后一点,axios默认把HTTP响应中不正常的处理掉了,比如403、500这类状态,在axios里会直接进reject,你可以集中处理。而fetch就不一样,它只在网络层面断掉的时候才会reject——也就是说服务器返回一个500,fetch照样resolve,你得自己检查res.ok。这个差异直接决定了业务代码的写法。

在实际项目里,你还要考虑一件事:团队里如果有人写代码比较随手,axios的拦截器机制能够强制统一请求的出入参格式,降低出问题的概率。而fetch封装如果做得不够完善,每个人写的请求代码很容易风格不一。

2. 核心API与拦截器机制:axios的灵魂拆解

2.1 常用API和请求配置,先过一遍

axios的基本API不需要死记硬背,记住一个核心原则:它是围绕“配置项”设计的。无论你是axios.get还是axios.post,最终都是对配置项的不同封装。

// 最基本的GET请求 axios.get('/user?id=123') .then(res => console.log(res.data)) .catch(err => console.log(err));

也可以把参数放在config里传:

axios.get('/user', { params: { id: 123 }, timeout: 5000, headers: { 'X-Token': 'abc' } });

POST请求同理,axios.post(url, data, config)的第二个参数是请求体,第三个参数是配置:

axios.post('/login', { username: '张三', password: '123456' }, { headers: { 'Content-Type': 'application/json' } });

这里有个易错点:如果你用params传参,axios会把它拼到URL的query string里;如果你用data传参,则会放到请求体里。GET请求没有请求体,所以GET只能靠params传参。这是很多新手第一次调接口报错的原因——后端明明说“我收到了参数”,你这边却一直拿不到。

再说并发请求。axios提供了axios.all方法,但本质上是Promise.all的封装,现在完全可以直接用Promise.all,没有区别:

const [userRes, configRes] = await Promise.all([ axios.get('/user'), axios.get('/config') ]);

然后是一个很多人不常用但很实用的API:axios.create()。它能创建一个独立的实例,拥有自己独立的默认配置和拦截器。处理多域名请求时,这个能力是救命的:

const serviceA = axios.create({ baseURL: 'https://api-a.example.com', timeout: 10000 }); const serviceB = axios.create({ baseURL: 'https://api-b.example.com', timeout: 30000 }); // serviceA和serviceB互不干扰,各自的拦截器也是独立的

在多后端服务架构的项目里,单独一个axios实例很容易在配置上打架。拆成两个实例,每个实例处理自己的鉴权逻辑和超时时间,代码清晰不少。

2.2 拦截器机制:为什么说它是axios的灵魂

拦截器是axios最核心的设计,可以理解为“请求流水线上的两道关卡”。请求发出前,会经过请求拦截器;响应回来后,会经过响应拦截器。每一道关卡你都能改数据、拦数据、做副作用操作。

请求拦截器最常见的三个用途:

  1. 注入鉴权信息:把token从store里取出来,挂到headers上。
  2. 统一增加请求参数:比如加时间戳防止GET请求缓存、加客户端类型标识。
  3. 开启全局loading:发请求前展示loading动画,请求结束后关闭。

响应拦截器最常见的三个用途:

  1. 统一解包:后端返回的数据结构通常是{code, msg, data},拦截器里直接return res.data.data,业务层拿到的就是干净数据。
  2. 统一错误处理:HTTP状态码为200但业务code非0,或者HTTP状态码直接401/500,都在这一层统一弹提示。
  3. 登录失效跳转:遇到401状态码,自动跳转登录页,同时清除本地登录状态。

看一段实际的代码:

// 请求拦截器 service.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( response => { const res = response.data; // 业务错误码统一处理 if (res.code !== 0) { if (res.code === 401) { // 登录失效,跳转登录页 window.location.href = '/login'; } return Promise.reject(new Error(res.msg || '请求失败')); } return res.data; }, error => { // HTTP层面的错误,比如404、500、超时 return Promise.reject(error); } );

这里有一个很容易踩的坑,必须提一下:拦截器的执行顺序。请求拦截器是从先到后执行,也就是先添加的先执行;响应拦截器是从后到先执行,后添加的先执行。如果你在应用里又加了一个第三方插件也用了拦截器,顺序就很容易出问题。排查思路很简单:把添加拦截器的代码按依赖顺序排列,不要东一个西一个。

2.3 配置优先级和错误对象结构

axios的配置优先级是:请求配置 > 实例配置 > 全局默认配置。也就是说,你单独给某个请求传的timeout、headers,会覆盖实例和全局的配置。

axios.defaults.timeout = 5000; // 全局:5秒 const service = axios.create({ timeout: 10000 }); // 实例:10秒 service.get('/user', { timeout: 3000 }); // 当前请求:3秒,最高优先级

还有一点要注意,headers并不是整体覆盖,而是字段级别合并。比如实例里配了X-Token和X-Version,请求里只传了X-Token,X-Version依然会保留。这个设计在多层配置下减少了重复代码,但也容易让人困惑——万一某次请求想不带某个header,直接传null不一定生效,要用delete或者单独赋值undefined。

错误对象的结构也要看懂。axios抛出的错误是AxiosError,它上面有以下几个关键字段:

  • error.message:标准错误消息,比如“timeout of 3000ms exceeded”。
  • error.config:发起请求的配置对象,排查问题时很有用。
  • error.request:浏览器端的XMLHttpRequest实例,或Node端的http.ClientRequest。
  • error.response:响应对象,包括status、statusText、headers、data。

判断是不是超时、是不是网络问题,你就去检查response存不存在:

error.response ? console.log('请求已发出,但服务器返回了非2xx状态,状态码:', error.response.status) : error.request ? console.log('请求已发出,但没有收到响应,可能被取消或网络异常') : console.log('在创建请求时就出错了');

这段逻辑在实际开发中很有用,但大家写代码时很少拆这么细。我自己的习惯是在catch里分三类打日志,后端排查问题时会拿着日志直接定位,效率高很多。

3. 手动封装axios:企业项目的标准做法

3.1 为什么要封装,封装了哪些东西

很多新手的做法是,在页面里直接import axios,然后axios.get一把梭。这种写在功能验证阶段没问题,但项目一复杂就会爆雷:几十个页面里分散着无数axios.get,每个都自己配baseURL、自己处理错误、自己写loading逻辑,后期想全局改个超时时间,你得上百个文件里改。

封装的本质就三个字:收口子。把所有请求相关的逻辑集中在一个模块里管理,页面只管调用统一的API函数,不关心底层细节。这样做有几个明确的好处:

  1. 请求配置集中管理:baseURL、timeout、headers、跨域相关配置,只写一次。
  2. 拦截器逻辑可维护:token注入、错误码处理、loading控制都在一个文件里,改动影响面可控。
  3. 方便切换请求库:今天用axios,明天想换fetch或者轻量封装,只需要改封装层,业务代码一行不动。
  4. IDE提示友好:封装好的api方法可以挂类型,业务开发者只需要看方法签名就知道入参和出参。

3.2 一个可以直接抄作业的封装模板

下面这个封装模板是我在多个项目里沉淀下来的,结构简单,但能覆盖绝大部分业务场景。整体分为三层:创建实例、拦截器设置、模块化API导出。

第一步,创建实例并配置基础信息:

// request.js import axios from 'axios'; const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL || '/api', timeout: 15000, withCredentials: false });

baseURL这里值得展开说。如果你在开发环境用了devServer代理,baseURL通常设为代理前缀,比如/api;生产环境如果直接同域部署,baseURL可以留空或设为相对路径。如果在环境变量里管理,不同环境自动切,这是最优雅的。

第二步,设置拦截器:

// 请求拦截器 service.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = 'Bearer ' + token; } // GET请求参数加时间戳,防止缓存 if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; }, error => Promise.reject(error) ); // 响应拦截器 service.interceptors.response.use( response => { const res = response.data; // 二进制数据直接返回,比如文件下载 if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') { return response; } if (res.code !== 0) { if (res.code === 401) { localStorage.clear(); window.location.href = '/login'; return Promise.reject(new Error('登录状态已过期')); } // 这里可以做统一Toast提示,比如vue项目里引入Element Plus消息组件 return Promise.reject(new Error(res.msg || '接口异常')); } return res.data; }, error => { let message = ''; if (error.response) { switch (error.response.status) { case 401: message = '登录失效,请重新登录'; localStorage.clear(); window.location.href = '/login'; break; case 403: message = '没有权限访问'; break; case 404: message = '接口不存在'; break; case 500: message = '服务器内部错误'; break; default: message = '请求失败(' + error.response.status + ')'; } } else if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) { message = '请求超时,请稍后重试'; } else if (!navigator.onLine) { message = '网络连接已断开'; } else { message = '网络异常,请稍后重试'; } // 统一提示 return Promise.reject(error); } );

第三步,统一导出常用方法并挂载泛型。这一步在TypeScript项目里尤为重要:

// api.ts 或者 request.ts 继续扩展 export const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> => service.get(url, { params, ...config }); export const post = <T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> => service.post(url, data, config); export const put = <T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> => service.put(url, data, config); export const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> => service.delete(url, { params, ...config });

这样写完之后,业务代码变成这样:

import { get } from '@/utils/request'; interface UserInfo { id: number; name: string; avatar: string; } // 直接拿到UserInfo类型,不用每次手动声明res.data.data const user = await get<UserInfo>('/user/info', { id: 123 });

第四步,按模块管理API:

// api/user.ts import { get, post } from '@/utils/request'; export const getUserInfo = (id: number) => get('/user/info', { id }); export const updateUserInfo = (data: object) => post('/user/update', data);

页面调用时直接import具体方法,语义化清晰,后端接口地址集中在api目录里,改动时好找。这种模块化管理在多人协作的项目里收益尤其明显——后端改了接口路径,只要在api目录里全局替换,不会漏改。

3.3 踩过的几个坑:拦截器返回类型、loading计数、取消请求

封装axios看起来简单,但真跑起来还是会遇到一些隐蔽问题。我把自己踩过的坑整理一下。

第一个坑是拦截器里返回类型不对。很多初学者在响应拦截器里只return数据,不return Promise,导致业务层拿到的res.msg是undefined或者整个链断裂。记住一条原则:拦截器函数内部,要么return一个值把它传给下一层,要么return Promise.reject(error)把它抛出去,不要什么都不返回。如果用了async/await,还要注意await的返回值不要忘写return。

第二个坑是多个并发请求的loading闪烁。如果你在请求拦截器里开启一个全局loading,在响应拦截器里关闭它,两个请求同时发出去,第一个请求回来就把loading关了,页面闪一下。解决办法是做一个loading计数器:

let loadingCount = 0; function showLoading() { if (loadingCount === 0) { // 显示loading } loadingCount++; } function hideLoading() { loadingCount--; if (loadingCount <= 0) { loadingCount = 0; // 隐藏loading } }

第三个坑是取消请求的处理。在搜索框场景下,用户每输入一个字符就发一个请求,如果上一个请求还没回来,应该把上一个取消掉。axios提供了两种取消方式:老版本的CancelToken和新的AbortController。新的方式在浏览器端的支持更标准,推荐直接用AbortController:

const controller = new AbortController(); service.get('/search', { signal: controller.signal }); // 发起下一个请求前,先取消上一个 controller.abort();

注意,取消请求触发的错误是Cancel,在响应拦截器的reject分支里可以用axios.isCancel(error)判断,如果被取消了就直接返回,不要弹错误提示:

error => { if (axios.isCancel(error)) { return Promise.reject(error); // 不弹提示 } // 其他错误正常处理 }

3.4 Next.js场景里,fetch封装和axios拦截器怎么选

看热词里很多人在讨论Next.js里用原生fetch还是axios拦截器。这个话题分两层:服务端组件和客户端组件。

Next.js服务端组件下,其实不太建议用axios。原因有两个:服务端组件本身是运行在Node环境,直接使用原生的fetch不仅不增加依赖,还能利用Next.js自带的fetch扩展能力——比如数据缓存、重新验证(revalidate)。这一层能力axios是没有的,强行用axios反而绕过了框架的最优解。

而在客户端组件里,比如需要交互的评论框、搜索功能,这类场景下axios的拦截器优势依然明显。因为你要处理token注入、登录失效跳转、统一错误提示,这些是典型的“客户端业务逻辑”,跟框架没有直接关系。我在一个Next.js混用项目里的做法是:服务端的数据获取用原生fetch,客户端的交互请求走封装好的axios实例。两边各自干自己擅长的事,效果很好。

// Next.js服务端组件示例 async function getServerData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 60 } // 60秒缓存 }); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); } // 客户端组件,使用封装的axios 'use client'; import { get } from '@/utils/request'; const handleSearch = async (keyword: string) => { const list = await get('/search', { keyword }); setList(list); };

总结就一句话:服务端优先用fetch,客户端交互优先用axios。没必要在服务端强行套axios,也不要在复杂客户端逻辑里用裸fetch手写拦截器。

4. Vue项目里接入axios与devServer转发全流程

4.1 在Vue项目中安装和接入axios

Vue 2老项目通常用vue-axios插件方式挂载,Vue 3项目直接npm包引入即可。推荐的做法是模块引入,而不是挂载到Vue原型上。

npm install axios

然后在项目src/utils/request.js里封装,如上面第三节的例子。你不需要在main.js里做任何额外操作,业务组件里直接import封装好的方法就完事了。

很多老教程会让你在Vue原型上挂axios:

Vue.prototype.$http = axios;

这个写法在Vue 3里已经取消了,而且在Vue 2里也不推荐。原因很简单:挂在原型上,组件里用this.$http调用,无法获得IDE的类型提示,也不好做模块替换。直接import的方式更利于按需引入、tree-shaking、类型推导。

4.2 开发环境的跨域问题,devServer转发到底解决的什么问题

这个问题几乎每个Vue前端都会碰到。你本地开发环境,前端跑在http://localhost:8080,后端接口跑在http://localhost:3000,浏览器直接发请求就会遇到跨域。浏览器同源策略限制了不同端口之间的请求,这时候你有两个选择:

  1. 后端开启CORS,在响应头里加Access-Control-Allow-Origin。
  2. 前端用devServer的proxy做代理转发,让浏览器的请求先发给同源的devServer,再让devServer把请求转发给后端。

第二种方案在本地开发中更常见,原因很简单:CORS配置经常需要后端配合改,而且不同环境的后端地址不一样;proxy配置只改前端文件,不需要动后端代码。

devServer转发的原理可以这样理解:你的浏览器和webpack-dev-server是同源的(都是localhost:8080),浏览器把请求发给了devServer,然后devServer作为服务端去请求真正的后端(localhost:3000)。服务端之间的请求是没有同源限制的,所以能拿到数据,devServer再把数据原样返回给浏览器。整个过程中,浏览器只和同源地址通信,跨域问题就被绕过了。

4.3 手把手配置vue.config.js的proxy

Vue项目的devServer配置在vue.config.js中:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };

这里有个特别容易搞错的配置项,pathRewrite。很多新手不明白为什么要有这一步。假设你的axios请求地址是axios.post('/api/login'),devServer看到请求路径以/api开头,就把它转发给target指定的地址。如果后端接口实际路径是/login而不是/api/login,你就需要用pathRewrite把/api前缀剥掉。如果后端接口本身就带/api这个前缀,那pathRewrite就可以不写,或者写成什么都不替换。

我举个例子:

  • 前端请求:/api/login
  • 后端实际接口:/login
  • 那么:pathRewrite: { '^/api': '' },转发后变成http://localhost:3000/login

如果后端实际接口是/api/login,那proxy配置改成:

proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true // 没有pathRewrite,原样转发 } }

changeOrigin这个配置也要说清楚。它控制的是转发请求时Host头部的值。默认false时,Host头是localhost:8080(前端地址);改为true后,Host头会变成target的地址。如果后端做了域名白名单校验,比如只允许来自api.example.com的请求,而你的target恰好是api.example.com,那就必须开changeOrigin: true,否则会被后端拒绝。

另外,我用Vite的时候配置方式是vite.config.js里的server.proxy,写法上大同小异:

// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } };

4.4 devServer转发和axios的baseURL怎么配合

这是实际开发中最容易让人迷惑的地方。我见过太多人为这个问题排查了半天。核心原则是:axios的baseURL要和proxy的路径前缀保持一致。

假设你的proxy配置了/api前缀,那么axios的baseURL就设为'/api'。这样你请求axios.post('/api/login'),实际发送到devServer的地址为http://localhost:8080/api/login,然后devServer根据proxy规则把它转发给http://localhost:3000/login。

如果你在axios里把baseURL直接设为'http://localhost:3000',那请求就直接发到后端了,完全没有经过proxy,跨域问题依然存在。这就相当于你的proxy配置白写了。

我总结一个简单的对照表:

前端请求axios baseURLproxy前缀实际转发到
/login''http://localhost:3000/login
/api/login'/api'/apihttp://localhost:3000/login
/api/login''/apihttp://localhost:3000/api/login
/api/login''直接跨域,报错

生产环境则不一样,通常是Nginx反向代理同域部署,不需要配CORS,也不需要proxy。所以你在封装的request.js里,通常这样写baseURL:

const service = axios.create({ baseURL: process.env.NODE_ENV === 'development' ? '/api' : 'https://api.example.com' });

或者用环境变量的方式管理,不同构建环境自动切换,更加省心。

4.5 本地方案:Node端也可以配置代理转发

连Vue项目都用Vite或Webpack的devServer,Node端——比如Express写一个简单的开发服务器——也可以加代理转发。原理一样,用http-proxy-middleware就可以:

const { createProxyMiddleware } = require('http-proxy-middleware'); app.use('/api', createProxyMiddleware({ target: 'http://localhost:3000', changeOrigin: true, pathRewrite: { '^/api': '' } }));

如果你的项目是一个纯Node构建的SSR服务,这种配置方式很实用。核心思路和devServer的proxy完全一样,理解原理后可以灵活迁移。

5. 开发中最常见的axios问题排查实录

5.1 排查思路:先确定问题发生在哪个环节

前端请求链路说长不长,但也分好几个环节:请求创建 -> 请求拦截器 -> 发出请求 -> 代理转发(开发环境) -> 后端处理 -> 响应拦截器 -> 业务代码。遇到请求类问题,我的排查习惯是逐层缩小范围,先在Network面板里确认请求是否真的发出去了,再确认请求地址是否正确、响应状态码是多少、返回的数据是什么。这一步能筛掉很多低级错误。

比如一个典型的“请求404”问题,先看Network面板里请求的URL,再对比后端接口文档。如果URL和我们预期的一致,那就是后端接口路径不对;如果不一致,那就是axios的baseURL或请求路径拼接出了问题。

5.2 高频问题速查表

我整理了一份实战中非常高频的问题速查表,方便在群里救火。

问题现象可能原因排查方法
请求一直Pending 不返回devServer代理没生效、后端接口挂了、请求被浏览器拦截先看Network面板,确认请求地址是否正确;再看Console有没有报错
请求返回404路径写错、proxy pathRewrite配置错误对比接口文档;临时关掉pathRewrite看看转发路径
GET请求数据被缓存浏览器缓存或后端缓存请求拦截器加时间戳参数;或后端设置Cache-Control: no-cache
token带上了但后端说没收到header名称不一致,比如后端要X-Token你写成了Authorization到后端日志确认收到的header;统一header命名
业务code非0但页面没有报错提示后端返回了200,拦截器返回了res.data,业务代码忽略了code检查响应拦截器是否做了code判断;确保全局错误提示已接入
401之后重复弹跳转多个请求同时回401,每个请求都执行了跳转在拦截器里用标志位防止重复跳转
下载文件时拿到的是一堆乱码未设置responseType: 'blob'在下载请求里显式指定responseType
页面显示加载中一直转圈请求失败了但loading没有被关闭检查响应错误分支是否正确调用了hideLoading
生产环境无法访问接口Nginx没有做反向代理或CORS配置不对用curl直接访问接口地址,确定后端服务是否可达

5.3 几个我亲历过的疑难case

Case 1:token失效后并发请求连环跳转

这个场景非常经典。用户在页面上一个动作触发了三个并发请求,三个请求都带着过期的token,后端都返回401。响应拦截器里三个请求各自执行一次跳登录页,结果页面疯狂刷新,用户体验极差。

解决方案是加一个“是否正在处理401”的标志位:

let isHandling401 = false; async function handleUnauthorized() { if (isHandling401) return; isHandling401 = true; localStorage.clear(); // 跳转前可以先尝试静默刷新token,刷新失败才跳登录 window.location.href = '/login'; }

如果后端支持refresh token机制,可以更完善:第一次401时,用refresh token换新token,把并发中的其他请求暂存起来,等token刷新成功后再重放。这个方案实现起来复杂一点,但在中大型后台工具体系里值得做。

Case 2:拦截器里用了async/await,结果data丢了

这个坑很隐蔽。在请求拦截器里,如果你用了async/await做某些异步操作:

service.interceptors.request.use(async config => { await getConfig(); // 异步操作 config.headers['X-Custom'] = '123'; return config; });

这段代码看起来没问题,但async返回的是一个Promise,axios内部是会等待这个Promise resolve的。问题出在某些旧版本axios上,或者是在响应拦截器里误用了async/await,导致返回的Promise链断裂。排查这个问题的关键是用console.log在拦截器入口和出口各打一次,看数据流在哪一步断了。

Case 3:代理配了,但请求根本没走代理

有一次,我用Vue CLI起项目,配好了devServer.proxy,但请求还是直接打到了后端的绝对地址上,浏览器直接跨域报错。排查完发现,项目里axios的baseURL写死了后端地址,比如'http://localhost:3000',根本没有走相对路径。后来把baseURL改成'/api',问题就解决了。绝大多数代理不生效的问题,都是这个原因。

Case 4:axios取消请求后,loading没关

用AbortController取消了一个请求,响应拦截器的reject分支会被触发。如果你在reject分支里直接return Promise.reject(error),而没有调用hideLoading,loading就会一直转圈。所以取消请求的错误处理里,一定要记得关闭loading。

5.4 排查工具和习惯

分享两个好用的排查习惯。第一个是在响应拦截器里加一个“环境判断”,如果处于开发模式,就把请求信息打印到控制台:

service.interceptors.response.use( response => { if (process.env.NODE_ENV === 'development') { console.log(`[API] ${response.config.method.toUpperCase()} ${response.config.url}`, response.data); } return response.data; } );

这个习惯能让你在开发时直接看到每个接口的出入参,不用时刻盯着Network面板。

第二个习惯是给请求加唯一标识。在config对象里塞一个私有字段,比如config._name = 'getUserInfo',这样在错误日志里你一眼就能看出是哪个接口出了问题,不用靠URL去猜。

6. 最后一个实用小技巧:结合TypeScript让axios更好用

axios本身是自带TypeScript类型定义的。如果你的项目用了TypeScript,可以进一步让封装更完善。

可以给业务模块的API方法写清晰的返回类型:

import { get, post } from '@/utils/request'; export interface LoginParams { username: string; password: string; } export interface LoginResult { token: string; userInfo: { id: number; name: string; }; } // 调用方直接得到LoginResult类型 export const login = (data: LoginParams) => post<LoginResult>('/login', data);

这样在业务代码里,login方法调用后的返回值就有完整的类型提示,不会出现“接口返回了但我在代码里不知道里面有什么字段”的窘境。这一点在多人协作时价值巨大:后端改了字段,前端编译期就能报错,不用等运行时才发现。

另外,如果你需要处理文件上传,axios也提供了标准方案:

const uploadFile = (file) => { const formData = new FormData(); formData.append('file', file); return post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); };

在实际项目中,我很少手动设置Content-Type,直接把formData传给axios,它会自动帮我们设置正确的Content-Type并生成boundary。手动去设置反而容易出问题。

做文件下载时,记得要指定responseType: 'blob':

export const downloadFile = (fileId) => service.get('/file/download', { params: { fileId }, responseType: 'blob' });

拿回来的是Blob对象,需要自己借助URL.createObjectURL生成临时链接触发下载。如果不设responseType,浏览器会尝试把二进制当JSON解析,出来的就是乱码。

我在实际项目里折腾axios折腾了几年,最大的体会是:这个库的API设计非常简洁,但真正好用是要靠“封装”去触发的。你花半天时间把请求层梳理清楚,后面所有业务开发都会受益。面试的时候,如果有人问你“axios拦截器怎么用”,你能把请求拦截器、响应拦截器、配置优先级、错误对象、取消请求、代理转发这些点串起来讲清楚,再讲一两个自己踩过的坑,这一题基本就稳了。希望这篇文章能帮你在axios这条路上少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询