Vue3+TS项目Axios封装实战:拦截器与useRequest详解
先问个实在的问题你写前端页面的时候接口数据是怎么拿的如果还是 fetch 一把梭、每个页面把请求逻辑复制粘贴一遍、遇到错误在 catch 里打个 console.log 就完事那我强烈建议你花十分钟看看这篇。Axios 作为目前前端圈使用率最高的 HTTP 请求库几乎每个中大型项目的 package.json 里都有它。配合 Vue3 的组合式 API 和 TypeScript 的类型系统它能让你的请求代码从能用变成好用。这篇文章不整虚的我会从 Axios 的安装、基础用法一路讲到拦截器封装、TS 类型约束最后给一套可以直接抄进 Vue3TS 项目的实战方案。新手照着敲一遍就能上手老手也能在拦截器、取消请求这些细节上查漏补缺。1. Axios 是什么为什么前端离不开它1.1 聊聊前端请求的进化史早年间我们写接口请求用的是 XMLHttpRequest。那玩意儿写起来又长又丑要手动处理 readyState、status、onreadystatechange调一次接口能写出二十行代码还感受不到任何结构。后来 jQuery 的 $.ajax 一度成为主流把请求逻辑收敛了不少。再后来 Promise 普及fetch 原生出现大家觉得终于解放了。但真到了复杂业务场景里fetch 的原生能力还是不够请求被取消fetch 需要配合 AbortController写法麻烦。请求超时fetch 自身没有超时机制要自己包一层。统一加 token、统一处理错误码fetch 需要在每个调用处重复做。低端浏览器兼容fetch 在旧环境还得靠 polyfill。这就是 Axios 的价值。它把 HTTP 请求的基础设施打包好了你在任何项目里都用同一套心智模型去对接后端接口。1.2 Axios 核心特性Axios 本质上是基于 Promise 的 HTTP 客户端同时适配了浏览器和 Node.js 环境。它最核心的几个能力支持 Promise APIthen/catch/async-await 随便用。支持请求拦截器和响应拦截器在发出请求前和收到响应后做统一处理。支持取消请求这在组件卸载、重复提交场景里非常关键。自动转换 JSON 数据不需要手动 JSON.parse。支持客户端防 CSRF 等安全相关能力实际业务里用得少但要知道。内部对浏览器环境使用了 XMLHttpRequest所以能拿到上传下载进度事件。用一句话总结Axios 把发起请求 → 处理响应 → 统一错误这件事做成了流水线你要做的只是往流水线上挂自己的工具。1.3 对比Axios 还是 fetch我见过不少团队从性能角度喊不要用 Axios用 fetch但实际落地时又会为了拦截器、超时、取消请求写一堆封装。最后封装出来的东西复杂度并不比 Axios 低反而更容易出 bug。能力Axios原生 fetch请求拦截器 / 响应拦截器内置需要手动封装超时控制timeout 配置需要 AbortController setTimeout取消请求cancelToken / AbortSignalAbortController自动 JSON 序列化内置手动 JSON.stringify 手动解析错误处理网络层面统一进 catch只有网络错误 rejectHTTP 错误码不 reject上传下载进度onUploadProgress / onDownloadProgress需要 ReadableStream 处理兼容性基本所有现代浏览器IE 完全不支持我最想吐槽 fetch 的一点就是HTTP 404、500 这种响应fetch 默认不会进入 reject你必须在 then 里面再判断 response.ok。项目里只要有一个人忘了判断就会出现明明报错了页面还显示成功的诡异 bug。Axios 的做法是 HTTP 状态码不在 2xx 范围内就直接 reject配合响应拦截器统一兜底少操心很多事。2. 十分钟上手Axios 核心用法速览2.1 安装与第一个请求安装没什么好说的看你自己项目用的什么包管理器# 我习惯用 npm npm install axios # 或者用 pnpm我目前主力 pnpm add axios # 用 yarn 也行 yarn add axios装完之后一个最简单的 GET 请求长这样import axios from axios axios.get(/api/users) .then(res { console.log(res.data) }) .catch(err { console.error(err) })更推荐直接上 async/await代码更平配合调试也方便async function fetchUsers() { try { const res await axios.get(/api/users) console.log(res.data) } catch (err) { console.error(err) } }这里先理解一个关键点axios 请求默认返回的响应对象不是后端给你的那包业务数据而是一个 AxiosResponse 包装结构。里面包含 data后端实际返回、statusHTTP 状态码、statusText、headers、config 这些字段。新手最容易卡住的地方就在这里——我明明拿到了数据为什么 res 里还有一堆乱七八糟的东西因为 axios 把整个响应上下文都给你了真正要用的业务数据在 res.data 里。2.2 常用请求方法与传参姿势POST 请求和 GET 在写法上差别不大关键在参数位置// GET参数放在 params axios.get(/api/users, { params: { page: 1, size: 10 } }) // POST参数放在请求体 axios.post(/api/login, { username: admin, password: 123456 }) // 也可以把请求体直接作为 axios.post 的第二个参数 axios.post(/api/order, orderData)如果你不想用这类快捷方法也可以用统一的 axios.request 写法把所有东西都塞进配置项axios.request({ url: /api/users, method: get, params: { page: 1, size: 10 } })其实两种方式本质一样快捷方法只是帮你把 method 预设好了。项目里我一般更推荐 request 的统一写法因为封装的时候你可以把所有配置都归拢到一个对象里。再提一个并发请求。所谓并发不是你用 Promise.all 自己拼而是用 axios 提供的 all 方法const [usersRes, ordersRes] await axios.all([ axios.get(/api/users), axios.get(/api/orders) ])这个 old 写法在新版本里被逐步弱化了很多人觉得不如直接 Promise.all 来得熟悉。确实用 Promise.all 也完全没问题。真到了封装层我用得更多的是 Promise.all 而不是 axios.all因为后者少了泛型推导的乐趣。2.3 关键配置项这些参数你必须知道axios 的配置项很多但日常开发真正高频用到的就那么几个我列一张速查表配置项作用示例url请求地址/api/loginmethod请求方法get | postbaseURL基础路径自动拼接在 url 前面import.meta.env.VITE_API_BASEheaders请求头{ Authorization: Bearer xxx }paramsURL 查询参数{ page: 1 }data请求体数据{ name: 张三 }timeout超时时间(ms)超时会中止请求10000responseType响应类型json | blobwithCredentials跨域时是否携带 Cookietruetimeout 这个配置我要单独说一下。默认情况下 axios 是没有超时时间的实际上取决于环境如果后端接口挂了或者网络异常你的页面可能一直转圈。给所有请求设一个合理的 timeout 是基本功。我通常设置在 10 到 30 秒之间具体看你接口的业务预期。3. Vue3 TypeScript 实战封装一套能直接上线的请求层3.1 为什么写业务前先搞封装很多新手学完 axios 基础之后直接在组件里到处写 axios.get、axios.post。这种写法在 demo 里没什么但项目一复杂就难受了每个接口都要重复写 baseURL、headers、timeout。后端统一返回的 code 码没人校验接口报错只能靠浏览器 console。Token 过期要一处一处改。想给请求加个全局 loading无从下手。存在非常大的潜在问题如果后端统一了响应格式比如 { code: 0, message: ok, data: ... }你不做统一拦截每个页面都得自己写 if (code 0)。所以有经验的团队都会先封装一个 request 实例把通用逻辑收敛到位。下面这套是我在某管理后台项目里实际用过的方案基于 Vue3 Vite TypeScript稍微改改就能放进你自己项目。3.2 先定义后端响应类型动手写代码之前先把类型定义好。TypeScript 的好处就在这接口返回的是数据但我们可以先定义好数据的形状。假设后端统一返回下面这种 JSON 结构{ code: 0, message: success, data: {} }那我一般会在 src/types/api.d.ts 里定义// 后端统一响应结构 export interface ApiResponseT any { code: number message: string data: T } // 分页数据结构后端分页接口常用 export interface PageResultT { list: T[] total: number page: number pageSize: number } // 用户信息 export interface UserInfo { id: number name: string avatar: string email: string } // 登录接口的入参 export interface LoginParams { username: string password: string captcha?: string }这里要适应你自己的后端约定。有的后端喜欢用 status 而不是 code有的喜欢把 data 拆成 data.list 和 data.total你的类型定义必须向后端字段对齐不然 TS 一检查就红一片反而更麻烦。3.3 创建 Axios 实例与拦截器封装新建 src/utils/request.ts这是整个请求层的核心。import axios from axios import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from axios import type { ApiResponse } from /types/api // 创建实例 const service: AxiosInstance axios.create({ // 这个 baseURL 建议用环境变量别写死 baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000, headers: { Content-Type: application/json;charsetutf-8 } }) // 请求拦截器在请求发出前统一处理 service.interceptors.request.use( (config) { // 从本地存储拿 token放进请求头 const token localStorage.getItem(token) if (config.headers token) { config.headers.Authorization Bearer ${token} } return config }, (error) { // 请求错误统一进这里一般是配置写错 return Promise.reject(error) } ) // 响应拦截器在拿到响应后统一处理 service.interceptors.response.use( (response: AxiosResponse) { // 这里返回整个 response // 但我们真正关心的是后端业务数据 const res response.data as ApiResponse // 如果业务 code 不是 0说明后端返回了业务异常 if (res.code ! 0) { // 消息提示组件比如 Element Plus 的 ElMessage // 我用的是挂到全局的 message这里写 ElMessage.error(res.message) return Promise.reject(new Error(res.message || 请求失败)) } // code 为 0 时直接返回业务数据 return data }, (error) { // HTTP 层错误403、404、500、超时等 let message 网络异常请稍后重试 if (error.response) { // 能拿到 status 就按状态码区分 const { status } error.response if (status 401) { message 登录已过期请重新登录 // 在这里可以做跳转登录页、清空 token 等操作 } else if (status 403) { message 没有权限访问该资源 } else if (status 404) { message 请求的接口不存在 } else if (status 500) { message 服务器开小差了请稍后再试 } } else if (error.code ECONNABORTED) { message 请求超时请检查网络 } console.error(message) // 这里接全局错误提示比如 ElMessage.error(message) return Promise.reject(error) } ) export default service有几个细节值得单独拿出来讲。第一响应拦截器里我为什么 return 的是 data 而不是整个 response因为业务层根本不关心 HTTP 包装结构只关心后端 code。把 data 直接剥出来业务代码写起来会更清爽const user await loginApi(params)拿到的就是用户对象。如果返回整个 AxiosResponse你后面每个调用都要写.data属于没事给自己找事。第二请求拦截器里config.headers.Authorization ...这行在 TS 里偶尔报错因为 config.headers 类型是 AxiosRequestHeaders可能为 undefined。实际项目中稳妥写法是先判断if (config.headers) { config.headers.Authorization Bearer ${token} }或者用config.headers.set?.(Authorization, ...)不过兼容旧版 axios 时直接赋值更保险。第三网络错误和业务错误的区别要拎清楚。响应拦截器的第二个回调处理的是 HTTP 层面的失败对于 HTTP 200 但业务 code 非 0 的情况要在第一个回调里手动 reject。很多人以为只要返回 Promise.reject 就能被调用处 catch 到确实可以。但你要看清楚你的调用处到底会不会正确处理这个 rejection如果不会错误就会被吞掉只看到一个小小的 console 输出。3.4 封装 API 模块把每个接口变成函数请求层搭好之后接下来是把每个接口封装成函数。我习惯在 src/api 目录下按业务模块拆分文件比如 user.ts、order.ts。以用户模块为例// src/api/user.ts import request from /utils/request import type { ApiResponse, LoginParams, PageResult, UserInfo } from /types/api // 登录 export function loginApi(data: LoginParams) { return request.postApiResponseUserInfo, ApiResponseUserInfo, LoginParams(/auth/login, data) } // 获取当前用户信息 export function getUserInfoApi() { return request.getApiResponseUserInfo, ApiResponseUserInfo(/user/info) } // 分页获取用户列表 export function getUserListApi(params: { page: number; pageSize: number }) { return request.getApiResponsePageResultUserInfo, ApiResponsePageResultUserInfo(/user/list, { params }) }这个泛型写法看起来有点吓人我来解释一下。axios 的 request.getT, R, D 泛型参数分别是第一个 T响应里的 data 类型也就是 AxiosResponse T 第二个 R你希望最终返回的类型第三个 D请求体数据的类型因为我们拦截器里已经把 data 剥出来返回了所以这里把 R 也设成 ApiResponse... 才能让 TypeScript 正确推断出拿到的最终值类型。如果嫌麻烦也可以不写第二和第三个泛型只写第一个 T然后在用的时候再断言。但既然是实战还是建议把类型标清楚后面引用起来省心很多。3.5 在 Vue3 组件里调用接口封装完 API 函数组件里用起来就非常清爽了。我写一个典型的用户列表页面用 Vue3 的 setup 语法template div classuser-list el-table :datauserList v-loadingloading / el-pagination :totaltotal v-model:current-pagepage v-model:page-sizepageSize / /div /template script setup langts import { ref, watch, onMounted } from vue import { ElMessage } from element-plus import { getUserListApi } from /api/user import type { UserInfo } from /types/api const userList refUserInfo[]([]) const loading ref(false) const total ref(0) const page ref(1) const pageSize ref(10) async function fetchUserList() { loading.value true try { const res await getUserListApi({ page: page.value, pageSize: pageSize.value }) userList.value res.data.list total.value res.data.total } catch (err) { // 拦截器已经统一处理过错误这里可以只做兜底 console.error(加载用户列表失败, err) } finally { loading.value false } } // 监听分页变化 watch([page, pageSize], fetchUserList) onMounted(fetchUserList) /script看到没有组件里不用再关心 token 有没有带上、code 是不是 0这些事全部在请求层处理完了。你只需要关心业务数据和 loading 状态。这就是封装的意义。不过这里还有个可以优化的点每次写列表页都要手动定义 loading、error、data然后写 try/catch/finally代码重复度还是不低。所以接着往下看我教你怎么用组合式函数把这一切再收敛一层。3.6 进阶封装写一个 useRequest 组合式函数Vue3 的组合式 API 天然适合做状态管理 异步请求的封装。我写了一个简化版 useRequest可以直接抽到 src/hooks/useRequest.ts 里// src/hooks/useRequest.ts import { ref, type Ref } from vue interface UseRequestOptionsT { immediate?: boolean // 是否立即执行 defaultData?: T // 默认数据 onSuccess?: (data: T) void onError?: (err: any) void } export function useRequestT( requestFn: (...args: any[]) PromiseT, options: UseRequestOptionsT {} ) { const { immediate true, defaultData, onSuccess, onError } options const data refT | undefined(defaultData) as RefT | undefined const loading ref(false) const error refany(null) async function run(...args: any[]) { loading.value true error.value null try { const res await requestFn(...args) data.value res onSuccess?.(res) return res } catch (err) { error.value err onError?.(err) throw err } finally { loading.value false } } if (immediate) { run() } return { data, loading, error, run } }组件里就可以这么用const { data: userList, loading } useRequest( () getUserListApi({ page: 1, pageSize: 10 }), { immediate: true } )这里有个类型工作需要额外注意requestFn 返回的是 Promise 但实际我们拦截器返回的结构是 ApiResponse 所以 useRequest 拿到的 T 实际上就是 ApiResponse 整体还是业务数据完全取决于你接口封装时的返回类型。如果你接口函数返回的是ApiResponsePageResultUserInfo那么 data 就是 ApiResponse 包裹的对象你需要继续 data.value.data.list如果你在 API 函数里再做一层解包data 就直接是 PageResult。两种都能用关键是整个项目要统一。我个人的习惯是 API 层只返回 ApiResponse 结构业务代码里解包因为这样最直观想看 code/message 随时能看。4. 实战现场一个完整的登录请求闭环光说不练假把式我把登录这个场景从头到尾捋一遍它会涉及请求、响应拦截、token 携带、路由跳转这几个环节。4.1 登录页调接口import { ref } from vue import { useRouter } from vue-router import { loginApi } from /api/user const router useRouter() const loginForm ref({ username: , password: }) async function handleLogin() { if (!loginForm.value.username || !loginForm.value.password) { return } try { const res await loginApi({ username: loginForm.value.username, password: loginForm.value.password }) // 登录成功后后端通常会返回 token localStorage.setItem(token, res.data.token) // 跳转到首页 router.push(/dashboard) } catch (err) { // 统一错误提示已经在拦截器里处理过 // 这里可以提示登录失败请检查用户名或密码 } }这里要注意拦截器里我们已经处理了业务 code 非 0 的 rejection所以登录失败会走进 catch。有些后端会把账号密码错误也作为 code 非 0 返回这时候 catch 里要拿到后端 message 提示用户。可你不能在 catch 里直接看 error.message因为拦截器 reject 的 error 可能是普通 Error也可能带 response.data.message。稳妥的方式是在拦截器 reject 之前把后端 message 挂到 error.message 或者自定义字段上方便调用处使用。这是很多新手踩坑的地方。我常用的做法是在拦截器里做一层错误归一if (res.code ! 0) { const error new Error(res.message || 请求失败) ;(error as any).code res.code return Promise.reject(error) }这样在 catch 里拿error.message就是能直接给用户看的中文描述避免了error.response.data.message这种深层次访问。代码可读性、健壮性都能上去一截。4.2 Token 过期时的 401 处理响应拦截器里遇到 401 时我们通常会做三件事清掉本地存储的 token。跳转到登录页带上当前页面路径便于登录后回跳。弹一条登录已过期的提示。if (status 401) { localStorage.removeItem(token) // 用 router 的方式跳转或者在 main.ts 里统一处理 // 比如 window.location.href /login?redirect encodeURIComponent(router.currentRoute.value.fullPath) ElMessage.error(登录已过期请重新登录) }如果你接的是单点登录这里还要处理一件事不能让所有页面并发请求同时触发跳转否则会出现多次跳转闪烁。简单做法是加一个标志位比如let isRedirecting false只有第一次 401 触发跳转。再高级一点的就是token 静默刷新用 refreshToken 调刷新接口拿新的 accessToken然后立刻重放失败的请求。这个方案好是好但复杂度也跟着上来了要处理并发队列。我建议新手先把基础版跑通再考虑刷新 token。4.3 取消请求组件卸载不再报错在 Vue3 里如果一个组件发起了请求但请求还没返回时组件已经被销毁那么响应回来后更新 ref 会触发 Vue 的警告严重时还会造成内存泄漏。解决办法是在 onBeforeUnmount 里取消请求。axios 里取消请求有两种方式// 方式一AbortController推荐新版 axios 很清晰 const controller new AbortController() axios.get(/api/users, { signal: controller.signal }) // 组件卸载时 onBeforeUnmount(() { controller.abort() }) // 方式二CancelToken老版本常见现在不推荐新的使用 const source axios.CancelToken.source() axios.get(/api/users, { cancelToken: source.token }) source.cancel(组件已卸载取消请求)如果你用了 useRequest 这种 hooks可以把取消逻辑也收进去比如在组件卸载时自动 abort。大家自己加的时候要注意取消导致的 rejection 通常带 axios 的错误标识比如error.code ERR_CANCELED在全局错误提示里要跳过这种错误别给用户弹一条网络异常——明明是正常取消用户什么都没做错。4.4 文件下载与进度展示还有一个容易被忽略的场景文件下载。如果你用 axios 直接下载二进制文件不设置 responseType拿到的 data 会是一堆乱码字符串。正确姿势是const res await request.get(/file/export, { responseType: blob, timeout: 60000 // 大文件下载超时时间要放宽 }) // 生成下载链接 const blobUrl URL.createObjectURL(res.data) const link document.createElement(a) link.href blobUrl link.download 报表.xlsx link.click() URL.revokeObjectURL(blobUrl)下载进度可以配合 onDownloadProgressawait request.get(/file/export, { responseType: blob, onDownloadProgress(progressEvent) { const percent Math.round( (progressEvent.loaded / (progressEvent.total || 1)) * 100 ) console.log(下载进度${percent}%) } })上传文件则用 FormData onUploadProgress。这些在管理后台的导入导出功能里几乎一定会用到提前封装好能省很多事。5. 常见问题与排查技巧实录这部分是我多年实操中踩过、帮同事也排查过的坑整理成速查表你遇到问题直接对着看。5.1 为什么组件里拿到的 res 没有 data极大概率是拦截器的返回写错了。检查你的响应拦截器是return data我推荐的方式还是return response。如果是后者那你调用处就必须写res.data才能拿到业务数据。这里没有对错关键是接口函数、拦截器、使用方三者的类型和写法保持一致。最怕的就是有的人拦截器返回 response某个调用处却写成res.data.data又能在 devtools 里跑通纯属运气好。5.2 跨域报错怎么办开发环境下最常见的报错是Access-Control-Allow-Origin。这是浏览器同源策略在拦你。生产环境一般交给后端配反向代理开发环境我推荐用 Vite 的 proxy 解决// vite.config.ts export default defineConfig({ server: { proxy: { /api: { target: https://mock.example.com, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })注意这里有个小坑baseURL 写了/apiproxy 也匹配/api但后端接口可能不包含/api前缀。rewrite 的作用就是把前缀剥掉。你要是忘了 rewrite请求会到后端地址/api/xxx大概率 404。5.3 为什么请求总是慢半拍并行请求可以怎么优化Axios 本身是串行 await 的如果你一个页面里先 await 接口 A再 await 接口 B而 AB 之间没有依赖那白白多等一个接口的耗时。正确姿势是并行const [profileRes, optionsRes] await Promise.all([ getProfileApi(), getOptionsApi() ])代码逻辑清晰总耗时从 AB 变成了 max(A, B)。这个优化在接口多、数据互不依赖的页面里收益很明显尤其是首屏加载场景。另外如果你有进入页面立刻加载和用户交互后加载两种场景别在 onMounted 里把所有请求都塞进去。把纯展示、不需要用户操作的数据提前到路由守卫或父组件去请求往往能再提前几百毫秒。5.4 TS 里老是提示 res.data 类型不对最常见的原因是后端返回的数据结构和你的 interface 对不上比如后端code是字符串 0 而不是数字 0。这时候先别急着加as any先打一下真实响应确认字段。很多所谓类型问题根本不是 TS 的问题是接口文档和实际返回不一致。我处理这种问题的标准做法是先用 console.log 打 res 看真实结构修正类型定义再跑 TypeScript 检查。宁可多看两眼接口返回也不要到处as any不然类型保护就成了摆设。5.5 请求头里的 Content-Type 到底什么时候需要改默认我们设置了application/json绝大多数 POST 请求就够用了。但有几个场景要改文件上传用multipart/form-data并且不要手动设置 Content-Type让浏览器自动带上 boundary。表单提交传统application/x-www-form-urlencoded需要自己把对象转成 URLSearchParams。用 axios 上传文件时你直接传 FormData 对象给 dataaxios 会自动处理。手动设置了错误的 Content-Type 反而会导致后端接收不到文件字段。这块我也吃过亏在某台服上手动设成multipart/form-data不写 boundary上传一直失败后来删掉手动设置直接把 FormData 扔进去就好了。5.6 再多说几个我踩过的坑除了上面这些还有几个零散的坑值得单独提一下axios 默认的 params 序列化对数组的处理是tags[]1tags[]2有些后端要的是tags1tags2或者 JSON 字符串。这时候需要配置 paramsSerializer或者在后端约定好之前先问清楚。为此我掉进过前端数组参数对不上的坑排查了半天发现是序列化方式不一致。如果你的项目里要上传大文件记得把全局 timeout 调大或者针对该请求单独覆盖。全局 15 秒超时在普通 JSON 接口上够用但传 200MB 的文件肯定超。环境变量命名一定要以 VITE_ 开头否则import.meta.env里读不到。我曾经在 .env 文件里写了API_BASE_URL没加 VITE_ 前缀结果打包后接口路径全部变成 undefined排查到怀疑人生。拦截器和 API 模块的循环引用问题如果 api 模块引用了 requestrequest 里又引用了某个 api 做 token 刷新注意别写成循环依赖。一般我会把 token 刷新的逻辑放在一个单独的文件里避免 request.ts 和 api 模块互相 require。6. 请求层设计的心得什么时候该做全局什么时候该放手我在前面给了不少统一处理的封装思路但有个很重要的原则要提醒你不是所有东西都适合塞进全局拦截器。有些团队把几十个错误码的判断全堆在拦截器里最后拦截器变成一个两三百行的怪兽没人敢动。这其实是把复杂度从组件转移到了请求层问题并没有消失。我的取舍标准很简单和 HTTP 协议强相关的放全局和业务强相关的放局部。Token 携带、401 跳登录、超时、网络异常这些是全局的任何接口都要面对。业务 code 的判断比如某个模块特有的业务码、特殊的业务字段我倾向于在每个 API 函数里处理而不是全堆在拦截器。这样拦截器保持短小稳定业务逻辑留在业务模块里后期维护的时候边界清晰改别人的代码也更容易看懂。再者不要在产品还没稳定的时候过度设计。你完全可以从最简单的 axios.get 裸写开始等发现重复代码变多了再一步步引入拦截器、封装 useRequest。博客里给出的是一套成熟方案但你的项目完全可以根据自己的节奏渐进式落地。很多东西不是一上来就要全上工程化的本质是解决问题不是堆砌模式。说点我自己的体会。刚开始学前端那会我也觉得 axios 不就是个发请求的库照着文档调一调就完事了。直到后来维护一个老项目里面请求代码散落一地、错误处理全靠 alert、Token 过期了不会自动跳登录页我才意识到真正拉开差距的不是会用 axios而是会不会设计请求层。这篇文章给的封装方案本质是把 HTTP 请求的复杂度收拢到一个入口让业务代码保持干净。你把这个思路吃透了不管以后项目是用 Vue2、Vue3 还是 React都能移植过去。最后再补一句demo 里的代码你直接抄没问题但一定要结合你项目的实际情况调整——尤其是后端的响应协议结构、错误码定义、token 方案。别人嚼过的糖只是让你少走一段弯路路终究要自己走一遍。折腾出属于你自己的 request 封装那种感觉是看一百遍教程也换不来的。

