资讯动态

Axios拦截器封装与前端请求层实践:从Vue到Next.js的选型与踩坑

发布时间:2026/9/9 7:58:50 来源:尧图企业网站定制
1. 为什么前端项目离不开 Axios先把话说在前面Axios 这个库凡是有过前后端联调经验的人都绕不开。我自己整理笔记的起因很简单就是团队里新来的同事拿着this.$http.get和fetch混着写一个项目里请求方式五花八门最后排查接口问题的时候谁也不知道该去哪找统一逻辑。于是我索性把一套封装思路和踩坑记录整理成文这就是这份 Axios 笔记的由来。Axios 的本质是一个基于 Promise 的 HTTP 客户端浏览器端底层走 XMLHttpRequestNode 端走 http 模块。它能帮你做的事概括起来就是发起请求、转换数据、拦截处理、统一错误。听起来不复杂但实际项目里请求层往往是整个前端的命脉token 怎么带、错误怎么提示、loading 怎么控制、取消请求怎么处理全都依赖一个稳定可靠的请求封装。这篇文章适合谁看如果你是刚接触前端、对网络请求还停留在“照着文档调接口”阶段的新手可以把它当一份从零到一的实践手册如果你已经写过不少请求代码但总觉得项目里拦截器逻辑写得乱、报错很难排查那这份笔记里的封装思路和避坑经验应该能给你一些参考。我会从拦截器机制、实例封装、Vue 项目的 devServer 转发、再到 Next.js 场景下 fetch 与 Axios 的选型对比把高频问题一次讲透。1.1 从 XMLHttpRequest 到 Axios解决了什么痛点在没有 Axios 的年代前端发请求主要靠 XMLHttpRequest。它的写法不复杂但问题在于需要手动处理一堆状态码监听onreadystatechange自己拼接 query 参数还要分别处理超时、取消、响应类型等等。写多了你会发现每次请求其实都是重复劳动而且很容易漏掉某些边界情况。jQuery 的$.ajax算是把这类操作封装了一层但它整个库太重而且现在很多项目并不需要 jQuery 的 DOM 操作能力。后来社区出现了axios、fetch这些方案其中 Axios 能在众多工具里长期占据主流核心就几点浏览器兼容性好、API 简单直观、拦截器机制强大、支持请求取消、还能在 Node 环境直接用。这些能力叠在一起基本覆盖了业务开发里 90% 的请求需求所以很多老项目一用就是好几年。1.2 Axios 比 fetch 到底强在哪这个话题其实挺容易被杠的因为 fetch 是浏览器原生 API没有人怀疑它的“正统地位”。但实际写业务的时候fetch 的短板体感非常明显。第一fetch 对 HTTP 状态的判定不符合直觉。fetch只在网络错误或请求被取消时 reject像 404、500 这种状态它照样 resolve你必须在 then 里手动判断response.ok再抛错。Axios 则是在默认情况下把非 2xx 状态当成 reject 处理配合响应拦截器可以一步到位。第二fetch 的超时机制默认不存在。浏览器原生 fetch 不提供超时配置得自己组合AbortController写一套逻辑。而 Axios 在配置里直接写timeout: 10000就完事取消请求也有内置能力。第三fetch 的拦截能力非常弱。Axios 可以在请求发出前和响应回来前嵌入统一逻辑fetch 想做到类似效果得自己包一层函数而且要在每个请求点手动调用维护成本直线上升。所以后面提到 Next.js 场景时我还会展开一次fetch不是不能用而是你得自己补一堆脚手架。2. 拦截器机制拆解请求拦截与响应拦截拦截器是 Axios 的灵魂也是很多人刚开始封装时最容易搞混的地方。它的运行机制简单说就是请求拦截器在请求发出前执行响应拦截器在请求返回后、但在你调用 then 之前执行。这两个环节是插入统一逻辑的最佳位置。2.1 拦截器的工作流程一次完整的 Axios 请求经过的链路是这样的调用axios.get/axios.post等方法进入请求拦截器如果有此时可以修改 config、追加 headers、加 token真正发出 HTTP 请求拿到响应后进入响应拦截器此时可以统一解包、统一处理错误把最终数据交给业务代码里的 then 或 async/await。这个流程决定了哪些逻辑适合放拦截器。比如 token 注入、签名参数、loading 开始计数放请求拦截器统一剥离response.data、全局错误提示、登录失效跳转放响应拦截器。下面是一个很常见的拦截器写法import axios from axios; // 创建实例 const service axios.create({ baseURL: /api, timeout: 15000 }); // 请求拦截器 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) { // 后端通常会包一层 { code, data, message } const res response.data; if (res.code ! 0) { // 业务错误统一处理 return Promise.reject(new Error(res.message || 请求失败)); } return res.data; }, (error) { // HTTP 错误统一处理 if (error.response error.response.status 401) { // 登录失效 localStorage.removeItem(token); window.location.href /login; } return Promise.reject(error); } ); export default service;这段代码是绝大多数项目的基础模板但实际用的时候有几个细节值得注意。2.2 请求拦截器不只是加 token很多人以为请求拦截器就是“给请求加个头”其实它能做的事情远比这多。我接手的项目里至少见过以下几种用法。第一种是统一加时间戳防止某些服务端缓存 GET 请求。在 config 的参数里追加一个_t代码就一行但能省掉很多“数据没更新”的问题。第二种是接口签名。如果是内部系统后端会要求一定规则的对参数排序后拼接密钥做签名。这个逻辑如果散落在每个页面后期换签名算法的时候几乎要重构整个项目放进请求拦截器只需要改一个地方所有接口自动生效。第三种是控制全局 loading。有些项目要求在每次请求期间页面顶部显示进度条常见的做法是放一个全局计数器在请求拦截器加一在响应拦截器减一计数归零时关闭 loading。这里特别要注意加锁否则并发请求时会出现“第一个请求结束就关掉 loading而第二个请求还在飞”的闪现问题。第四种是为多个 baseURL 做分发。比如同一个页面既要请求业务接口又要请求用户中心接口可以在调用 axios 时通过自定义 config 属性区分比如config.isUserCenter true在请求拦截器里判断之后动态修改 baseURL。2.3 响应拦截器统一解包和错误兜底响应拦截器最重要的任务是让业务代码里少写重复的“判断 code 抛错 提示”逻辑。我见过一个非常乱的项目每个页面里都有这么一段const res await request.get(/xxx); if (res.code 200) { // ... } else if (res.code 401) { // ... } else { message.error(res.message); }同样的逻辑复制了十几次后来后端调整了错误码格式前端改到崩溃。正确做法是响应拦截器里把这些全部收敛掉让业务代码拿到的就直接是data而不是整包 response也不需要自己判断业务成功与否。这里有个取舍要提醒一下如果你的项目里存在一些“业务失败但希望单独处理”的接口比如登录时密码错误这种情况如果直接在响应拦截器里统一弹错误提示那页面里就很难做定制化处理。我的习惯是在拦截器里把一个带code的错误对象 reject 出去业务层可以用catch捕获并判断error.code方便做特殊场景的差异化处理。3. 封装一套真正好用的 Axios 实例拦截器写好了只是完成了第一步。项目里真正要落地还需要一套可维护的封装结构。这里分享一套我目前用得比较顺手的组织方式。3.1 基础配置baseURL、timeout、withCredentials创建实例时基础的配置项有几个值得花心思。baseURL建议统一写成相对路径/api而不是写死http://localhost:8080。这样在不同环境开发、测试、生产下只需要通过构建工具的代理或服务器转发来切换后端地址前端代码不用动。timeout根据业务类型区分。普通列表接口 15 秒足够但如果项目里存在大文件上传或导出报表全局只能取一个兜底值不能设太短。我一般全局设 30 秒上传接口单独传timeout: 0表示不限制。withCredentials这个配置很多人会漏。如果后端依赖 Cookie 做身份认证而你恰好又用了跨域请求不带这个配置浏览器不会携带 Cookie接口就一直报 401。反过来如果你用的是 token 认证完全不需要这个配置甚至开了反而可能引起跨域预检失败要视项目而定。3.2 模块化分层request 层和 api 层我推崇的结构是至少分两层第一层是request.js负责创建 axios 实例、注册拦截器、导出已经配置好的实例。第二层是api目录按业务模块拆文件比如user.js、order.js、goods.js每个文件里导出具体的请求函数。api层的好处在于页面里不需要看到 URL 字符串调用方式变成import { getUserInfo } from /api/user后续接口路径变了、参数变了只需要改 api 层的函数页面代码完全不用动。一个具体的 user.js 大概长这样import request from /utils/request; // 获取用户信息 export const getUserInfo (params) { return request.get(/user/info, { params }); }; // 修改密码 export const updatePassword (data) { return request.put(/user/password, data); };有些人会把第一和第二步合并直接在页面里import axios from /utils/request然后到处写request.get(/user/info)。这样不是不行但接口路径一旦散落到各个页面后期维护会非常痛苦还是建议多做一个 api 层。3.3 取消请求与重复提交抑制Axios 取消请求有旧版CancelToken和新版AbortController两种方式。旧版 API 后续版本里标记为已废弃虽然目前仍能用但新项目建议直接走 AbortController。最常见的需求是防止重复提交。比如用户快速点了两次“提交订单”按钮后端就收到了两条一模一样的订单。实现思路是在请求拦截器里维护一个请求映射表key 是请求方法加 URL 加参数如果映射表里已经存在相同 key 的请求就直接取消或忽略请求结束时把 key 从映射表移除。const pendingMap new Map(); const getRequestKey (config) { const { method, url, params, data } config; return [method, url, JSON.stringify(params || ), JSON.stringify(data || )].join(); }; const addPending (config) { const key getRequestKey(config); const controller new AbortController(); config.signal controller.signal; if (pendingMap.has(key)) { controller.abort(); // 取消后发的新请求 } else { pendingMap.set(key, controller); } }; const removePending (config) { const key getRequestKey(config); if (pendingMap.has(key)) { pendingMap.delete(key); } };在请求拦截器里调addPending在响应拦截器成功和失败分支里都调removePending。这里有个细节removePending要在拿到 config 时同步执行否则下一个相同请求会被误伤。3.4 泛型与 TS 类型约束项目用了 TypeScript 之后给 Axios 封装加类型约束是很有必要的。最简单的一层是给请求函数加返回类型。定义接口类型export interface ApiResultT unknown { code: number; message: string; data: T; } export const getUserInfo (params: { id: number }) { return request.getApiResultUserInfo(/user/info, { params }); };这里有一个很常见的坑axios.getT的 T 表示response.data的类型但如果你在响应拦截器里已经剥了一层response.data.data业务代码拿到的类型其实和 axios 声明的不一致。这时候要么在 api 层再包一个返回类型要么把拦截器剥壳后的结果通过泛型转一下。团队里没有约定清楚的话写起来类型全是anyTS 形同虚设。我的做法是 api 层所有函数都显式声明返回类型页面里拿到的类型和实际数据一致。4. Vue 项目中的 Axios devServer 转发Vue 项目里用 Axios 基本是标配但很多新手会卡在“为什么我请求一直报跨域”这个问题上。这里面的关键就是开发环境要配 devServer 的转发。4.1 为什么开发时要配置 devServer 转发前后端分离的开发模式下前端跑在http://localhost:5173Vite或者http://localhost:8080vue-cli后端接口一般在http://localhost:8081或其他地址。浏览器直接从前端地址请求后端地址会触发浏览器的同源策略出现跨域报错。跨域的解法有很多比如后端配 CORS、前端用 JSONP现在已经很少用了。开发环境下最省事的方案是在 devServer 层面做代理转发前端的请求还是发到当前开发服务器的地址devServer 收到后把请求转发到真正的后端地址。因为服务端之间的请求不涉及浏览器的同源策略所以跨域问题就绕过去了。注意一个核心点这种转发只是开发环境的便利措施生产环境前端静态资源部署的时候还是要靠 Nginx 或网关转发或者后端直接开 CORS否则上线照样跨域。4.2 Vite 与 vue-cli 的转发配置Vue 3 项目基本都是 Vite 构建配置在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } });这个配置的含义是所有以/api开头的请求会被转发到http://localhost:8081。changeOrigin: true的作用是修改请求头里的Host让后端感觉请求是同源发来的否则某些后端框架会校验 Host 导致 403。rewrite是把路径里的/api前缀去掉因为后端接口路径本身可能并不带这个前缀。vue-cli 写的则是vue.config.jsmodule.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };两者逻辑完全一致只是字段名和写法略有差别。4.3 转发配置里的隐藏坑第一个坑是路径重写。如果你的 axios 里baseURL写了/api而后端接口路径本身就是/api/user/list那 rewrite 应该把/api保留而不是替换成空字符串。很多人在这一步纠结很久就是想不明白“为什么接口 404”其实就是前缀被多去了一层。第二个坑是 https 后端地址。有些本地开发环境调用的后端是https://test.api.com它的证书是自签的或者是测试证书浏览器访问都会提示不安全。此时 devServer 转发默认会验证证书导致转发失败。Vite 里需要加secure: falseproxy: { /api: { target: https://test.api.com, changeOrigin: true, secure: false } }第三个坑是 Cookie 和登录态丢失。如果代理配置里没有设置cookieDomainRewrite或 ws 支持某些情况下后端种下来的 Cookie 域和前端不一致请求就带不上。前面提到的withCredentials也需要同时配合前后端都要允许凭证传递缺一个都会出问题。5. Axios 与 fetch 的实战对比Next.js 项目里怎么选现在不少新项目选择了 Next.js 这套 React 全栈框架。这时候就出现了一个热门问题Next.js 里用原生 fetch 封装拦截器还是直接用 Axios 拦截器我特意把两种方案的贴身感受拿出来说说。5.1 Next.js 专用场景的考量Next.js 的请求场景比纯前端项目复杂。服务端组件、客户端组件、路由处理器Route Handlers、静态生成SSG不同场景下对请求工具的要求不一样。原生 fetch 在 Next.js 里被强化了内置了缓存、重新验证等能力所以社区里有不少声音认为“Next.js 应用优先用 fetch”。这种说法有它的道理尤其是服务端组件里直接fetch(url, { next: { revalidate: 60 } })可以很自然地利用框架层面的缓存。但这种便利仅限于服务端到了客户端组件里fetch 的普通短板依然存在该封装还是得封装。5.2 用原生 fetch 封装拦截器难点在哪如果想在 Next.js 客户端组件里用 fetch 实现类似 Axios 拦截器的效果通常的做法是封装一个http函数const request async (url, options {}) { const token localStorage.getItem(token); const res await fetch(/api${url}, { ...options, headers: { Content-Type: application/json, Authorization: token ? Bearer ${token} : , ...options.headers } }); if (!res.ok) { // 这里是手动处理 HTTP 错误 throw new Error(HTTP ${res.status}); } const data await res.json(); if (data.code ! 0) { throw new Error(data.message); } return data.data; };这个封装看起来还可以但用下来有几个很明显的痛点。第一请求取消很啰嗦。fetch 原生的取消只能通过 AbortController每次请求都要手动创建 controller 并传入 signal代码量比 Axios 的CancelToken或signal配置大不少而且容易被遗漏。第二超时配置要自己写。不封装的话fetch 一个接口可能挂很久都不报错。自己封装超时就得组合AbortSignal.timeout或者手动开定时器去 abort写起来比 Axios 一个timeout字段麻烦得多。第三错误类型不统一。fetch 在网络异常时抛出的TypeError: Failed to fetch信息量很少而 Axios 会给你error.response、error.request、error.message这些结构化的对象排查问题方便很多。5.3 我的选择思路在 Next.js 项目里我的个人标准是这样的服务端组件里优先用原生 fetch因为可以直接利用 Next.js 的缓存机制客户端交互场景如果请求复杂、需要统一拦截器、需要考虑取消和超时那就直接上 Axios没必要为了“减少一个依赖”而自己造一套 fetch 封装轮子。其实很多团队纠结这个问题本质上是混淆了“服务端数据获取”和“客户端请求”两种场景。服务端组件渲染时确实应该优先用框架提供的数据获取能力但客户端用户点击按钮之后触发的请求还是用一个成熟的请求库更稳妥。两者完全可以共存不必非黑即白。6. 高频报错与排查实录最后这部分我整理了一些项目中最常遇到的 Axios 报错以及对应的排查思路。这些基本都是从实战里一个个踩出来的适合贴到团队文档里当速查表。6.1 常见报错速查报错信息常见原因排查方向Network Error网络断开、跨域被拦、接口地址不可达看浏览器 Network 面板确认请求是否发出、响应是否被 CORS 拦截timeout of 15000ms exceeded后端处理慢或接口一直 pending检查后端日志确认接口是否卡死顺带确认是否有全局 loading 阻塞401 Unauthorizedtoken 过期、未携带 token在请求拦截器里打印 headers确认 Authorization 是否存在403 Forbidden权限不足、后端校验了 Host 或 Referer检查 devServer 的 changeOrigin 配置后端服务有没有白名单404 Not Found接口路径错误、代理 rewrite 把路径重写多了/少了检查 axios baseURL 和 devServer rewrite 的组合结果Request failed with status code 500后端程序出错让后端看日志前端拿不到更多信息ERR_CONNECTION_REFUSED后端没启动或端口写错浏览器单独访问后端地址确认服务是否存活canceled请求被主动取消或重复请求被抑制检查是否有 AbortController 在控制重复提交的按钮逻辑是否正常6.2 一个实际的排错过程有一次线上反馈说“导出 Excel 点了没反应”打开控制台发现报错timeout。第一反应是接口慢但看了后端日志接口处理只用了几百毫秒。继续排查发现前端请求发出去以后页面上的 loading 遮罩一直没有关闭再往下一层查发现是响应拦截器里对二进制文件流做了 JSON 解析导致await response.json()抛了异常进入 reject 分支后 loading 没有走到关闭逻辑。问题出在 axios 默认会尝试把响应数据当作 JSON 解析但导出文件的 responseType 是blob数据格式不一样。遇到这类接口需要在 api 层单独指定responseType: blob并且响应拦截器里对 blob 类型做特殊判断不能走常规解包逻辑。类似这种“axios 统一封装误伤特殊接口”的问题排查的时候一定要先看拦截器再看 api 层有没有覆盖特殊类型。6.3 两条避坑建议第一条生产环境的接口地址绝对不要硬编码在前端代码里。我见过有人直接在 api 文件里写http://192.168.1.100:8080的换了个网络环境整个项目就崩掉。正确做法是读取构建环境的配置变量比如 Vite 的import.meta.env.VITE_API_BASE_URL在部署的时候按环境注入。第二条发布前一定要检查 baseURL 和代理规则的最终效果。开发环境靠 devServer 转发生产环境靠 Nginx 转发如果两边配置的前缀不一致就会出现“本地好好的一上线就 404”的经典问题。我的习惯是把所有环境转发后的最终 URL 各列一份在发布清单里让前后端对一遍能避开绝大多数部署故障。最后分享一点个人的真实体会。Axios 作为一个库你花十分钟看完官方 README 就能用起来但真正让它在项目里“好用”靠的是拦截器设计、api 层拆分、异常兜底这些工程化动作。这些代码写一次后面无数个业务迭代都能受益。我自己也在多个项目里反复调这套封装从最早的“所有逻辑塞拦截器”慢慢改成“拦截器只做通用逻辑特殊接口单独配置”踩过不少坑也沉淀了不少经验。希望这份笔记能帮你省下一些调研时间少走几步弯路。如果在实际封装中遇到什么特别棘手的问题欢迎随时交流我也很好奇大家在项目里是怎么取舍 Axios 和 fetch 的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价