← 返回首页

基于TypeScript的Axios封装实践方案

2026年3月21日 #typescript #封装 #请求 #axios

在项目中,合理封装请求可以提升代码复用性、统一错误处理并增强类型安全,本文将介绍一个完整的TypeScript封装方案,包含uniapp端和pc端请求拦截、响应拦截、错误处理和模块化API管理。

uniapp端请求封装(ts版)

// request.ts
const base_url = 'https://xx.com'

// ===== 类型定义 =====
interface ApiResponse<T = any> {
    code: number
    msg: string
    data: T
}

interface RequestOptions<T = any> {
    url?: string
    method?: UniApp.RequestOptions['method']
    data?: any
    header?: Record<string, any>
    loading?: boolean
    showError?: boolean
    showSuccess?: boolean   // ✅ 新增
    responseType?: UniApp.RequestOptions['responseType']
    timeout?: number
}

// ===== loading 控制 =====
let loadingCount = 0

function showLoading(title = '加载中...') {
    if (loadingCount === 0) {
        uni.showLoading({ title, mask: true })
    }
    loadingCount++
}

function hideLoading() {
    if (loadingCount > 0) {
        loadingCount--
    }
    if (loadingCount === 0) {
        uni.hideLoading()
    }
}

// ===== 核心请求函数 =====
export function request<T = any>(options: RequestOptions<T> = {}): Promise<ApiResponse<T>> {
    const {
        url = '',
        method = 'GET',
        data = {},
        header = {},
        loading = false,
        showError = true,
        showSuccess = false, 
        responseType = 'text',
        timeout = 15000
    } = options

    // token 注入
    const requestHeader: Record<string, any> = { ...header }
    const token = uni.getStorageSync('token')
    if (token) {
        requestHeader.Authorization = `Bearer ${token}`
    }

    if (loading) {
        showLoading()
    }

    return new Promise((resolve, reject) => {
        uni.request({
            url: base_url + url,
            method,
            data,
            header: requestHeader,
            timeout,
            responseType,

            success: (res) => {
                const { statusCode } = res
                const resData = res.data as ApiResponse<T>

                // HTTP 错误
                if (statusCode !== 200) {
                    handleHttpError(statusCode || 0, showError)
                    reject(res)
                    return
                }

                // 业务错误
                if (resData.code !== 1) {
                    if (showError) {
                        uni.showToast({
                            title: resData.msg || '请求失败',
                            icon: 'none'
                        })
                    }

                    if (resData.code === 401) {
                        handleLogout()
                    }

                    reject(resData)
                    return
                }

                //  成功提示
                if (showSuccess) {
                    uni.showToast({
                        title: resData.msg || '操作成功',
                        icon: 'none'
                    })
                }

                resolve(resData)
            },

            fail: (err) => {
                if (showError) {
                    uni.showToast({
                        title: '网络异常,请稍后再试',
                        icon: 'none'
                    })
                }
                reject(err)
            },

            complete: () => {
                if (loading) {
                    hideLoading()
                }
            }
        })
    })
}

// ===== HTTP 错误处理 =====
function handleHttpError(statusCode: number, showError: boolean) {
    const map: Record<number, string> = {
        400: '请求错误',
        401: '未授权,请重新登录',
        403: '拒绝访问',
        404: '接口不存在',
        500: '服务器错误'
    }

    if (showError) {
        uni.showToast({
            title: map[statusCode] || '网络错误',
            icon: 'none'
        })
    }

    if (statusCode === 401) {
        handleLogout()
    }
}

// ===== 退出登录 =====
function handleLogout() {
    uni.removeStorageSync('token')
    uni.reLaunch({
        url: '/pages/login/login'
    })
}

使用方法:

import { request } from '@/utils/request'
const res = await request<UserInfo>({
  url: '/user/info',
  method: 'GET',
  loading: true
})

uniapp端请求封装(js版)

const base_url = 'https://xx.com'

// ===== loading 控制 =====
let loadingCount = 0

function showLoading(title = '加载中...') {
    if (loadingCount === 0) {
        uni.showLoading({ title, mask: true })
    }
    loadingCount++
}

function hideLoading() {
    if (loadingCount > 0) {
        loadingCount--
    }
    if (loadingCount === 0) {
        uni.hideLoading()
    }
}

