axios统一封装的思路

1. axios 统一封装的整体结构设计及优势

核心结构与模块职责

axios 封装通常采用 "分层解耦" 设计,核心模块及职责如下:

  • 基础配置模块:定义全局默认配置,如baseURL(区分环境)、timeout(默认 5000ms)、responseType: 'json',以及请求头默认值(如Content-Type: application/json),通过axios.create()创建实例,避免污染全局 axios 对象。
  • 拦截器模块:包含请求拦截器和响应拦截器,集中处理通用逻辑(如参数添加、错误捕获),独立于业务请求,便于统一维护。
  • 工具函数模块:封装辅助方法,如getToken()(从 localStorage/cookie 获取 token)、formatParams()(处理 GET 请求参数拼接、POST 请求参数序列化)、handleBlob()(文件流处理),避免重复代码。
  • 请求方法封装模块:对外暴露get、post、download等方法,接收 "业务参数 + 自定义配置",内部整合基础配置与拦截器,简化业务方调用。
  • 错误处理模块:统一管理请求错误(网络错误、超时、后端错误码),提供错误提示、重试、跳转等标准化处理方案。

结构优势

  • 扩展性:新增功能(如请求缓存)时,只需在对应模块(如工具函数 / 拦截器)添加逻辑,无需修改所有业务请求;切换环境(如测试→生产)时,仅需修改基础配置的baseURL。
  • 维护性:通用逻辑(如 token 携带)集中在拦截器,若后端调整参数名(如token→authToken),仅需修改拦截器中 1 处代码,无需逐个修改业务接口。
  • 易用性:业务方调用时无需关注baseURL、拦截器等细节,只需传入 "接口路径 + 参数",降低使用成本(如request.get('/user/list', { page: 1 }))。

2. 两类公用参数的实现方式及特殊场景处理

(1)固定公用参数(如appId、token):请求拦截器全局添加

实现逻辑:在请求拦截器中,对所有请求的params(GET)或data(POST)添加固定参数,同时处理token过期的特殊情况(如 token 不存在时跳转登录)。

javascript 复制代码
// 请求拦截器示例
axiosInstance.interceptors.request.use(
  (config) => {
    const token = getToken(); // 自定义工具函数:从localStorage获取token
    // 1. 处理GET参数(params)
    config.params = {
      appId: 'com.xxx.frontend', // 固定参数
      token: token || '', // token存在则携带,否则传空
      ...config.params, // 业务参数覆盖默认(若有同名参数)
    };
    // 2. 处理POST参数(data)
    if (config.method === 'post' && config.data) {
      config.data = {
        appId: 'com.xxx.frontend',
        token: token || '',
        ...config.data,
      };
    }
    return config;
  },
  (error) => Promise.reject(error)
);

适用场景:所有接口必须携带的参数,且参数值相对固定(如appId固定,token仅随用户登录状态变化)。

(2)动态公用参数(如分页pageSize):请求方法默认值 + 业务方可选覆盖

实现逻辑:在封装的get/post方法中,为参数设置默认值,业务方若需修改可手动传入,无需全局强制添加。

csharp 复制代码
// 封装get方法示例
export const get = (url, params = {}, config = {}) => {
  // 动态公用参数:分页默认pageSize=10,page=1
  const defaultPageParams = {
    pageSize: 10,
    page: 1,
  };
  // 合并默认分页参数与业务参数(业务参数优先级更高)
  const finalParams = { ...defaultPageParams, ...params };
  return axiosInstance.get(url, {
    params: finalParams,
    ...config, // 自定义配置(如禁用loading)
  });
};

适用场景:部分接口(如列表查询)需要的通用参数,且参数值可能随业务场景变化(如 A 列表pageSize=20,B 列表pageSize=10)。

(3)排除特定公用参数的处理:自定义配置标记 + 拦截器过滤

业务方在请求时通过config传入excludeParams(需排除的参数名数组),拦截器中根据该标记过滤参数:

javascript 复制代码
// 业务方调用示例(排除token参数)
get('/user/info', { id: 123 }, { excludeParams: ['token'] });
// 拦截器中添加过滤逻辑
axiosInstance.interceptors.request.use(
  (config) => {
    const { excludeParams = [] } = config;
    // 过滤GET参数
    config.params = Object.fromEntries(
      Object.entries({
        appId: 'com.xxx.frontend',
        token: getToken() || '',
        ...config.params,
      }).filter(([key]) => !excludeParams.includes(key))
    );
    // 过滤POST参数逻辑类似...
    return config;
  },
  (error) => Promise.reject(error)
);

3. 请求 / 响应拦截器的常见逻辑与实现

(1)请求拦截器:除公用参数外的核心逻辑

  • 请求头设置:根据请求类型(如文件上传)动态修改Content-Type,避免手动配置错误。
arduino 复制代码
if (config.headers['Content-Type'] === 'multipart/form-data') {
  // 文件上传:无需序列化,axios自动处理FormData
  delete config.headers['Content-Type']; // 清除默认json头,避免冲突
} else if (config.method === 'post' && !config.headers['Content-Type']) {
  // 默认POST请求:设置json头
  config.headers['Content-Type'] = 'application/json';
}
  • token 过期前刷新:通过tokenExpireTime(登录时后端返回)判断 token 剩余时间,若 < 5 分钟则发起刷新请求,避免请求因 token 过期失败。
ini 复制代码
const tokenExpireTime = localStorage.getItem('tokenExpireTime');
const now = Date.now();
// 若token存在且即将过期,且无正在进行的刷新请求
if (token && tokenExpireTime - now < 5 * 60 * 1000 && !isRefreshing) {
  isRefreshing = true;
  // 发起刷新token请求
  refreshToken().then((newToken) => {
    localStorage.setItem('token', newToken);
    isRefreshing = false;
  }).catch(() => {
    // 刷新失败:跳转登录
    router.push('/login');
    isRefreshing = false;
  });
}
  • 请求参数格式转换:处理 GET 请求数组参数(如ids=[1,2]→ids=1&ids=2),避免后端解析错误。
javascript 复制代码
// 工具函数:格式化GET数组参数
const formatGetParams = (params) => {
  return Object.fromEntries(
    Object.entries(params).flatMap(([key, value]) => {
      if (Array.isArray(value)) {
        return value.map((v) => [key, v]); // 数组参数拆分为多个key-value
      }
      return [[key, value]];
    })
  );
};
// 拦截器中调用
config.params = formatGetParams(config.params);

(2)响应拦截器:统一处理响应结果与错误

  • 正常响应处理:提取后端返回的data(通常后端响应格式为{ code: 200, data: {}, message: '' }),直接返回data给业务方,简化调用。
javascript 复制代码
axiosInstance.interceptors.response.use(
  (response) => {
    const { code, data, message } = response.data;
    // 后端自定义成功码(如200)
    if (code === 200) {
      return data; // 业务方直接获取data,无需处理外层结构
    } else {
      // 非成功码:触发错误处理(如提示"参数错误")
      ElMessage.error(message || '请求失败');
      return Promise.reject(new Error(message || '响应错误'));
    }
  },
  (error) => {
    // 网络错误/超时处理
    if (!error.response) {
      if (error.message.includes('timeout')) {
        ElMessage.error('请求超时,请稍后重试');
      } else {
        ElMessage.error('网络异常,请检查网络连接');
      }
      return Promise.reject(error);
    }
    // 后端返回错误码处理
    const { status, data } = error.response;
    switch (status) {
      case 401:
        // 未授权:清除token并跳转登录
        localStorage.removeItem('token');
        router.push('/login?redirect=' + window.location.pathname);
        ElMessage.error(data.message || '登录已过期,请重新登录');
        break;
      case 403:
        // 权限不足:提示并返回上一页
        ElMessage.error('您无此操作权限');
        router.go(-1);
        break;
      case 500:
        // 服务器错误:提示并上报错误日志
        ElMessage.error('服务器内部错误,请联系管理员');
        reportError(error); // 自定义日志上报函数
        break;
      default:
        ElMessage.error(data.message || `请求错误(${status})`);
    }
    return Promise.reject(error);
  }
);

4. 全局 loading 自动化配置逻辑

(1)判断请求状态:计数器 + 请求状态管理

通过 "请求计数器" 记录当前活跃请求数,计数器 > 0 时显示 loading,=0 时隐藏 loading,避免单个请求的 loading 闪烁:

ini 复制代码
let requestCount = 0; // 请求计数器
const showLoading = () => {
  if (requestCount === 0) {
    // 使用Element Plus的Loading组件,全局遮罩
    ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.5)',
    });
  }
  requestCount++;
};
const hideLoading = () => {
  requestCount--;
  if (requestCount === 0) {
    // 关闭loading(需确保获取到Loading实例)
    const loadingInstance = ElLoading.service();
    loadingInstance.close();
  }
};
  • 调用时机:请求拦截器中调用showLoading(),响应拦截器(成功 / 失败)中调用hideLoading(),确保每个请求对应 "显示→隐藏" 的完整流程。

(2)单个请求禁用 loading:自定义配置标记

封装请求方法时,允许业务方通过config传入showLoading: false,拦截器中根据该标记决定是否执行 loading 逻辑:

javascript 复制代码
// 1. 请求拦截器中添加判断
axiosInstance.interceptors.request.use(
  (config) => {
    // 若未禁用loading,则显示
    if (config.showLoading !== false) {
      showLoading();
    }
    // ...其他逻辑
    return config;
  },
  (error) => {
    // 请求发送失败时,若未禁用loading,需隐藏
    if (error.config?.showLoading !== false) {
      hideLoading();
    }
    return Promise.reject(error);
  }
);
// 2. 业务方调用示例(下拉刷新请求禁用loading)
get('/user/list', { page: 2 }, { showLoading: false });

(3)多请求同步 loading:依赖计数器机制

由于使用requestCount计数器,多个请求同时发起时,每个请求会使计数器 + 1,只有所有请求完成(计数器归 0)才会隐藏 loading,天然避免 "先隐藏再显示":

  • 例:同时发起 A、B 两个请求→requestCount从 0→1→2(显示 loading)→A 完成→1→B 完成→0(隐藏 loading)。

5. 统一文件下载处理(文件流 + 不同类型文件)

(1)axios 请求配置:确保正确接收文件流

  • 设置responseType: 'blob':告知 axios 将响应数据解析为 Blob 对象(文件流),而非默认的 JSON,避免文件流被错误解析导致乱码。
  • 设置responseEncoding: 'utf-8'(可选):确保中文文件名正常解析。
javascript 复制代码
// 封装文件下载方法
export const download = (url, params = {}, config = {}) => {
  return axiosInstance.get(url, {
    params,
    responseType: 'blob', // 核心配置:接收文件流
    responseEncoding: 'utf-8',
    ...config,
  }).then((response) => {
    handleDownloadResponse(response); // 处理文件流
  });
};

(2)文件流处理:区分下载与预览

  • 核心逻辑:从响应头content-disposition中提取文件名,通过URL.createObjectURL(blob)创建临时 URL,结合a标签(下载)或iframe(预览)实现功能。
  • 处理中文文件名乱码:通过decodeURIComponent()解码头信息中的文件名(后端需将文件名编码为 UTF-8)。
javascript 复制代码
// 处理下载响应(通用方法)
const handleDownloadResponse = (response) => {
  const blob = response.data;
  const headers = response.headers;
  // 1. 提取文件名(从content-disposition头)
  const disposition = headers['content-disposition'] || '';
  const filenameMatch = disposition.match(/filename=([^;]+)/i);
  // 解码中文文件名(避免乱码)
  const filename = filenameMatch 
    ? decodeURIComponent(filenameMatch[1].replace(/"/g, '')) 
    : 'default_file';
  // 2. 判断文件类型:预览(PDF)或下载(Excel/Word等)
  const blobType = blob.type;
  const url = URL.createObjectURL(blob); // 创建临时URL
  if (blobType.includes('pdf')) {
    // PDF预览:使用iframe
    const iframe = document.createElement('iframe');
    iframe.src = url;
    iframe.style.width = '100%';
    iframe.style.height = '800px';
    iframe.style.border = 'none';
    // 替换页面中预览容器的内容
    const previewContainer = document.getElementById('pdf-preview');
    if (previewContainer) {
      previewContainer.innerHTML = '';
      previewContainer.appendChild(iframe);
    }
  } else {
    // 其他文件:下载(a标签)
    const a = document.createElement('a');
    a.href = url;
    a.download = filename; // 设置下载文件名
    // 触发点击(解决部分浏览器不支持直接点击)
    document.body.appendChild(a);
    a.click();
    // 清理临时资源
    document.body.removeChild(a);
    URL.revokeObjectURL(url); // 释放URL对象,避免内存泄漏
  }
};

6. 统一错误处理与业务方自定义错误逻辑

(1)请求过程中的常见错误类型及统一处理

错误类型 触发场景 统一处理逻辑
网络错误 断网、服务器宕机 提示 "网络异常,请检查连接",不上报日志(用户环境问题)
请求超时 timeout时间内未收到响应(如默认 5000ms) 提示 "请求超时,请稍后重试",可配置重试 1 次(偶发网络波动)
401 未授权 token 过期、未登录 清除 token→跳转登录页(带当前页面地址,登录后返回)→提示 "登录已过期"
403 权限不足 无接口访问权限(如普通用户调用管理员接口) 提示 "无操作权限"→返回上一页,不上报日志(业务权限问题)
404 接口不存在 接口路径错误、环境配置错误 提示 "请求资源不存在"→上报日志(开发配置问题)
500/504 服务器错误 后端代码异常、网关超时 提示 "服务器内部错误,请联系管理员"→上报详细日志(后端问题)
后端自定义错误(如 400) 参数错误、业务逻辑错误(如 "手机号已存在") 提示后端返回的message(如 "手机号已存在"),不上报日志(业务场景错误)

统一处理代码 :参考 "响应拦截器" 部分的错误处理逻辑,通过switch (status)和 code

(2)业务方自定义错误处理逻辑:支持catch捕获与配置回调

全局错误处理覆盖通用场景,但部分业务需自定义逻辑(如登录失败后不跳转,而是显示验证码),可通过两种方式实现:

  • 方式 1:业务方在请求 catch 中重写处理

由于拦截器中已将错误Promise.reject,业务方可直接在catch中捕获错误,覆盖全局逻辑:

typescript 复制代码
// 业务方调用示例:自定义401错误处理(不跳转登录,显示弹窗)
login({ username: 'test', pwd: '123' })
  .then((res) => { /* 成功逻辑 */ })
  .catch((error) => {
    const status = error.response?.status;
    if (status === 401) {
      // 自定义处理:显示验证码弹窗,而非跳转登录
      showVerifyCodeModal();
    } else {
      // 非401错误:沿用全局处理(如提示网络错误)
      ElMessage.error(error.message || '请求失败');
    }
  });
  • 方式 2:封装时提供 errorCallback 配置

在请求方法中增加errorCallback参数,允许业务方传入自定义回调,拦截器中优先执行回调:

typescript 复制代码
// 1. 封装post方法:增加errorCallback参数
export const post = (url, data = {}, config = {}) => {
  const { errorCallback, ...restConfig } = config;
  return axiosInstance.post(url, data, restConfig)
    .catch((error) => {
      // 若业务方传入回调,优先执行
      if (typeof errorCallback === 'function') {
        return errorCallback(error);
      }
      // 无回调:沿用全局错误处理
      return Promise.reject(error);
    });
};
// 2. 业务方调用:传入errorCallback
post(
  '/login',
  { username: 'test', pwd: '123' },
  {
    errorCallback: (error) => {
      if (error.response?.status === 401) {
        showVerifyCodeModal(); // 自定义处理
      }
    }
  }
);

7. 多环境baseURL动态切换与代理配置

(1)baseURL动态切换方案:环境变量 + 配置文件

通过 "环境变量区分环境"+"配置文件存储baseURL" 实现切换,避免硬编码修改,常见方案如下:

  • 步骤 1:创建环境配置文件

在项目src/config目录下创建多环境配置文件,存储不同环境的baseURL:

arduino 复制代码
// src/config/env.js
export default {
  // 开发环境(本地)
  development: {
    baseURL: '/api' // 配合代理,避免跨域
  },
  // 测试环境(测试服)
  test: {
    baseURL: 'https://test.xxx.com/api'
  },
  // 生产环境(正式服)
  production: {
    baseURL: 'https://prod.xxx.com/api'
  }
};
  • 步骤 2:通过环境变量判断当前环境

利用构建工具(如 Vite、Webpack)的环境变量,自动识别当前环境(如NODE_ENV):

javascript 复制代码
// src/utils/request.js(axios封装文件)
import envConfig from '@/config/env';
// 1. 获取当前环境(Vite中用import.meta.env.MODE,Webpack中用process.env.NODE_ENV)
const currentEnv = import.meta.env.MODE || 'development';
// 2. 创建axios实例:动态设置baseURL
const axiosInstance = axios.create({
  baseURL: envConfig[currentEnv].baseURL,
  timeout: 5000
});
  • 步骤 3:手动切换环境(可选)

若需支持用户手动切换环境(如测试人员切换测试 / 预发布环境),可通过 "本地存储 + 重新初始化实例" 实现:

ini 复制代码
// 1. 手动切换环境的函数
export const switchEnv = (targetEnv) => {
  // 存储环境到localStorage
  localStorage.setItem('currentEnv', targetEnv);
  // 重新初始化axios实例:更新baseURL
  axiosInstance.defaults.baseURL = envConfig[targetEnv].baseURL;
};
// 2. 初始化时优先读取localStorage中的环境
const initAxios = () => {
  const savedEnv = localStorage.getItem('currentEnv') || currentEnv;
  axiosInstance.defaults.baseURL = envConfig[savedEnv].baseURL;
};
initAxios(); // 项目启动时初始化

(2)开发环境代理配置:避免跨域

开发环境中,baseURL设为/api,通过构建工具配置代理,将/api前缀的请求转发到真实后端地址,解决跨域问题:

  • Vite 代理配置(vite.config.js)
javascript 复制代码
export default defineConfig({
  server: {
    proxy: {
      // 匹配所有以/api开头的请求
      '/api': {
        target: 'https://dev.xxx.com', // 后端开发环境地址
        changeOrigin: true, // 开启跨域(修改请求头中的Origin)
        rewrite: (path) => path.replace(/^/api/, '') // 移除/api前缀(若后端无该前缀)
      }
    }
  }
});
  • Webpack 代理配置(vue.config.js,Vue2 项目)
css 复制代码
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://dev.xxx.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
};

(3)代理配置注意事项

  1. changeOrigin: true 必须开启 :若不开启,后端会收到Origin: localhost:3000(前端地址),可能因跨域策略拒绝请求;开启后,Origin会改为target地址(如dev.xxx.com)。
  1. rewrite/pathRewrite 适配后端前缀:若后端接口无/api前缀(如真实接口是/login,而非/api/login),需通过rewrite移除前缀,避免 404。
  1. 避免代理冲突:若项目中存在本地 Mock 服务(如/mock前缀),需确保代理规则不覆盖 Mock 请求(如仅代理/api前缀)。

8. 请求取消功能实现(CancelToken/AbortController)

axios 支持两种取消请求方式:CancelToken(旧版,axios < 0.22.0)和AbortController(新版,axios ≥ 0.22.0),以下以新版AbortController为例,实现请求取消与管理。

(1)核心实现:封装取消令牌管理

通过 "Map 存储取消控制器"+"请求标识关联控制器",实现单个 / 批量取消请求:

  • 步骤 1:创建取消令牌管理工具
dart 复制代码
// src/utils/requestCancel.js
// 存储取消控制器:key=请求标识(如url+method),value=AbortController实例
const controllerMap = new Map();
// 1. 创建取消控制器并存储
export const createCancelController = (requestKey) => {
  // 若已有相同请求的控制器,先取消旧请求(避免重复请求)
  if (controllerMap.has(requestKey)) {
    cancelRequest(requestKey);
  }
  const controller = new AbortController();
  controllerMap.set(requestKey, controller);
  return controller.signal; // 返回signal,传入axios请求配置
};
// 2. 取消指定请求
export const cancelRequest = (requestKey) => {
  const controller = controllerMap.get(requestKey);
  if (controller) {
    controller.abort(); // 触发取消
    controllerMap.delete(requestKey); // 移除控制器
  }
};
// 3. 取消所有未完成的请求(如页面跳转时)
export const cancelAllRequests = () => {
  controllerMap.forEach((controller) => controller.abort());
  controllerMap.clear();
};
  • 步骤 2:集成到 axios 封装中

在请求方法中生成requestKey(如url+method),创建signal并传入 axios 配置:

javascript 复制代码
// src/utils/request.js
import { createCancelController, cancelRequest } from './requestCancel';
export const get = (url, params = {}, config = {}) => {
  // 生成请求标识:url + 'GET'(避免GET/POST同url冲突)
  const requestKey = `${url}-GET`;
  // 创建signal,传入axios配置
  const signal = createCancelController(requestKey);
  // 合并配置:添加signal
  const finalConfig = { signal, ...config };
  return axiosInstance.get(url, { params, ...finalConfig })
    .finally(() => {
      // 请求完成(成功/失败):移除控制器
      cancelRequest(requestKey);
    });
};

(2)业务方触发取消请求

  • 场景 1:取消单个请求(如快速点击按钮)

业务方需存储requestKey,调用cancelRequest取消:

ini 复制代码
// 业务组件中:取消"获取用户列表"请求
const cancelUserListRequest = () => {
  const requestKey = '/user/list-GET'; // 与封装时的requestKey一致
  cancelRequest(requestKey);
};
// 点击"取消"按钮触发
<button @click="cancelUserListRequest">取消加载</button>
  • 场景 2:页面跳转时取消所有请求

在路由守卫中调用cancelAllRequests,避免页面跳转后请求仍占用资源:

javascript 复制代码
// src/router/index.js
import { cancelAllRequests } from '@/utils/requestCancel';
router.beforeEach((to, from, next) => {
  // 离开当前页面时,取消所有未完成的请求
  cancelAllRequests();
  next();
});

(3)注意事项

  • AbortController 兼容性:axios ≥ 0.22.0 才支持,若项目使用旧版 axios,需改用CancelToken(逻辑类似,替换为CancelToken.source())。
  • 避免重复取消:请求完成后需调用cancelRequest移除控制器,避免重复调用abort()导致错误。
  • 错误捕获:取消请求会触发Cancel错误,需在拦截器中过滤,避免错误提示:
javascript 复制代码
// 响应拦截器中过滤取消错误
axiosInstance.interceptors.response.use(
  (res) => res,
  (error) => {
    // 取消请求的错误:不提示,直接返回
    if (axios.isCancel(error)) {
      console.log('请求已取消', error.message);
      return Promise.reject(new Error('请求已取消'));
    }
    // 其他错误:正常处理
    return Promise.reject(error);
  }
);

9. 请求函数参数设计与配置优先级

(1)参数设计:兼顾易用性与灵活性

封装的请求函数(get/post/download)需支持 "基础参数 + 自定义配置",参数结构设计如下(以post为例):

typescript 复制代码
/**
 * POST请求封装
 * @param {string} url - 接口路径(如'/login')
 * @param {Object} data - 请求体参数(POST/PUT等)
 * @param {Object} config - 自定义配置(覆盖全局配置)
 * @param {number} config.timeout - 超时时间(覆盖全局5000ms)
 * @param {Object} config.headers - 请求头(覆盖全局headers)
 * @param {boolean} config.showLoading - 是否显示loading(默认true)
 * @param {Function} config.errorCallback - 自定义错误回调
 * @returns {Promise} - 请求结果Promise
 */
export const post = (url, data = {}, config = {}) => {
  // 解构自定义配置,默认值与全局配置对齐
  const {
    timeout = axiosInstance.defaults.timeout, // 全局默认超时5000ms
    headers = {},
    showLoading = true,
    errorCallback,
    ...restConfig
  } = config;
  // 合并配置:自定义配置覆盖全局
  const finalConfig = {
    timeout,
    headers: { ...axiosInstance.defaults.headers, ...headers }, //  headers合并
    showLoading,
    errorCallback,
    ...restConfig
  };
  return axiosInstance.post(url, data, finalConfig)
    .catch((error) => {
      if (errorCallback) return errorCallback(error);
      return Promise.reject(error);
    });
};

(2)配置优先级:业务自定义 > 全局默认

配置优先级从高到低为:业务方传入的自定义配置全局默认配置axios 默认配置,具体规则如下:

  • timeout:业务方传入config.timeout=10000 → 覆盖全局timeout=5000 → 覆盖 axios 默认timeout=0(无超时)。
  • headers:采用 "浅合并",业务方传入的headers字段覆盖全局,未传入字段沿用全局:
css 复制代码
// 全局headers:{ 'Content-Type': 'application/json', 'appId': '123' }
// 业务方headers:{ 'Content-Type': 'multipart/form-data' }
// 合并后:{ 'Content-Type': 'multipart/form-data', 'appId': '123' }
  • showLoading:业务方传入showLoading=false → 覆盖全局默认true,不显示 loading。
  • responseType:文件下载时传入responseType: 'blob' → 覆盖全局responseType: 'json'。

(3)业务方调用示例:灵活配置

go 复制代码
// 示例1:自定义超时+禁用loading
post(
  '/upload/file',
  formData, // FormData格式(文件上传)
  {
    timeout: 30000, // 上传文件超时30s
    showLoading: false, // 禁用loading
    headers: { 'Content-Type': 'multipart/form-data' } // 覆盖Content-Type
  }
);
// 示例2:自定义错误回调+修改baseURL
get(
  '/user/info',
  { id: 123 },
  {
    baseURL: 'https://special.xxx.com/api', // 临时使用特殊baseURL
    errorCallback: (error) => {
      if (error.response?.status === 404) {
        ElMessage.error('用户不存在');
      }
    }
  }
);

10. 性能优化与用户体验增强功能(以 "请求重试" 为例)

(1)优化点选择:请求重试机制

针对 "偶发网络波动导致的请求失败"(如 502 网关错误、超时),自动重试 1-2 次,避免用户手动刷新,提升体验。

(2)实现思路

  • 核心逻辑:在响应拦截器中判断错误类型(仅重试偶发错误),记录重试次数,达到最大次数后停止重试,返回错误。
  • 关键配置:全局设置 "最大重试次数(如 2 次)" 和 "重试间隔(如 1000ms,避免频繁重试压垮服务器)"。

(3)核心代码实现

javascript 复制代码
// src/utils/request.js
// 全局重试配置
const retryConfig = {
  maxRetryCount: 2, // 最大重试次数
  retryDelay: 1000, // 重试间隔(ms)
};
// 封装重试函数
const retryRequest = async (config, retryCount = 0) => {
  try {
    // 克隆请求配置:避免修改原配置
    const cloneConfig = { ...config };
    // 增加重试次数标识
    cloneConfig._retryCount = retryCount;
    // 发起重试请求
    return await axiosInstance(cloneConfig);
  } catch (error) {
    // 重试次数达到上限:返回错误
    if (retryCount >= retryConfig.maxRetryCount) {
      return Promise.reject(error);
    }
    // 未达上限:延迟后重试(递归调用)
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(retryRequest(config, retryCount + 1));
      }, retryConfig.retryDelay);
    });
  }
};
// 响应拦截器中集成重试逻辑
axiosInstance.interceptors.response.use ((response) => response,
  async (error) => {
    // 1. 过滤无需重试的错误(取消请求、401 未授权、403 权限不足等)
    const shouldRetry = !axios.isCancel (error) &&
    error.response?.status !== 401 &&
    error.response?.status !== 403 &&
    error.response?.status !== 404 &&
    (error.response?.status >= 500 || error.message.includes ('timeout'));
    // 2. 若无需重试:直接返回错误
    if (!shouldRetry) {
      return Promise.reject (error);
    }
    // 3. 若需重试:获取原请求配置,调用重试函数
    const originalConfig = error.config;
    // 避免重复添加_retryCount(首次重试时无该字段)
    const currentRetryCount = originalConfig._retryCount || 0;
    // 4. 判断是否达到最大重试次数
    if (currentRetryCount>= retryConfig.maxRetryCount) {
      ElMessage.error (请求失败(已重试${currentRetryCount}次),请稍后再试);
      return Promise.reject(error);
    }
    // 5. 发起重试
    console.log (请求重试中(第${currentRetryCount + 1}次):${originalConfig.url});
    return retryRequest(originalConfig, currentRetryCount);​
  });

