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

相关推荐
如果你好2 小时前
一文了解 Cookie、localStorage、sessionStorage的区别与实战案例
前端·javascript
前端无涯2 小时前
React父子组件回调传参避坑指南:从基础到进阶实践
前端·react.js
RichardMiao2 小时前
axios 的 withCredentials 到底做了什么?
前端·javascript·http
donecoding2 小时前
CSS scroll-behavior 与 JS scrollTo 的协同与博弈
前端
匠心网络科技2 小时前
JavaScript进阶-深入解析ES6的Set与Map
前端·javascript·学习·ecmascript·es6
Moment2 小时前
到底选 Nuxt 还是 Next.js?SEO 真的有那么大差距吗 🫠🫠🫠
前端·javascript·后端
神州数码云基地2 小时前
首次开发陌生技术?用 AI 赋能前端提速开发!
前端·人工智能·开源·ai开发
程序员小易3 小时前
前端轮子(2)--diy响应数据
前端·javascript·浏览器
前天的五花肉3 小时前
D3.js研发Biplot(代谢)图
前端·javascript·css