// ===== 核心请求函数 =====
export function request(options = {}) {
    const {
        url = '',
        method = 'GET',
        data = {},
        header = {},
        loading = false,
        showError = true,
        showSuccess = false,
        responseType = 'text',
        timeout = 15000
    } = options

    // token 注入
    const requestHeader = { ...header }
    const token = uni.getStorageSync('token')
    if (token) {
        requestHeader.Authorization = `Bearer ${token}`
    }

    if (loading) {
        showLoading()
    }

    return new Promise((resolve, reject) => {
        uni.request({
            url: base_url + url,
            method,
            data,
            header: requestHeader,
            timeout,
            responseType,

            success: (res) => {
                const statusCode = res.statusCode
                const resData = res.data || {}

                // HTTP 错误
                if (statusCode !== 200) {
                    handleHttpError(statusCode || 0, showError)
                    reject(res)
                    return
                }

                // 业务错误
                if (resData.code !== 1) {
                    if (showError) {
                        uni.showToast({
                            title: resData.msg || '请求失败',
                            icon: 'none'
                        })
                    }

                    if (resData.code === 401) {
                        handleLogout()
                    }

                    reject(resData)
                    return
                }

                // 成功提示
                if (showSuccess) {
                    uni.showToast({
                        title: resData.msg || '操作成功',
                        icon: 'none'
                    })
                }

                resolve(resData)
            },

            fail: (err) => {
                if (showError) {
                    uni.showToast({
                        title: '网络异常,请稍后再试',
                        icon: 'none'
                    })
                }
                reject(err)
            },

            complete: () => {
                if (loading) {
                    hideLoading()
                }
            }
        })
    })
}

// ===== HTTP 错误处理 =====
function handleHttpError(statusCode, showError) {
    const map = {
        400: '请求错误',
        401: '未授权,请重新登录',
        403: '拒绝访问',
        404: '接口不存在',
        500: '服务器错误'
    }

    if (showError) {
        uni.showToast({
            title: map[statusCode] || '网络错误',
            icon: 'none'
        })
    }

    if (statusCode === 401) {
        handleLogout()
    }
}

// ===== 退出登录 =====
function handleLogout() {
    uni.removeStorageSync('token')
    uni.reLaunch({
        url: '/pages/login/login'
    })
}

pc端请求封装(ts版)

import axios, { type AxiosRequestConfig, type AxiosResponse, type AxiosInstance } from 'axios'
import { ElLoading, ElMessage, type LoadingInstance } from 'element-plus'
import router from '@/router'
import { useUserStore } from '@/stores/user'

/**
 * Axios 实例
 * baseURL 优先使用环境变量,没有则退回 /api
 */
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

/**
 * 后端统一返回结构
 */
export interface ApiResponse<T = unknown> {
  data: T
  msg: string
  code: number
}

/**
 * 扩展后的请求配置
 * 仍兼容 AxiosRequestConfig
 */
export interface RequestConfig extends AxiosRequestConfig {
  showSuccessMessage?: boolean
  showErrorMessage?: boolean
  showLoading?: boolean
}

/**
 * 自定义默认行为(集中管理)
 */
const options = {
  showSuccessMessage: false,
  showErrorMessage: true,
  showLoading: false
}

// -------- Loading 计数,避免多个请求同时闪烁 --------
let loadingInstance: LoadingInstance | null = null
let loadingCount = 0

function showLoading(title = '加载中...'): void {
  if (loadingCount === 0) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: title,
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  loadingCount += 1
}

function hideLoading(): void {
  if (loadingCount > 0) {
    loadingCount -= 1
  }
  if (loadingCount === 0) {
    loadingInstance?.close()
    loadingInstance = null
  }
}

// -------- 请求拦截器 --------
service.interceptors.request.use(
  (config) => {
    const requestConfig = config as RequestConfig

    // 根据 options 控制 loading
    if (requestConfig.showLoading ?? options.showLoading) {
      showLoading()
    }

    // 统一拼装请求头
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers = {
        ...(config.headers || {}),
        Authorization: `Bearer ${userStore.token}`
      }
    }

    return config
  },
  (error) => Promise.reject(error)
)

// -------- 响应拦截器 --------
service.interceptors.response.use(
  (response: AxiosResponse<ApiResponse>) => {
    const config = response.config as RequestConfig
    if (config.showLoading ?? options.showLoading) {
      hideLoading()
    }

    const { code, msg } = response.data
    const showError = config.showErrorMessage ?? options.showErrorMessage

    if (code === 1) {
      if (config.showSuccessMessage ?? options.showSuccessMessage) {
        ElMessage.success(msg || '操作成功')
      }
      return response
    }

    if (showError) {
      ElMessage.error(msg || '请求失败')
    }
    if (code === 401) {
      handleLogout()
    }
    return Promise.reject(msg || '请求失败')
  },
  (error) => {
    const config = (error?.config || {}) as RequestConfig
    if (config.showLoading ?? options.showLoading) {
      hideLoading()
    }

    const showError = config.showErrorMessage ?? options.showErrorMessage
    if (error?.response?.status) {
      handleHttpError(error.response.status, showError)
    } else if (showError) {
      ElMessage.error('网络异常,请稍后再试')
    }

    return Promise.reject(error)
  }
)

