ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue项目Axios请求封装:全局配置与自定义实例及拦截器实践

Vue项目Axios请求封装:全局配置与自定义实例及拦截器实践 1. 问题从哪里来为什么一个请求配置能让人焦头烂额先聊个实际场景。你在公司接手一个Vue项目页面不多但后端服务拆了三个用户中心一个域名、订单中心一个域名、支付回调一个域名。A接口要带token才能调B接口是白名单不用带C接口超时时间得拉长到30秒其他接口5秒就该报错。如果整个项目都用裸的axios.get(/api/user)这么写后面前端会乱到什么程度基本是每个页面里都塞着一堆重复的请求配置改一个超时时间得全局搜加一个请求头得一个一个文件去改最后代码review的时候没人愿意碰那块地方。axios本身不是难用的库难用的是“让项目里所有人按同一种方式去用它”。很多团队刚起步时图省事直接在main.js里Vue.prototype.$http axios然后到处this.$http.get三门后端服务全打同一个baseURL遇到跨域、鉴权、状态码判断这种问题就只能在每个业务组件里各写各的逻辑。所以这篇东西我打算从两个层面去拆一个是“axios全局配置”就是你在项目入口统一设置的那一套默认参数另一个是“自定义配置axios实例”用axios.create()创建出特性完全不同的多个请求对象。两者不是二选一实际项目里往往是组合着用。我会把每一步为什么这么写、里面有哪些坑、踩过之后怎么补都讲清楚。写这套内容之前先声明我假设你已经会装依赖、会建Vue项目至少跑起来过npm create vue之类的东西。我讲的重点是请求层怎么组织不是Vue基础语法。如果你是想看axios从零怎么用这套内容里的代码你也能直接用但要有点耐心把后面拦截器、实例拆分的逻辑一起看完。2. 先搞懂axios配置体系默认配置、实例配置、请求配置2.1 axios这套配置到底分几层axios的核心设计其实不复杂你可以把请求配置理解成三层叠加第一层是axios.defaults全局默认配置。你在main.js里写axios.defaults.baseURL /api相当于给所有通过这个axios对象发出的请求都设置了默认地址。第二层是实例配置。通过axios.create({ ... })创建的每一个实例都拥有自己独立的一份默认配置比如const service axios.create({ baseURL: /order, timeout: 10000 })这个实例发出去的请求baseURL就是/order不会污染全局的axios。第三层是单次请求配置。在调用具体请求时比如service.get(/list, { params: { page: 1 } })或者service({ method: get, url: /list })你传的这些配置只对这一次请求生效。这三层的关系是单次请求配置 实例配置 全局默认配置。如果你在全局设置了timeout: 5000又在实例里设置了timeout: 10000那通过这个实例发出的请求超时时间就是10000毫秒。如果你在某个请求里又单独写了timeout: 0那就以这个请求的配置为准表示不超时。这个层级关系我在实际项目里用得最多的地方就是处理“大部分接口5秒超时但上传文件接口可能要几分钟”这种需求。直接在实例创建时给一个常规超时在上传请求时单独传一个长超时完事。如果不理解这个覆盖顺序很多人会去全局配置里反复修改timeout最后哪个请求超时都分不清排查起来非常头疼。2.2 常见配置项逐个说baseURL、timeout、headers、withCredentials在这三层配置里有些配置项出现频率特别高我逐个说下我在真实项目里是怎么用的。baseURL这个字段决定了你所有请求的公共前缀。我建议不管项目多小都先定义好baseURL。比如开发环境用/api这种相对路径然后在vue.config.js里配proxy代理到后端地址这样可以避开开发环境的跨域问题。线上环境再通过环境变量把它替换成https://api.xxx.com。这里有个小细节如果baseURL以/开头axios会把它拼在域名后面如果后端接口本身已经带了版本号比如/v1/user/list你最好把/v1也放进baseURL里或者不要用相对路径而在每个请求url中带全否则容易拼出/v1/v1/user/list这种鬼东西。timeout这个参数很多人设了一个值之后就再也不管了。但实际场景里你要结合业务去定普通查询接口可以短一点3000到5000毫秒导入导出这种重操作可能需要30000毫秒以上。我见过不少团队把timeout统一设成5000结果有一个报表接口要跑8秒前端用户在页面干等着然后突然报错。设置timeout的时候肯定要跟后端对一下接口的预估耗时再留一点余量。headers这个配置项通常用来放Content-Type和自定义请求头。比如Content-Type: application/json或者X-Requested-With: XMLHttpRequest。在设置自定义headers时最典型的场景就是登录鉴权不过我不建议在全局配置里写死token因为token是会变的你最好在请求拦截器里动态设置这一点后面详细讲。withCredentials这个字段是一个容易踩坑的配置。它决定跨域请求是否需要携带cookie凭证。默认值是false如果你要用cookie做登录态而且后端跨域配置了Access-Control-Allow-Origin: 具体域名而不是*那你必须把withCredentials设为true否则cookie带不过去。但注意一旦打开这个开关后端的Access-Control-Allow-Origin就不能设置为*了必须是明确的域名否则浏览器会拦下整个请求。2.3 全局配置的代码长什么样说完了概念直接上最基本的全局配置写法。在Vue 3项目里通常是在main.js入口或单独的工具文件里做// request/global.js import axios from axios import { message } from ant-design-vue axios.defaults.baseURL import.meta.env.VITE_BASE_URL || /api axios.defaults.timeout 10000 axios.defaults.headers.post[Content-Type] application/json axios.defaults.withCredentials true这段配置写完之后项目里任何一个地方直接import axios from axios发出的请求都会自动带上这些默认行为。如果你项目只有一个后端服务而且没有复杂的权限体系这一层配置其实够用了。不过你很快会遇到问题。比如某个页面需要下载文件响应类型是blob但全局配置里没设置responseType你就要在具体请求里写axios.get(/file/download, { responseType: blob })。这还能接受但后面一旦出现多套鉴权、多套超时、多套错误处理逻辑你就得考虑拆实例了。3. 自定义axios实例多场景复用的正解3.1 为什么光有全局配置不够项目稍微做大了之后全局配置暴露出来的问题就很明显。比如用户中心的后端接口要求token放在Authorization头里而订单中心的后端要求token放在X-Token头里。如果你只有一个全局axios那只能在每个请求里单独传headers拦截器里判断url的前缀来给不同的header赋值代码就慢慢变成了屎山。更麻烦的是错误处理。接口返回的业务状态码可能是{ code: 200, data: ... }表示成功{ code: 401, message: 登录过期 }表示鉴权失败{ code: 500, message: 服务器错误 }表示服务端异常。不同模块甚至可能用不同的状态码规范比如用户中心用200表示成功订单中心用0表示成功。这种时候全局只配一套响应拦截器逻辑根本处理不了不同模块的差异化。自定义axios实例解决的正是这个问题。每个实例可以携带完全独立的baseURL、timeout、headers以及完全独立的拦截器。模块A的实例只管模块A的接口规约模块B的实例只管模块B的规则大家互不干扰代码结构也清楚。3.2 用axios.create创建多个独立实例创建实例的方式非常简单代码上就是调用axios.create(config)import axios from axios const userService axios.create({ baseURL: /user-api, timeout: 5000, headers: { X-Requested-With: XMLHttpRequest } }) const orderService axios.create({ baseURL: /order-api, timeout: 15000, headers: { X-Token: } })这样你就有了两个不同的请求对象。userService.post(/login)实际请求的是/user-api/loginorderService.get(/list)实际请求的是/order-api/list。它们之间互不影响超时时间不同请求头预设也不同。这里要提醒一个很多初学者会踩的坑创建实例之后不能把拦截器直接挂在全局axios上必须分别给每个实例挂拦截器。下面的写法才是正确的userService.interceptors.request.use(config { config.headers[Authorization] Bearer ${getToken()} return config }) orderService.interceptors.request.use(config { config.headers[X-Token] getToken() return config })如果图省事在全局axios上挂了一个拦截器然后让所有实例都走这个拦截器那实例自己配置的headers反而可能被覆盖或者被追加出两份token后端一看请求头里有Authorization和X-Token同时存在直接懵掉。3.3 实例命名的规范建议这个可能有人觉得无所谓但我实际维护多个项目下来觉得命名规范非常重要。我习惯在文件里统一命名成xxxService或者xxxRequest比如userService、orderService、uploadService。不要起什么http1、http2这种名字也不要整个项目只用一个request变量。因为当你在一个组件里同时引入两个实例时代码可读性的差距一下就出来了。script setup import { userService } from /api/user import { orderService } from /api/order userService.get(/info) orderService.get(/detail) /script这么写读代码的人一眼就知道当前请求打给哪个服务出了问题也容易定位。反而是那种全局只导出一个request然后到处使用的写法等后端服务拆分、鉴权方式变化之后改起来会非常痛苦。4. 拦截器全局配置和自定义实例真正发力的地方4.1 请求拦截器统一加token、加loading、防重复提交拦截器是axios的精髓。拦截器分两种请求拦截器和响应拦截器。请求拦截器在请求发出前执行通常会做四件事注入token、格式化参数、控制loading、处理重复请求。先看一个最基础版本service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) } )这段逻辑在任何一篇文章里都能看到但实际项目里你要注意几个细节。第一token不该直接裸存localStorage更安全的做法是放在内存变量里或者至少做一层简单的加密这个属于Web安全范畴暂时不展开。第二如果项目里同时有多个实例每个实例是否需要token、token放在哪个头里要规划清楚。再说loading控制。有些团队喜欢在请求拦截器里开一个全局loading在响应拦截器里关掉。简单场景这样没问题但如果同一个页面同时发多个请求第一个请求进来开了loading第一个响应回来就关掉loading其他请求还在转圈页面就会出现明显的闪烁。我的方案是在项目里维护一个请求计数请求发起时加一响应时减一等于0才关闭loading。这个计数器放在store或者一个单独的模块里都行不要在每个页面组件里各管各的。防重复提交是另一个容易被忽略的需求。比如用户连续点击提交按钮同一个请求可能被发出两三次。简单的方式是在请求拦截器里做一个pendingMap记录当前正在进行的请求标识比如method url params拼接成的字符串如果在pendingMap里已经存在就取消新请求或者直接返回一个提示。axios在v0.22.0之后推荐用AbortController来取消请求老项目里用的是CancelToken。我建议新项目直接用AbortController。const pendingMap new Map() function getPendingKey(config) { return [config.method, config.url, JSON.stringify(config.params)].join() } service.interceptors.request.use(config { const key getPendingKey(config) if (pendingMap.has(key)) { const controller pendingMap.get(key) controller.abort() pendingMap.delete(key) } const controller new AbortController() config.signal controller.signal pendingMap.set(key, controller) return config })这段代码的意思就是新请求如果发现同一个key之前已经在飞了就把旧的请求取消掉再重新发起新的。这样用户连续点击提交时永远只有最后一次请求在生效不会重复提交。4.2 响应拦截器状态码分流、错误处理、登录过期响应拦截器的核心逻辑比请求拦截器更复杂因为你要处理的状态太多了。HTTP层面有200、401、403、500、502业务层面有code字段定义的成功、失败、未登录、无权限。不同项目甚至同一项目不同模块规范可能都不一样。我常用的统一处理逻辑是这样的service.interceptors.response.use( response { const res response.data if (response.config.responseType blob) { // 文件下载场景直接返回数据不校验业务码 return res } if (res.code 0 || res.code 200) { return res.data } // 未登录或登录过期 if (res.code 401) { // 跳转登录页或刷新token message.warning(登录状态已过期请重新登录) redirectToLogin() return Promise.reject(new Error(登录过期)) } message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) }, error { if (error.response) { const status error.response.status switch (status) { case 400: message.error(请求参数错误) break case 403: message.error(没有权限访问) break case 404: message.error(请求的资源不存在) break case 500: case 502: case 503: message.error(服务器繁忙请稍后重试) break default: message.error(请求异常${status}) } } else if (error.code ECONNABORTED) { message.error(请求超时请检查网络) } else { message.error(网络异常请检查网络连接) } return Promise.reject(error) } )这里有几个关键点我要单独讲。第一response.data和response的区别。很多人刚接触时以为响应拦截器的response直接就是后端返回的数据其实它是一个完整的axios响应对象包含status、statusText、headers、config等字段真正的数据在response.data里。所以拦截器里第一行先取出res response.data。第二关于code字段的判断。有的后端用code 0表示成功有的用code 200表示成功还有的直接用HTTP状态码。你要先跟后端对齐规范然后在实例的响应拦截器里分别适配。这就是自定义实例的好处之一不同模块可以用不同的取法。第三401处理要格外小心。如果项目里已经用了token刷新机制401不一定要直接跳登录页也可能先尝试刷新token刷新成功就重新发起原来的请求。这个逻辑比较复杂通常要配合单独封装的refreshToken模块来做。如果直接跳登录页后端频繁重启导致token失效前端所有页面都会被弹到登录页去体验非常差。4.3 一个可以直接抄作业的完整封装模板理论讲太多容易绕晕直接给一个我项目里一直在用的标准封装模板。以userService为例完整文件大概长这样// api/request/user.js import axios from axios import { message } from ant-design-vue import { useUserStore } from /store/user import router from /router const userService axios.create({ baseURL: /user-api, timeout: 5000, headers: { Content-Type: application/json } }) userService.interceptors.request.use( config { const userStore useUserStore() if (userStore.token) { config.headers[Authorization] Bearer ${userStore.token} } return config }, error Promise.reject(error) ) userService.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { message.warning(登录过期请重新登录) userStore.clear() router.push(/login) return Promise.reject(new Error(登录过期)) } message.error(res.message || 请求失败) return Promise.reject(new Error(res.message || 请求失败)) }, error { message.error(error.response?.data?.message || error.message || 请求失败) return Promise.reject(error) } ) export default userService然后在具体的接口模块里这样包装// api/user.js import userService from ./request/user export function login(data) { return userService.post(/login, data) } export function getUserInfo() { return userService.get(/info) }页面组件里只需要引用login、getUserInfo这些方法完全不需要关心底层怎么发请求、怎么拦截、怎么处理错误。这样封层的意义在于如果有一天后端把登录接口从/login改到/auth/login你只需要改一行url如果整个user服务域名的baseURL变了你只需要改实例配置。页面代码一行都不用动。5. 全局配置与自定义实例怎么搭配使用5.1 两者不是互斥关系是不同粒度的配置入口有些人看完前面会有一个误解觉得既然有了自定义实例全局配置是不是就不需要了其实不是。我的习惯是全局配置管“最基本的默认值”自定义实例管“业务模块的差异化”。举例来说全局配置里可以设一个最通用、最保守的baseURL: /api、timeout: 15000、Content-Type: application/json。这样一来就算某天有人临时在项目里import axios from axios直接发起请求也不会因为缺少配置而直接报错走偏。真正核心的请求统一走自定义实例实例里覆盖掉不适合自己的配置。比如订单服务超时需求是30秒那就在orderService的create配置里写timeout: 30000这相当于在默认配置之上做了一次定制化覆盖。这个思路跟CSS的样式覆盖有点像先定义一套全局的基础样式兜底再针对不同组件写自己的样式微调。如果全局统一则不需要定制如果某个模块有特例就在实例层面覆盖完全不用动全局的东西。5.2 配置优先级和覆盖关系再确认一遍这三层逻辑全局默认配置axios.defaults.xxx对所有axios实例和直接使用axios发起的请求生效实例创建时如果未指定对应配置。实例配置axios.create({ ... })在创建时手动指定的配置会覆盖全局默认配置。单次请求配置axios.get(url, { ... })或者service({ ... })只会对当前这次请求生效覆盖实例配置和全局默认配置。这层覆盖关系我在实际项目中遇到的最典型场景就是“下载文件”。项目里全局配置和实例配置默认都是responseType: json但下载接口需要responseType: blob。这时你不用新建一个实例只需在请求方法里单独传export function downloadFile(id) { return userService.get(/file/download, { responseType: blob, timeout: 60000 }) }这就是单次请求配置覆盖实例配置的用法。我在项目里很多类似的临时性覆盖都是用这种方式实现的避免了为了一个接口单独建一个实例。5.3 目录结构和代码组织的建议说完了配置优先级再聊一下代码怎么组织更合理。我目前最常用的结构是这样的src/ api/ request/ index.js # 全局axios配置可能还会初始化默认实例 user.js # userService实例 order.js # orderService实例 upload.js # uploadService实例 user.js # 用户模块接口api统一导出 order.js # 订单模块接口api统一导出 index.js # 汇总导出所有apirequest目录管的是“请求工具”每个文件导出一个已经配好baseURL、timeout、拦截器的axios实例api目录管的是“业务接口”每个文件里写的是具体请求方法。这种结构有一个明显的优势当你要找某个接口的请求地址、请求方式时直接去api目录对应模块文件里看当你要调某类请求的超时时间、拦截逻辑时直接去request目录对应实例文件里改。职责清晰不会互相折腾。6. 常见问题与排查技巧实录6.1 为什么我设置的baseURL没生效这大概是新手最容易踩的坑。常见原因有几种第一你在实例里明确设置了baseURL但在某些请求方法里url写的是完整路径比如service.get(https://other-api.xxx.com/list)。axios会优先使用url里的绝对地址baseURL直接失效。这在对接第三方接口时经常发生不算bug但要心中有数。第二你设置了baseURL但实际看network面板里请求的地址不对。这种一般是代理配置的问题。比如baseURL设置为/apidevServer的proxy没有把/api转发到后端导致浏览器直接请求当前前端开发服务器的/api路径返回404或者index.html。如果你遇到请求地址看起来没问题但就是报错先去看代理配置。第三baseURL拼接的问题。如果baseURL是/api/带斜杠url也是/loginaxios处理后会变成/api/login。但如果baseURL是/api没斜杠url是login没斜杠有的版本拼出来是/apilogin这是我非常不推荐把斜杠随便省略的原因。稳妥的写法是baseURL以/开头但结尾不加斜杠url以/开头。6.2 明明设置了超时时间为什么请求还不超时这个坑来自一个比较隐蔽的机制。axios的超时计时是从请求发出到收到响应头为止不包含下载响应体的时间。如果你下载一个大文件服务器很快返回了响应头但数据传输很慢axios是算不超时的。很多人以为超时应该包含整个数据传输时间实测中没包含所以下载大文件时即使设了超时时间也可能卡很久。另一个原因是instance和axios混用。如果你在实例里设置了超时时间但在请求中用axios.get而不是userService.get那这个请求走的是全局axios的配置实例的超时对它无效。排查时先确认请求是不是真的通过你想要的实例发出的在拦截器里console.log(config.timeout)是最直接的验证方式。6.3 登录过期为什么会造成死循环这个我实在见得太多单独拿出来讲。假设你在响应拦截器里发现code是401然后跳转登录页。但如果你在请求拦截器里判断token不存在时自动调用刷新token的接口而刷新token的接口也走的是同一个实例刷新逻辑又要求必须带tokentoken又已经过期了那就会造成“请求401 - 刷新token - 刷新token也401 - 再刷新”的死循环。解决办法是给刷新token的接口单独建一个不参与401处理的实例或者在请求拦截器里判断当前url是不是刷新token的地址是的话就不走通用的token处理逻辑。这个细节不处理好项目上线后很容易出现“页面卡死接口刷屏”的现象。6.4 排查配置问题最实用的几个技巧第一在拦截器里打印config。请求发出前console.log(config)可以让你看到最终的baseURL、url、headers、timeout到底是什么看起来最笨但最有效配置覆盖问题一查一个准。第二看network面板的地址和headers。具体请求的完整URL、请求头信息都能看到跟你在代码里预期的值逐一比对。很多时候问题是出在代理、环境变量这种axios边界之外的地方。第三写一个最简单的测试用例。单独写一个html或一个Vue组件只调一个接口输出响应和错误信息排除业务干扰。尤其是多个实例共存时用最小用例验证每个实例的行为是否符合预期比在复杂项目里反复调试要快得多。第四善用axios.isCancel。如果你使用了取消请求机制响应拦截器里要对取消请求做单独判断不要当成错误弹提示。在拦截器的error分支里加if (axios.isCancel(error)) { console.log(请求已取消:, error.message) return Promise.reject(error) }只靠这四件事我几乎能排查掉百分之九十的axios配置问题。你在实战里如果遇到什么配置失效的奇葩案例按照这个思路走一遍基本都能找到根因。结尾一些实在话我在实际项目里做axios封装也好几年了最大的感受是封装不是越高级越好是要贴合自己的业务场景。如果你的项目就一个后端服务、一套鉴权逻辑那一个配好拦截器和默认参数的实例完全够用没必要为了“企业级”搞五六个实例互相嵌套。如果后端服务多、鉴权方式不同、超时要求差异大那就老老实实拆实例每个实例各管一摊别嫌代码多。最后再分享一个小技巧封装完axios之后不要急着把那些通用的错误提示全部用message.error弹出。实际业务里有些错误是用户主动取消的有些是需要静默处理的比如自动刷新token接口失败。你可以在拦截器里给每个请求配置一个自定义字段比如config.silent通过判断这个字段决定要不要弹错误提示。接口调用方可以根据场景自行选择灵活很多。这套配置方案我目前一直在用也一直在根据项目需求修修补补。前端请求层这块没有银弹但把axios的全局配置和自定义实例吃透至少能让你的项目在请求管理上少踩八成的坑。
返回列表