相关新闻

PSO-CNN-RF-ABKDE组合模型:多变量时序预测与超参数优化实践

PSO-CNN-RF-ABKDE组合模型:多变量时序预测与超参数优化实践

我花了一周时间把PSO-CNN-RF-ABKDE这套组合模型从理论推到落地,又在多个数据集上做了对比实验。整体下来的感受是:这个方向被很多文章写复杂了,实际拆开看就是三件事——用粒子群算法解决CNN和RF的超参数搜索问题,用CNNRF的并联结…

2026/10/17 19:36:47 阅读更多 →
自制浏览器网页背景:突破CSS局限的底层底色控制方案

自制浏览器网页背景:突破CSS局限的底层底色控制方案

1. 项目概述:这不是换张壁纸,而是一次对浏览器底层交互逻辑的重新理解“自制浏览器网页背景是什么体验?”——这句话最近在几个技术向社区里反复出现,不是作为教程标题,而是像一句带着试探和兴奋的自问。它背后藏着的&…

2026/10/17 19:36:47 阅读更多 →
内存映射I/O与独立端口I/O:从原理到驱动实战

内存映射I/O与独立端口I/O:从原理到驱动实战

做底层开发这些年,几乎天天跟I/O映射和内存映射这两大硬件通信范式打交道。不管是写一个最简单的点灯驱动,还是排查一块板载网卡在不同主机上表现不一的诡异问题,最终都得回到同一个分叉口上:CPU到底怎么访问外部设备?…