/**
 * 核心请求函数
 * 约定:后端 code === 1 才视为成功
 */
export async function request<T = unknown>(config: RequestConfig): Promise<T> {
  const response = await service.request<ApiResponse<T>, AxiosResponse<ApiResponse<T>>>(config)

  return response.data.data as T
}

// -------- HTTP 状态码统一处理 --------
function handleHttpError(status: number, showError: boolean): void {
  const map: Record<number, string> = {
    400: '请求错误',
    401: '未授权,请重新登录',
    403: '拒绝访问',
    404: '接口不存在',
    500: '服务器错误'
  }

  if (showError) {
    ElMessage.error(map[status] || '网络错误')
  }

  if (status === 401) {
    handleLogout()
  }
}

// -------- 退出登录 --------
function handleLogout(): void {
  const userStore = useUserStore()
  userStore.clearAuth()
  router.replace({ name: 'login' })
}

使用方法

import { request } from '@/utils/request'

export function getProfile() {
  return request<{ name: string; uid: number }>({
    url: '/api/user/profile',
    method: 'get'
  })
}

export function updateProfile(data: { name: string }) {
  return request({
    url: '/api/user/profile',
    method: 'post',
    data,
    showLoading: true,
    showSuccessMessage: true
  })
}

pc端请求封装(js版)

import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import router from '@/router'
import { useUserStore } from '@/stores/user'

/**
 * Axios 实例
 */
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

/**
 * 默认配置
 */
const options = {
  showSuccessMessage: false,
  showErrorMessage: true,
  showLoading: false
}

// -------- Loading 控制 --------
let loadingInstance = null
let loadingCount = 0

function showLoading(title = '加载中...') {
  if (loadingCount === 0) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: title,
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
  loadingCount++
}

function hideLoading() {
  if (loadingCount > 0) {
    loadingCount--
  }
  if (loadingCount === 0) {
    loadingInstance && loadingInstance.close()
    loadingInstance = null
  }
}

// -------- 请求拦截器 --------
service.interceptors.request.use(
  (config) => {
    const requestConfig = config

    // loading 控制
    if (
      requestConfig.showLoading !== undefined
        ? requestConfig.showLoading
        : options.showLoading
    ) {
      showLoading()
    }

    // token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers = {
        ...(config.headers || {}),
        Authorization: `Bearer ${userStore.token}`
      }
    }

    return config
  },
  (error) => Promise.reject(error)
)

// -------- 响应拦截器 --------
service.interceptors.response.use(
  (response) => {
    const config = response.config

    if (
      config.showLoading !== undefined
        ? config.showLoading
        : options.showLoading
    ) {
      hideLoading()
    }

    const { code, msg } = response.data
    const showError =
      config.showErrorMessage !== undefined
        ? config.showErrorMessage
        : options.showErrorMessage

    // 成功
    if (code === 1) {
      if (
        config.showSuccessMessage !== undefined
          ? config.showSuccessMessage
          : options.showSuccessMessage
      ) {
        ElMessage.success(msg || '操作成功')
      }
      return response
    }

    // 失败
    if (showError) {
      ElMessage.error(msg || '请求失败')
    }

    if (code === 401) {
      handleLogout()
    }

    return Promise.reject(msg || '请求失败')
  },
  (error) => {
    const config = error?.config || {}

    if (
      config.showLoading !== undefined
        ? config.showLoading
        : options.showLoading
    ) {
      hideLoading()
    }

    const showError =
      config.showErrorMessage !== undefined
        ? config.showErrorMessage
        : options.showErrorMessage

    if (error?.response?.status) {
      handleHttpError(error.response.status, showError)
    } else if (showError) {
      ElMessage.error('网络异常,请稍后再试')
    }

    return Promise.reject(error)
  }
)

/**
 * 核心请求函数
 */
export async function request(config) {
  const response = await service.request(config)
  return response.data.data
}

// -------- HTTP 状态码处理 --------
function handleHttpError(status, showError) {
  const map = {
    400: '请求错误',
    401: '未授权,请重新登录',
    403: '拒绝访问',
    404: '接口不存在',
    500: '服务器错误'
  }

  if (showError) {
    ElMessage.error(map[status] || '网络错误')
  }

  if (status === 401) {
    handleLogout()
  }
}

// -------- 退出登录 --------
function handleLogout() {
  const userStore = useUserStore()
  userStore.clearAuth()
  router.replace({ name: 'login' })
}