Vue3 + Axios 企业级请求封装实战:从零搭建完整的 HTTP 请求层

本文将分享一套经过生产环境验证的 Axios 请求封装方案,涵盖请求拦截、响应处理、Token 管理、多端适配等核心功能,适用于 Vue3 + TypeScript 项目。

前言

在实际的企业级项目中,一个健壮的 HTTP 请求层需要处理很多问题:

  • 统一的请求头管理(Token、设备信息、来源追踪等)
  • 灵活的响应拦截(错误处理、消息提示、登录态失效)
  • 多端环境适配(H5、微信浏览器、App WebView)
  • 参数加密与安全传输
  • 营销追踪参数透传

本文将逐一拆解这些问题的解决方案。


一、整体架构设计

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                      业务层 (APIs)                           │
│              各业务模块的 API 调用                            │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    HTTP 实例 (单例)                          │
│              配置 baseURL,导出全局实例                       │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                   Axios 封装类                               │
│         请求拦截器 + 响应拦截器 + 参数加密                    │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                      工具层                                  │
│       缓存管理、存储封装、环境判断、Cookie 操作               │
└─────────────────────────────────────────────────────────────┘

二、核心封装实现

2.1 Axios 类封装

typescript 复制代码
// src/plugins/axios/axios.ts
import axios, { AxiosRequestConfig } from 'axios'

export default class Http {
    private instance

    constructor(config: AxiosRequestConfig) {
        this.instance = axios.create(config)
        this.interceptors()
    }

    // 通用请求方法
    public async request<T, D = ResponseResult<T>>(config: AxiosRequestConfig) {
        return new Promise(async (resolve, reject) => {
            try {
                const response = await this.instance.request<D>(this.encryption(config))
                resolve(response.data)
            } catch (error) {
                reject(error)
            }
        }) as Promise<D>
    }

    // 分页请求方法
    public async requestMeta<T, D = ResponsePageResult<T>>(config: AxiosRequestConfig) {
        return new Promise(async (resolve, reject) => {
            try {
                const response = await this.instance.request<D>(this.encryption(config))
                resolve(response.data)
            } catch (error) {
                reject(error)
            }
        }) as Promise<D>
    }

    private interceptors() {
        this.interceptorsRequest()
        this.interceptorsResponse()
    }

    // 参数加密(下文详解)
    private encryption(config: AxiosRequestConfig) {
        // ...
    }
}

2.2 创建全局实例

typescript 复制代码
// src/plugins/axios/index.ts
import Http from './axios'
import env from '@/utils/env'

const http = new Http({
    baseURL: `${env.VITE_API_URL}/${env.VITE_API_PREFIX}`
})

export { http }

三、请求拦截器:统一注入请求头

请求拦截器是整个封装的核心,需要处理以下信息:

3.1 完整实现

typescript 复制代码
private interceptorsRequest() {
    this.instance.interceptors.request.use(
        (config: any) => {
            // 1. 来源标识:区分微信浏览器和普通移动端
            const source = this.isWeiXin() ? 'MP' : 'MB'

            // 2. 平台标识
            const platform = sessionStorage.getItem('platform') || 'h5'

            // 3. 组装请求头
            let headers = {
                ...config.headers,
                platform,
                source,
                Accept: 'application/json',
            } as any

            // 4. 设备ID(用于设备指纹)
            const deviceId = sessionStorage.getItem('deviceId')
            if (deviceId) {
                headers['device-id'] = deviceId
            }

            // 5. 营销追踪参数
            const marketLinkUuid = localStorage.getItem('market-link-uuid')
            if (marketLinkUuid) {
                headers['market-link-uuid'] = marketLinkUuid
            }

            // 6. 认证 Token
            const token = localStorage.getItem('token')
            if (token) {
                headers.Authorization = `Bearer ${token}`
            }

            // 7. 自定义消息头编码(防止中文乱码)
            if (headers.sucMsg) {
                headers.sucMsg = encodeURIComponent(headers.sucMsg)
            }
            if (headers.errMsg) {
                headers.errMsg = encodeURIComponent(headers.errMsg)
            }

            // 8. 浏览器信息(用于日志分析)
            const { name, info } = this.getOSAndBrowserName()
            headers['browser-name'] = name
            headers['browser-info'] = info

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

3.2 请求头字段说明

字段 说明 示例值
platform 平台标识 h5
source 来源标识 MP(微信) / MB(移动端)
device-id 设备唯一标识 uuid-xxx
Authorization 认证令牌 Bearer eyJhbG...
market-link-uuid 营销链接追踪 link-uuid
browser-name 浏览器标识 Android-WeChat
sucMsg / errMsg 自定义提示消息 %E6%93%8D%E4%BD%9C%E6%88%90%E5%8A%9F

四、响应拦截器:统一错误处理

4.1 成功响应处理

一个亮点设计是动态消息替换机制,允许前端自定义提示文案:

typescript 复制代码
// 响应拦截器 - 成功处理
(response) => {
    // 后端返回的消息包含占位符 {#title}
    if (response.data.message.includes('{#title}')) {
        // 解码前端传入的自定义消息
        const sucMsg = decodeURIComponent(response.config.headers.sucMsg || '')
        const errMsg = decodeURIComponent(response.config.headers.errMsg || '')

        // 根据业务码替换消息
        if (response.data.code === 20000) {
            response.data.message = response.data.message.replace('{#title}', sucMsg)
        } else {
            response.data.message = response.data.message.replace('{#title}', errMsg)
        }
    }
    return response
}

使用示例:

typescript 复制代码
// 同一个收藏接口,不同场景显示不同提示
await http.request({
    url: '/api/favorite',
    method: 'POST',
    headers: {
        sucMsg: '课程收藏成功',  // 前端自定义
        errMsg: '课程收藏失败'
    }
})

4.2 错误响应处理

typescript 复制代码
// 响应拦截器 - 错误处理
(error) => {
    closeToast()  // 关闭加载提示

    const { status, data } = error.response

    switch (status) {
        case 401:
            // 认证失效处理
            this.handle401Error()
            break

        case 422:
            // 参数验证错误:提取第一个错误信息
            const firstErrKey = Object.keys(data.errors)[0]
            const errorMsg = data.errors[firstErrKey][0]
            showToast(errorMsg || data.message)
            break

        default:
            showToast(data.message)
    }

    return Promise.reject(error)
}

4.3 401 认证失效的完整处理

typescript 复制代码
private handle401Error() {
    // 1. 判断是否在 App WebView 中
    if (this.isFlutterWebView()) {
        // 通知原生 App 处理登录
        window.flutter_inappwebview.callHandler('flutterHandler', {
            code: 'token_expired',
            msg: '登录已过期'
        })
        return
    }

    // 2. 清除本地登录信息
    this.clearLoginInfo()

    // 3. 跳转登录页
    router.replace('/login')
}

五、参数加密机制

对于敏感参数,可以使用 Base64 编码进行简单加密:

typescript 复制代码
import { Base64 } from 'js-base64'

// 加密方法
function encryption(data: any): string {
    return Base64.encode(
        encodeURI(JSON.stringify(data))
    ).replace(/=/g, '')  // 移除末尾的等号
}

// 解密方法
function decryption(data: string): any {
    return JSON.parse(
        decodeURIComponent(Base64.decode(data))
    )
}

// 在请求中使用
private encryption(config: AxiosRequestConfig) {
    if (config.params?.f) {
        config.params.f = encryption(config.params.f)
    }
    return config
}

使用示例:

typescript 复制代码
// 原始请求
http.request({
    url: '/api/config',
    params: {
        f: { code: 'website_settings', type: 'public' }
    }
})

// 实际发送
// GET /api/config?f=eyJjb2RlIjoid2Vic2l0ZV9zZXR0aW5ncyIsInR5cGUiOiJwdWJsaWMifQ

六、本地存储封装

6.1 带过期时间的存储工具

typescript 复制代码
// src/utils/store.ts
interface CacheData {
    data: any
    expire?: number  // 过期时间戳
}

export default {
    set(key: string, data: any, useLocalStorage = true, expireSeconds?: number) {
        const cache: CacheData = { data }

        if (expireSeconds) {
            cache.expire = Date.now() + expireSeconds * 1000
        }

        const storage = useLocalStorage ? localStorage : sessionStorage
        storage.setItem(key, JSON.stringify(cache))
    },

    get(key: string, useLocalStorage = true, defaultValue: any = null) {
        const storage = useLocalStorage ? localStorage : sessionStorage
        const cacheStr = storage.getItem(key)

        if (!cacheStr) return defaultValue

        const cache: CacheData = JSON.parse(cacheStr)

        // 检查是否过期
        if (cache.expire && cache.expire < Date.now()) {
            storage.removeItem(key)
            return defaultValue
        }

        return cache.data
    },

    remove(key: string, useLocalStorage = true) {
        const storage = useLocalStorage ? localStorage : sessionStorage
        storage.removeItem(key)
    }
}

6.2 登录状态管理

typescript 复制代码
// src/utils/cache.ts
export default {
    // 判断登录状态:同时检查 localStorage 和 Cookie
    getIsLogin(): boolean {
        const token = store.get('token')
        const cookieToken = this.getCookie('token')
        return !!token && !!cookieToken
    },

    // 设置登录信息
    setLoginInfo(data: { token: string }) {
        store.set('token', data.token)
        this.setCookie('token', data.token, {
            domain: '.yourdomain.com',
            path: '/',
            expires: Infinity
        })
    },

    // 清除登录信息
    clearLoginInfo() {
        store.remove('token')
        this.removeCookie('token')
        store.remove('user_info')
    }
}

七、多端环境判断

7.1 微信浏览器判断

typescript 复制代码
function isWeiXin(): boolean {
    const ua = navigator.userAgent.toLowerCase()
    return /micromessenger/i.test(ua)
}

7.2 操作系统和浏览器识别

typescript 复制代码
function getOSAndBrowserName() {
    const ua = navigator.userAgent

    // 操作系统识别
    const osMap = [
        { regex: /Android/i, name: 'Android' },
        { regex: /iPhone|iPad|iPod/i, name: 'iOS' },
        { regex: /Windows/i, name: 'Windows' },
        { regex: /Macintosh/i, name: 'Mac' },
        { regex: /Linux/i, name: 'Linux' }
    ]
    const os = osMap.find(item => item.regex.test(ua))?.name || 'Unknown'

    // 浏览器识别(注意顺序,微信优先)
    const browserMap = [
        { regex: /MicroMessenger/i, name: 'WeChat' },
        { regex: /QQBrowser/i, name: 'QQBrowser' },
        { regex: /Edg/i, name: 'Edge' },
        { regex: /Chrome/i, name: 'Chrome' },
        { regex: /Safari/i, name: 'Safari' },
        { regex: /Firefox/i, name: 'Firefox' }
    ]
    const browser = browserMap.find(item => item.regex.test(ua))?.name || 'Unknown'

    return {
        name: `${os}-${browser}`,
        info: ua
    }
}

7.3 Flutter WebView 判断

typescript 复制代码
function isFlutterWebView(): boolean {
    return !!sessionStorage.getItem('flutter_flag')
}

// 在 Flutter 中打开 WebView 时设置标识
function setFlutterFlag() {
    sessionStorage.setItem('flutter_flag', 'true')
}

八、TypeScript 类型定义

typescript 复制代码
// types/response.d.ts

// 通用响应结构
interface ResponseResult<T> {
    code: number
    message: string
    success: boolean
    data: T
}

// 分页响应结构
interface ResponsePageResult<T> {
    code: number
    message: string
    success: boolean
    data: T[]
    meta: {
        current_page: number
        last_page: number
        per_page: number
        total: number
    }
}

九、实际使用示例

9.1 基础请求

typescript 复制代码
import { http } from '@/plugins/axios'

// GET 请求
const userInfo = await http.request<UserInfo>({
    method: 'GET',
    url: '/user/info'
})

// POST 请求
const result = await http.request<LoginResult>({
    method: 'POST',
    url: '/auth/login',
    data: { username, password }
})

9.2 分页请求

typescript 复制代码
const courseList = await http.requestMeta<Course>({
    method: 'GET',
    url: '/courses',
    params: { page: 1, per_page: 10 }
})

console.log(courseList.data)       // Course[]
console.log(courseList.meta.total) // 总数

9.3 带加密参数

typescript 复制代码
const config = await http.request({
    method: 'GET',
    url: '/config',
    params: {
        f: { code: 'site_settings' }  // 自动 Base64 加密
    }
})

9.4 自定义提示消息

typescript 复制代码
await http.request({
    method: 'POST',
    url: '/favorite',
    data: { course_id: 123 },
    headers: {
        sucMsg: '收藏成功,可在"我的收藏"中查看',
        errMsg: '收藏失败,请稍后重试'
    }
})

十、最佳实践总结

  1. 单例模式:全局只创建一个 HTTP 实例,避免重复配置

  2. 拦截器分离:请求拦截和响应拦截逻辑清晰分离,便于维护

  3. Token 双重存储:同时存储在 localStorage 和 Cookie 中,兼容不同场景

  4. 环境适配:通过 UA 判断自动适配微信、App WebView 等环境

  5. 消息定制化:通过 headers 传递自定义消息,同一接口可显示不同提示

  6. 类型安全:充分利用 TypeScript 泛型,保证类型推导正确

  7. 优雅降级:401 错误时区分 Web 和 App 环境,分别处理


结语

一个好的请求封装层应该是"无感"的------业务开发者只需要关注接口调用,而不用操心 Token 注入、错误处理、环境适配等底层细节。

本文分享的方案已在多个生产项目中稳定运行,希望能给你的项目带来一些启发。

如果你有更好的实践方案,欢迎在评论区交流讨论!


相关技术栈: Vue 3 + TypeScript + Axios + Vant + Pinia

适用场景: H5 移动端、微信公众号、App 内嵌 WebView

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax