一、先搞清楚 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 项目跟后端对接就非常丝滑了。把上面的代码按步骤加到你的项目里,以后所有请求都井井有条。
有问题评论区说,我挨个回。下篇见!