2026/10/17 19:35:46 阅读更多 →

最新新闻

外包项目30天:从全栈开发到系统设计的实战蜕变

外包项目30天:从全栈开发到系统设计的实战蜕变

1. 外包项目干了三十天,技术反而突飞猛进先说个反常识的事情:我之前一直以为外包是技术的坟墓,进去就是复制粘贴、写业务代码、跟产品经理扯皮,干完一个项目啥也沉淀不下来。结果上个月临时接了一个外包项目,前后干了一…

2026/10/17 20:25:31 阅读更多 →
XGBoost实战手册:从调参泥潭到梯度提升的工程化指南

XGBoost实战手册:从调参泥潭到梯度提升的工程化指南

简介:《XGBoost with Python》是Jason Brownlee撰写的英文原版经典教材,面向具备一定机器学习基础、希望系统掌握梯度提升与XGBoost建模的开发者与数据科学从业者。全书从AdaBoost与梯度提升原理讲起,逐步过渡到XGBoost的核心特性与优势&…

2026/10/17 20:25:31 阅读更多 →
这篇全身 WAM 无需生成视频,仅用一个模型搞定 11 项家庭任务!TaoToken 统一 Key 实测

这篇全身 WAM 无需生成视频,仅用一个模型搞定 11 项家庭任务!TaoToken 统一 Key 实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 20:25:31 阅读更多 →
Hermes agent 使用记录:把本地 endpoint 改到 TaoToken 的完整配置与验证

Hermes agent 使用记录:把本地 endpoint 改到 TaoToken 的完整配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 20:24:31 阅读更多 →
10分钟看懂多模态视频理解大模型VideoChat-TPO:面向新手的完整入门指南

10分钟看懂多模态视频理解大模型VideoChat-TPO:面向新手的完整入门指南

【免费下载链接】VideoChat-TPO 项目地址: https://ai.gitcode.com/OpenGVLab/VideoChat-TPO 点击查看 免费下载 VideoChat-TPO 是 OpenGVLab 开源的一个多模态视频理解大模型(Multimodal LLM),基于 Mistral-7B 语言模型 ViT 视…

2026/10/17 20:24:31 阅读更多 →
并发容器源码解析:从CAS到锁的Java线程安全设计

并发容器源码解析:从CAS到锁的Java线程安全设计

1. 源码阅读前,先给并发容器建个坐标系我一直觉得,并发容器类是Java并发编程里最值得啃的一块硬骨头。它不像锁那样直观,也不像线程池那样有清晰的配置参数,它的精妙之处全藏在“性能”和“正确性”的权衡里。我自己读JDK源码那段…

2026/10/17 20:24:31 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →