本指南基于当前系统的 http.ts 封装实践,归纳出一套可供其他项目复用的 Axios 二次封装方案。
1. 核心封装逻辑 (封装指南)
1.1 认证与标识 (Request Interceptor)
- 多 Token 支持 :通过请求头
Authorization(通用 Token) 和IAM-Authorization(跨系统登录 Token) 实现兼容。 - 环境上下文注入 :自动在 Header 中注入
language(语言)、moduleId(模块 ID)、Unique-Identifier(设备指纹) 等。
1.2 响应预处理 (Response Interceptor)
- 下载异常拦截 :针对
blob响应,自动检测后端返回的特殊错误码(如 416 文件过大),并执行业务重定向(如引导至下载任务中心)。 - 认证失效自动登出:当接口返回 401 或特定的过期状态码时,封装层应自动清理本地存储、通知全局状态管理并跳转至登录页。
1.3 监控与上报
- Sentry 集成:接口异常(非 200)时自动上报请求 URL、参数及返回结果,实现"静默监控"。
2. 跨系统复用指南
2.1 依赖准备
复用前请确保目标系统已安装:
axiosqsant-design-vue(或相应的 UI 组件库用于提示)pinia(或其它状态管理工具)
2.2 解耦与适配建议
若要在其它系统中快速集成,请参考以下代码结构:
-
环境配置分离 : 将 API 地址映射、Mock 规则等配置项提取到
config/目录下。 -
行为注入 (Hooks) : 将具体的业务行为(如
router.push、store.logout)作为初始化函数传入封装类,而非在封装层硬编码引用。 -
错误码外部映射 : 通过配置表(如
errorCode.ts)映射全局错误提示,以便在不同系统中按需定制 UI 反馈。
3. 复用示例结构
建议将该工具打包为独立模块,结构如下:
index.ts: 导出封装后的 http 实例interceptor.ts: 请求与响应拦截器定义types.ts: 类型定义config.ts: 基础配置 (URL, Timeout等)
typescript
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse, Method, ResponseType } from 'axios'
import qs from 'qs'
import { message, Modal } from 'ant-design-vue'
/**
* 可复用 Axios 封装模板。
* 复用时请通过 initHttpBehaviors 注入以下系统耦合行为:
* - onLogout(): 清理 token、触发 store 登出
* - onRedirect(path): 页面跳转(如 router.push)
* - getEquipmentName(): 返回设备标识字符串(可选,对应 Equipment-Name 请求头)
*/
// ── 行为注入(复用时替换为实际实现)────────────────────────────────────────
let onLogout: () => void = () => {}
let onRedirect: (path: string) => void = () => {}
let getEquipmentName: () => string | null = () => null
export const initHttpBehaviors = (hooks: {
onLogout?: () => void
onRedirect?: (path: string) => void
getEquipmentName?: () => string | null
}) => {
if (hooks.onLogout) onLogout = hooks.onLogout
if (hooks.onRedirect) onRedirect = hooks.onRedirect
if (hooks.getEquipmentName) getEquipmentName = hooks.getEquipmentName
}
// ── Sentry 上报占位(复用时替换为实际 Sentry 实例)────────────────────────
const reportApiError = (response: AxiosResponse) => {
// TODO: 接入 Sentry,上报请求 URL、参数及返回结果
// sentry.captureMessage(response.config.url, ...)
console.warn('[API Error]', response.config.url, response.data)
}
// 1. 基础配置(复用时可改为环境变量或 baseURLMap)
const baseURL = import.meta.env.VITE_APP_BASE_URL || ''
axios.defaults.baseURL = baseURL
axios.defaults.timeout = 90000
// 2. 请求拦截器:注入认证与标识
axios.interceptors.request.use(
(config: AxiosRequestConfig) => {
config.headers = config.headers || {}
const token = localStorage.getItem('token')
const iamToken = localStorage.getItem('iamToken')
const moduleId = localStorage.getItem('moduleId')
const uniqueIdentifier = localStorage.getItem('uniqueIdentifier')
// 每次请求时实时读取,保证语言切换后立即生效
const lang = (localStorage.getItem('lang') || 'zh-cn') as 'zh-cn' | 'en-us'
const equipmentName = getEquipmentName()
if (token) config.headers.Authorization = token
if (iamToken) config.headers['IAM-Authorization'] = iamToken
if (uniqueIdentifier) config.headers['Unique-Identifier'] = uniqueIdentifier
if (equipmentName) config.headers['Equipment-Name'] = equipmentName
if (moduleId && !['undefined', 'null'].includes(moduleId)) {
config.headers.moduleId = moduleId
}
// 业务标识:0-中文,1-英文
config.headers.language = lang === 'zh-cn' ? '0' : '1'
return config
},
err => {
return Promise.reject(err)
}
)
// 3. 响应拦截器:全局错误处理与文件流下载
axios.interceptors.response.use(
response => {
const { code, data, msg } = response.data
const isStream = ['blob', 'arraybuffer'].includes(response.config.responseType as string)
// 3.1 处理文件流下载异常
if (isStream) {
let downloadExceptionCode: string | null = null
try {
downloadExceptionCode = JSON.parse(
String.fromCharCode.apply(null, new Uint8Array(response.data) as any)
).data
} catch {}
if (downloadExceptionCode === '416') {
// 文件过大,引导至下载任务中心
Modal.confirm({
title: '提示',
content: '文件较大,请前往下载任务中心查看',
okText: '前往下载',
cancelText: '取消',
onOk() {
onRedirect('/download-task')
}
})
return false
} else if (downloadExceptionCode === '550') {
message.error('下载异常,请稍后重试')
return false
}
return response
}
// 3.2 业务成功逻辑 (code === 200)
if (code === 200) {
if (data === 0) return data
if (typeof data === 'boolean') return data
return data !== undefined ? data : true
}
// 3.3 上报异常
reportApiError(response)
// 3.4 认证失效:401
if (code === 401) {
message.error(msg || '登录已过期,请重新登录')
onLogout()
onRedirect('/login')
return false
}
// 3.5 其他业务错误提示(可由页面自行处理时传 isPageHandle: true 跳过)
if (!(response.config as any).isPageHandle && msg) {
message.error(msg)
}
return false
},
err => {
if (err.response?.status === 401) {
message.error('认证过期,请重新登录')
onLogout()
onRedirect('/login')
} else {
try {
if ((err + '').includes('timeout')) return 'timeout'
} catch {}
message.error(err.response?.data?.msg || '网络异常,请稍后重试')
}
return false
}
)
// 4. 封装导出方法
const http = {
get(url: string, param?: any, isPageHandle?: boolean): Promise<any> {
return new Promise(resolve => {
axios({ method: 'get', url, params: param, isPageHandle } as AxiosRequestConfig)
.then(res => resolve(res))
})
},
post(url: string, param?: any, isPageHandle?: boolean, timeout?: number): Promise<any> {
return new Promise(resolve => {
axios({ method: 'post', url, data: param, isPageHandle, timeout } as AxiosRequestConfig)
.then(res => resolve(res))
})
},
patch(url: string, param?: any, isPageHandle?: boolean, timeout?: number): Promise<any> {
return new Promise(resolve => {
axios({ method: 'patch', url, data: param, isPageHandle, timeout } as AxiosRequestConfig)
.then(res => resolve(res))
})
},
put(url: string, param?: any): Promise<any> {
return new Promise(resolve => {
axios({ method: 'put', url, data: qs.stringify(param) })
.then(res => resolve(res))
})
},
delete(url: string, param?: any): Promise<any> {
return new Promise(resolve => {
axios({ method: 'delete', url, data: param })
.then(res => resolve(res))
})
},
postForm(url: string, param?: any): Promise<any> {
return new Promise(resolve => {
axios({ method: 'post', url, data: qs.stringify({ ...param }) })
.then(res => resolve(res))
})
},
getDocImgStream(url: string, param?: any, responseType = 'blob', timeout = 600000): Promise<any> {
return new Promise(resolve => {
axios({ method: 'get', url, data: param, responseType, timeout } as AxiosRequestConfig)
.then(res => resolve(res))
})
},
postDocImgStream(url: string, param?: any, responseType = 'blob', timeout = 600000): Promise<any> {
return new Promise(resolve => {
axios({ method: 'post', url, data: param, responseType, timeout } as AxiosRequestConfig)
.then(res => resolve(res))
})
},
base: axios
}
// 与 http.ts 保持一致,单独具名导出(import request, { download } from '@/utils/http')
export const download = async (
url: string,
data: any,
method: Method = 'post',
responseType: ResponseType = 'blob',
timeout: number = 600000
) => {
const res = await axios({ method, url, data, responseType, timeout })
return Promise.resolve(res.data)
}
export default http