← 返回首页

基于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?: 'GET' | 'POST'
    data?: any
    header?: Record<string, any>
    loading?: boolean
    showError?: boolean
    showSuccess?: boolean   // ✅ 新增
    responseType?: 'text' | 'arraybuffer'
    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 !== 200) {
                    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'
                    })
                }
                // 只返回 data 字段
                resolve(resData.data)
            },

            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 !== 200) {
                    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'
                    })
                }

                // 只返回 data 字段
                resolve(resData.data)
            },

            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,
    type InternalAxiosRequestConfig
} from 'axios'
import { ElLoading, ElMessage, type LoadingInstance } from 'element-plus'
import router from '@/router'

/**
 * 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
}

/**
 * 自定义element-plus行为(集中管理)
 */
const requestConfig = {
    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: InternalAxiosRequestConfig) => {
        const reqConfig = config as RequestConfig

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

        // 统一拼装请求头
        const token = localStorage.getItem('token')
        if (token) {
            config.headers.set('Authorization', `Bearer ${token}`)
        }

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

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

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

        if (code === 200) {
            if (config.showSuccessMessage ?? requestConfig.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 ?? requestConfig.showLoading) {
            hideLoading()
        }

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

        return Promise.reject(error)
    }
)

/**
 * 核心请求函数
 * 约定:后端 code === 200 才视为成功
 */
export async function request<T = unknown>(config: RequestConfig): Promise<T> {
    const response = await service.request<ApiResponse<T>, AxiosResponse<ApiResponse<T>>>(config)
    //成功时只返回 data 字段,简化业务层调用
    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 {
    localStorage.removeItem('token')
    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'
/**
 * Axios 实例
 */
const service = axios.create({
    baseURL: (import.meta.env && import.meta.env.VITE_API_BASE_URL) || '/api',
    timeout: 15000,
    headers: {
        'Content-Type': 'application/json;charset=UTF-8'
    }
})

/**
 * 全局默认配置
 */
const defaultConfig = {
    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 custom = config

        // ⚠️ 修复逻辑:优先用单次配置,否则用全局配置
        if ((custom.showLoading ?? defaultConfig.showLoading)) {
            showLoading()
        }

        // token
        const token = localStorage.getItem('token')
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`
        }

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

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

        if ((config.showLoading ?? defaultConfig.showLoading)) {
            hideLoading()
        }

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

        if (code === 200) {
            if (config.showSuccessMessage ?? defaultConfig.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 ?? defaultConfig.showLoading)) {
            hideLoading()
        }

        const showError = config.showErrorMessage ?? defaultConfig.showErrorMessage

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

        return Promise.reject(error)
    }
)

/**
 * 核心请求函数
 */
export function request(config) {
    return service.request(config).then((res) => {
        //成功时只返回 data 字段,简化业务层调用
        return res.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() {
    localStorage.removeItem('token')
    router.replace({ name: 'login' })
}