本文将分享一套经过生产环境验证的 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: '收藏失败,请稍后重试'
}
})
十、最佳实践总结
-
单例模式:全局只创建一个 HTTP 实例,避免重复配置
-
拦截器分离:请求拦截和响应拦截逻辑清晰分离,便于维护
-
Token 双重存储:同时存储在 localStorage 和 Cookie 中,兼容不同场景
-
环境适配:通过 UA 判断自动适配微信、App WebView 等环境
-
消息定制化:通过 headers 传递自定义消息,同一接口可显示不同提示
-
类型安全:充分利用 TypeScript 泛型,保证类型推导正确
-
优雅降级:401 错误时区分 Web 和 App 环境,分别处理
结语
一个好的请求封装层应该是"无感"的------业务开发者只需要关注接口调用,而不用操心 Token 注入、错误处理、环境适配等底层细节。
本文分享的方案已在多个生产项目中稳定运行,希望能给你的项目带来一些启发。
如果你有更好的实践方案,欢迎在评论区交流讨论!
相关技术栈: Vue 3 + TypeScript + Axios + Vant + Pinia
适用场景: H5 移动端、微信公众号、App 内嵌 WebView