Vue + Axios 从入门到封装:拦截器、错误处理、请求取消、接口管理全搞定
一、先搞清楚 Axios 是什么
浏览器自带的fetch能发请求,但写起来比较原始:要手动判断response.ok,要手动转换 JSON,还没有请求/响应拦截这种高级货。Axios是一个第三方库,帮我们把请求这件事变得特别省心:
自动转换 JSON 数据
可以在请求发出前和收到响应后统一处理(拦截器)
支持取消请求
支持请求超时设置
支持请求重试(配合插件)
用人话讲:Axios 就是给fetch包了一层高级皮,让你写更少的代码,干更多的事。
二、在 Vue 项目里装上 Axios
打开你的项目终端,敲一行命令:
bash
npm install axios
装完之后,就能在组件里直接 import 用。但千万别在每一个组件里都写一遍axios.get(...),那样以后要改配置得改几百个文件,会死人。我们需要把它封装成一个统一的“请求工具”。
三、封装第一步:创建 Axios 实例
在src下面新建一个utils/request.js,专门管理 Axios 实例。
javascript
// src/utils/request.js import axios from 'axios' // 创建一个 Axios 实例,就像创建一个“专属的网络助手” const request = axios.create({ // baseURL:所有请求都会自动在前面加上这个地址 // 开发环境用本地代理,或者直接写你的后端地址 baseURL: 'http://localhost:3000/api', // timeout:请求超时时间,超过 10 秒还没响应就自动放弃 timeout: 10000, // headers:每次请求默认带上的请求头 headers: { 'Content-Type': 'application/json' } }) export default request解释:
axios.create()就像克隆一个新 axios,可以有自己的默认配置,不会影响全局。baseURL让你以后写/user/login就行,不用每次写完整地址。timeout防止一个请求卡死半天没反应,用户体验更好。
四、请求拦截器:在请求发出前做点事
很多时候我们需要在请求头里带上 token,告诉后端“我是谁”。如果每个接口都手动加,太蠢了。用请求拦截器,统一在发送前加上。
javascript
// src/utils/request.js(接上面) // 添加请求拦截器 request.interceptors.request.use( (config) => { // config 就是这次请求的所有配置信息,你可以在这里修改它 // 从 localStorage 里取出 token(假设登录后存在那) const token = localStorage.getItem('token') // 如果 token 存在,就在请求头里加上 Authorization 字段 if (token) { config.headers.Authorization = `Bearer ${token}` } // 最后一定要把 config 返回,不然请求发不出去 return config }, (error) => { // 请求出错时的处理(一般不会走到这里) console.error('请求发出失败:', error) return Promise.reject(error) } )重点:config.headers.Authorization这种写法是约定俗成的,后端会从这个字段里拿 token 验证身份。
五、响应拦截器:统一处理返回数据
后端返回的数据一般都有固定格式,比如{ code: 200, data: {...}, message: '成功' }。如果每次请求完都判断code,又累又容易漏。用响应拦截器统一处理。
javascript
// src/utils/request.js(接上面) // 添加响应拦截器 request.interceptors.response.use( (response) => { // 响应状态码是 2xx 时进入这里 // response.data 是后端返回的实际数据 const res = response.data // 根据约定的后端返回码处理 if (res.code === 200) { // 成功,直接返回 data,组件里就不用每次都取 .data 了 return res.data } else if (res.code === 401) { // token 过期或未登录,跳转到登录页 window.location.href = '/login' return Promise.reject(new Error(res.message || '登录已过期')) } else { // 其他业务错误,给个提示 alert(res.message || '请求失败') return Promise.reject(new Error(res.message)) } }, (error) => { // 响应状态码不是 2xx 时进入这里(比如 404、500) if (error.response) { const status = error.response.status switch (status) { case 404: alert('请求的资源不存在') break case 500: alert('服务器错误,请稍后再试') break default: alert(`请求失败,状态码:${status}`) } } else if (error.code === 'ECONNABORTED') { // 超时的错误码是 ECONNABORTED alert('请求超时,请检查网络') } return Promise.reject(error) } )解释:
响应拦截器第一个参数是成功回调,第二个是失败回调。
我们根据
code统一处理业务成功/失败,组件里只接收成功后的data,干净很多。错误状态码(404/500)也在这里统一弹提示,不用在每个请求的地方重复写。
六、封装具体的 API 接口
现在request实例已经很强了,但我们在组件里还不想直接写request.get('/user/login')。最好把接口都集中管理,新建一个src/api文件夹,每个模块一个文件。
6.1 用户相关接口src/api/user.js
javascript
// src/api/user.js // 引入刚才封装好的请求实例 import request from '@/utils/request' // 登录接口 export function login(data) { return request({ url: '/user/login', // 接口路径,会自动拼上 baseURL method: 'post', // 请求方法 data: data // post 请求体数据 }) } // 获取用户信息 export function getUserInfo(userId) { return request({ url: `/user/${userId}`, // restful 风格 method: 'get' }) } // 更新用户信息 export function updateUser(data) { return request({ url: '/user/update', method: 'put', data: data }) } // 退出登录 export function logout() { return request({ url: '/user/logout', method: 'post' }) }6.2 在组件中使用
vue
<template> <div> <button @click="handleLogin">登录</button> <p v-if="user">{{ user.name }}</p> </div> </template> <script setup> import { login, getUserInfo } from '@/api/user' import { ref } from 'vue' const user = ref(null) async function handleLogin() { try { // 调用登录接口,传入用户名密码 const result = await login({ username: 'admin', password: '123456' }) console.log('登录成功返回的数据:', result) // 假设登录后返回了 token 和 userId localStorage.setItem('token', result.token) // 再获取用户信息 user.value = await getUserInfo(result.userId) } catch (error) { console.error('登录流程出错:', error) } } </script>好处:
组件里看不到任何路径和请求细节,只调用
login()就行。以后接口路径变了,只需要改
api/user.js,不用全局搜索改组件。
七、取消重复请求
有时候用户手快点了两下提交按钮,同时发出两个一样的请求,容易造成数据错乱。我们可以在请求拦截器里做请求去重:同一个请求在上一个还没完成时,自动取消上一个。
javascript
// src/utils/request.js 完整版(在上面的基础上增加取消请求功能) import axios from 'axios' // 用于存放正在进行的请求的标识和取消函数 const pendingMap = new Map() // 生成请求的唯一标识(url + method + 参数) function getRequestKey(config) { const { url, method, params, data } = config return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&') } // 添加请求到 pendingMap function addPending(config) { const key = getRequestKey(config) // 如果已经有相同请求在进行,就取消上一个 if (pendingMap.has(key)) { const cancel = pendingMap.get(key) cancel('请求被取消,原因是重复请求') pendingMap.delete(key) } // 给当前请求添加 cancelToken config.cancelToken = new axios.CancelToken((cancel) => { pendingMap.set(key, cancel) }) } // 请求完成后,从 pendingMap 中移除 function removePending(config) { const key = getRequestKey(config) if (pendingMap.has(key)) { pendingMap.delete(key) } } // 创建实例 const request = axios.create({ baseURL: 'http://localhost:3000/api', timeout: 10000 }) // 请求拦截器 request.interceptors.request.use((config) => { // 处理 token(同前面) const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` // 取消重复请求 addPending(config) return config }, (error) => Promise.reject(error)) // 响应拦截器 request.interceptors.response.use((response) => { // 请求完成,移除 pending removePending(response.config) // 和前面的处理一样 const res = response.data if (res.code === 200) return res.data // ... 其他处理 return Promise.reject(new Error(res.message)) }, (error) => { // 如果是取消请求,特殊处理 if (axios.isCancel(error)) { console.log('请求已取消:', error.message) } else { // 其他错误处理 } // 也要移除 pending if (error.config) removePending(error.config) return Promise.reject(error) }) export default request解释:
用一个
Map存正在进行的请求和对应的取消函数。请求前检查是否有相同的 key,有就取消上一个。
请求完成(成功或失败)后移除 key。
组件里不需要任何改动,完全是透明的。
八、环境变量管理
开发环境和生产环境的 API 地址不一样,我们不能每次上线都改代码。Vite 项目支持环境变量文件。
项目根目录新建三个文件:
.env.development
text
VITE_API_BASE_URL=http://localhost:3000/api
.env.production
text
VITE_API_BASE_URL=https://api.yourdomain.com
然后修改request.js中的baseURL:
javascript
const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, // 根据环境自动切换 timeout: 10000 })import.meta.env.VITE_xxx就是读取.env文件里VITE_开头的变量。开发时自动用 development,打包上线时自动用 production。
九、实战:封装一个完整的请求 Hook
结合前面学的组合式函数,我们可以把“发请求+loading+error”封装成一个useRequest。
javascript
// src/hooks/useRequest.js import { ref } from 'vue' export function useRequest(apiFn) { // apiFn 是一个返回 Promise 的函数(比如 () => login(data)) const data = ref(null) // 存放响应数据 const loading = ref(false) // 是否正在加载 const error = ref(null) // 错误信息 async function execute(...args) { loading.value = true error.value = null data.value = null try { const result = await apiFn(...args) data.value = result return result } catch (err) { error.value = err.message || '请求失败' throw err } finally { loading.value = false } } return { data, loading, error, execute } }在组件中使用:
vue
<template> <div> <button @click="doLogin">登录</button> <p v-if="loading">登录中...</p> <p v-else-if="error">错误:{{ error }}</p> <p v-else>用户:{{ data?.name }}</p> </div> </template> <script setup> import { useRequest } from '@/hooks/useRequest' import { login } from '@/api/user' // 不需要手动管理 loading/error,useRequest 帮你全管了 const { data, loading, error, execute: doLogin } = useRequest( () => login({ username: 'admin', password: '123456' }) ) </script>这么一来,组件里关于请求的代码短到只剩一行调用,干净到令人发指。
十、总结
今天我们完整走了一遍 Axios 在 Vue 项目里的最佳实践:
安装并创建实例:统一配置 baseURL、超时时间。
请求拦截器:自动加 token。
响应拦截器:统一处理返回格式、错误码、弹提示。
接口统一管理:按模块放在
api/文件夹,组件只调函数。取消重复请求:避免手快发两次的问题。
环境变量:开发、生产自动切换地址。
封装 useRequest:把 loading/error 逻辑也抽出来,组件极简。
这下你的 Vue 项目跟后端对接就非常丝滑了。把上面的代码按步骤加到你的项目里,以后所有请求都井井有条。
有问题评论区说,我挨个回。下篇见!