(4)请求重试的收益与注意事项

  • 核心收益
  1. 提升用户体验:偶发的502网关错误、网络波动导致的超时,无需用户手动刷新页面,自动重试即可恢复,减少用户操作成本。

  2. 降低错误率:针对非业务性错误(如服务器临时过载),重试机制可有效降低最终的请求失败率,提升系统稳定性。

  • 注意事项
  1. 明确重试范围:仅对"服务器端偶发错误"(500+状态码、超时)重试,避免对"客户端错误"(401/403/404)重试,否则会无效请求浪费资源。

  2. 设置重试间隔 :通过retryDelay=1000ms避免短时间内频繁重试,防止加重服务器负担(如服务器过载时,频繁重试会加剧问题)。

  3. 避免幂等性问题:对"非幂等请求"(如POST提交表单、创建订单),需谨慎使用重试,避免重复创建数据(可通过后端接口幂等设计解决,如添加唯一请求ID)。

  4. 业务方自定义控制 :可在请求配置中增加enableRetry: false,允许业务方禁用特定请求的重试(如支付接口,避免重复扣款):

javascript 复制代码
// 业务方调用:禁用重试(支付接口)
post('/order/pay', { orderId: 123 }, { enableRetry: false });
// 响应拦截器中增加判断:若disableRetry则不重试
const shouldRetry = !originalConfig.enableRetry === false && ...;
相关推荐
聪明的笨猪猪3 小时前
Java Spring “IOC + DI”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
笔尖的记忆3 小时前
【前端架构和框架】react组件化&数据流
前端·面试
小小前端_我自坚强4 小时前
UniApp 微信小程序开发使用心得
面试·微信小程序·代码规范
南北是北北4 小时前
采用ExoPlayer播放器,进行播放器池的重用,如何防止解码器不重用?
面试
渣哥5 小时前
从配置文件到 SpEL 表达式:@Value 在 Spring 中到底能做什么?
javascript·后端·面试
顾林海5 小时前
Android编译插桩黑科技:ReDex带你给App"瘦个身,提个速"
android·面试·性能优化
渣哥9 小时前
不加 @Primary?Spring 自动装配时可能直接报错!
javascript·后端·面试
知其然亦知其所以然10 小时前
MySQL性能暴涨100倍?其实只差一个“垂直分区”!
后端·mysql·面试
Q741_14710 小时前
C++ 位运算 高频面试考点 力扣 面试题 17.19. 消失的两个数字 题解 每日一题
c++·算法·leetcode·面试·位运算