Skip to content
第 22 章 前端 ⏱ 13 分钟阅读

第 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

动手练习 ​

  1. 封装 axios:加 token 自动处理 401
  2. useRequest:写一个支持立即执行的组合式
  3. 取消请求:路由切换时 abort 掉旧请求

下一章:第 23 章:Element Plus 实战 →

本站基于 VitePress 构建 · 由 StackHub 团队维护