Axios 二次封装指南 & 跨系统复用建议

本指南基于当前系统的 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 依赖准备

复用前请确保目标系统已安装:

  • axios
  • qs
  • ant-design-vue (或相应的 UI 组件库用于提示)
  • pinia (或其它状态管理工具)

2.2 解耦与适配建议

若要在其它系统中快速集成,请参考以下代码结构:

  1. 环境配置分离 : 将 API 地址映射、Mock 规则等配置项提取到 config/ 目录下。

  2. 行为注入 (Hooks) : 将具体的业务行为(如 router.push、store.logout)作为初始化函数传入封装类,而非在封装层硬编码引用。

  3. 错误码外部映射 : 通过配置表(如 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
相关推荐
jay神4 分钟前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神12 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat16 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue