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.pushstore.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
相关推荐
伊可历普斯1 小时前
前端数据校验太难?一个 Zod 就够了
前端·javascript
ZoeLandia2 小时前
基于 qiankun 的应用间页面跳转
前端·前端框架
前端 贾公子2 小时前
unplugin-icons == elementPlus自动引入icon
前端·javascript·vue.js
YFLICKERH2 小时前
【Python-Web后端开发框架】Flask | Django | FastAPI | Tornado 选型与 使用 | 特性
前端·python·flask
光影少年2 小时前
说说模块化规范?CommonJS和ES Module的区别?
前端·javascript·elasticsearch
telllong2 小时前
C++20 Modules:从入门到真香
java·前端·c++20
齐鲁大虾3 小时前
如何在HTML/JavaScript中禁用Ctrl+C
前端·javascript·html
qq_406176143 小时前
深入浅出 Vue 路由:从基础到进阶全解析
前端·javascript·vue.js