第 22 章:请求封装
学习目标
- 用 axios 封装统一请求
- 拦截器处理 token / 错误
- 写 useRequest 业务组合式
- 避坑:loading 全局化陷阱
一、安装 axios
bash
npm i axios二、最小封装
ts
// src/utils/request.ts
import axios from 'axios'
export const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000,
})
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
request.interceptors.response.use(
(res) => res.data,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
location.href = '/login'
}
return Promise.reject(err)
},
)三、统一错误
ts
import { ElMessage } from 'element-plus'
request.interceptors.response.use(
(res) => {
if (res.code !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(res)
}
return res.data
},
)⚠️ 坑 1:拦截器里
Promise.reject后,业务try/catch一定要处理,否则会吞噬错误。
四、API 模块化
ts
// src/api/user.ts
import { request } from '@/utils/request'
export const userApi = {
login: (data: { username: string; password: string }) =>
request.post('/api/login', data),
list: (params: { page: number }) => request.get('/api/users', { params }),
detail: (id: number) => request.get(`/api/users/${id}`),
remove: (id: number) => request.delete(`/api/users/${id}`),
}五、useRequest 组合式
ts
// src/composables/useRequest.ts
import { ref, type Ref } from 'vue'
export function useRequest<T, P = unknown>(
fetcher: (params: P) => Promise<T>,
options: { immediate?: boolean; defaultParams?: P } = {},
) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const run = async (params: P = options.defaultParams!) => {
loading.value = true
error.value = null
try {
data.value = await fetcher(params)
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
if (options.immediate) run()
return { data, loading, error, run }
}vue
<script setup lang="ts">
import { useRequest } from '@/composables/useRequest'
import { userApi } from '@/api/user'
const { data: list, loading, run } = useRequest(() => userApi.list({ page: 1 }), { immediate: true })
</script>六、loading 全局
ts
import axios from 'axios'
let count = 0
const showLoading = () => {
if (count === 0) ElLoading.service({ fullscreen: true })
count++
}
const hideLoading = () => {
count = Math.max(0, count - 1)
if (count === 0) ElLoading.service().close()
}
axios.interceptors.request.use((config) => {
if (config.headers.showLoading !== false) showLoading()
return config
})
axios.interceptors.response.use(
(res) => { hideLoading(); return res },
(err) => { hideLoading(); return Promise.reject(err) },
)⚠️ 坑 2:并发请求时,要用计数器不能用 boolean,否则第一个完成就关了 loading。
七、取消请求
ts
const controller = new AbortController()
request.get('/api/list', { signal: controller.signal })
controller.abort() // 取消ts
request.interceptors.request.use((config) => {
config.signal = config.signal || new AbortController().signal
return config
})八、重试
ts
import axiosRetry from 'axios-retry'
axiosRetry(axios, { retries: 3, retryDelay: axiosRetry.exponentialDelay })⚠️ 坑 3:POST 重试可能造成重复提交,只对 GET 重试。
九、本章小结
| 能力 | 关键 |
|---|---|
| 拦截器 | 统一加 token / 处理错误 |
| useRequest | 业务侧统一 loading/error |
| 全局 loading | 计数器 |
| 取消 | AbortController |
动手练习
- 封装 axios:加 token 自动处理 401
- useRequest:写一个支持立即执行的组合式
- 取消请求:路由切换时 abort 掉旧请求
下一章:第 23 章:Element Plus 实战 →