Vue3 + Axios + Ant Design Vue 请求封装详解教程(含 Token 鉴权、加密、下载)

发布于:2025-06-20 ⋅ 阅读:(17) ⋅ 点赞:(0)

Vue3 + Axios + Ant Design Vue 请求封装详解教程(含 Token 鉴权、加密、下载)


一、完整源码(请先阅读)

import { message, Modal } from 'ant-design-vue';
import axios from 'axios';
import { localRead } from '/@/utils/local-util';
import { useUserStore } from '/@/store/modules/system/user';
import { decryptData, encryptData } from './encrypt';
import { DATA_TYPE_ENUM } from '../constants/common-const';
import _ from 'lodash';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';

// token的消息头
const TOKEN_HEADER = 'Authorization';

// 创建axios对象
const smartAxios = axios.create({
  baseURL: import.meta.env.VITE_APP_API_URL,
});

// 退出系统
function logout() {
  useUserStore().logout();
  location.href = '/';
}

// ================================= 请求拦截器 =================================

smartAxios.interceptors.request.use(
  (config) => {
    // 在发送请求之前消息头加入token token
    const token = localRead(LocalStorageKeyConst.USER_TOKEN);
    if (token) {
      config.headers[TOKEN_HEADER] = 'Bearer ' + token;
    } else {
      delete config.headers[TOKEN_HEADER];
    }
    return config;
  },
  (error) => {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// ================================= 响应拦截器 =================================

// 添加响应拦截器
smartAxios.interceptors.response.use(
  (response) => {
    // 根据content-type ,判断是否为 json 数据
    let contentType = response.headers['content-type'] ? response.headers['content-type'] : response.headers['Content-Type'];
    if (contentType.indexOf('application/json') === -1) {
      return Promise.resolve(response);
    }

    // 如果是json数据
    if (response.data && response.data instanceof Blob) {
      return Promise.reject(response.data);
    }

    // 如果是加密数据
    if (response.data.dataType === DATA_TYPE_ENUM.ENCRYPT.value) {
      response.data.encryptData = response.data.data;
      let decryptStr = decryptData(response.data.data);
      if (decryptStr) {
        response.data.data = JSON.parse(decryptStr);
      }
    }

    const res = response.data;
    if (res.code && res.code !== 1) {
      // `token` 过期或者账号已在别处登录
      if (res.code === 30007 || res.code === 30008) {
        message.destroy();
        message.error('您没有登录,请重新登录');
        setTimeout(logout, 300);
        return Promise.reject(response);
      }

      // 等保安全的登录提醒
      if (res.code === 30010 || res.code === 30011) {
        Modal.error({
          title: '重要提醒',
          content: res.msg,
        });
        return Promise.reject(response);
      }

      // 长时间未操作系统,需要重新登录
      if (res.code === 30012) {
        Modal.error({
          title: '重要提醒',
          content: res.msg,
          onOk: logout,
        });
        setTimeout(logout, 3000);
        return Promise.reject(response);
      }
      message.destroy();
      message.error(res.msg);
      return Promise.reject(response);
    } else {
      return Promise.resolve(res);
    }
  },
  (error) => {
    // 对响应错误做点什么
    if (error.message.indexOf('timeout') !== -1) {
      message.destroy();
      message.error('网络超时');
    } else if (error.message === 'Network Error') {
      message.destroy();
      message.error('网络连接错误');
    } else if (error.message.indexOf('Request') !== -1) {
      message.destroy();
      message.error('网络发生错误');
    }
    return Promise.reject(error);
  }
);

// ================================= 对外提供请求方法:通用请求,get, post, 下载download等 =================================

/**
 * get请求
 */
export const getRequest = (url, params) => {
  return request({ url, method: 'get', params });
};

/**
 * 通用请求封装
 * @param config
 */
export const request = (config) => {
  return smartAxios.request(config);
};

/**
 * post请求
 */
export const postRequest = (url, data) => {
  return request({
    data,
    url,
    method: 'post',
  });
};

// ================================= 加密 =================================

/**
 * 加密请求参数的post请求
 */
export const postEncryptRequest = (url, data) => {
  return request({
    data: { encryptData: encryptData(data) },
    url,
    method: 'post',
  });
};

// ================================= 下载 =================================

export const postDownload = function (url, data) {
  request({
    method: 'post',
    url,
    data,
    responseType: 'blob',
  })
    .then((data) => {
      handleDownloadData(data);
    })
    .catch((error) => {
      handleDownloadError(error);
    });
};

/**
 * 文件下载
 */
export const getDownload = function (url, params) {
  request({
    method: 'get',
    url,
    params,
    responseType: 'blob',
  })
    .then((data) => {
      handleDownloadData(data);
    })
    .catch((error) => {
      handleDownloadError(error);
    });
};

function handleDownloadError(error) {
  if (error instanceof Blob) {
    const fileReader = new FileReader();
    fileReader.readAsText(error);
    fileReader.onload = () => {
      const msg = fileReader.result;
      const jsonMsg = JSON.parse(msg);
      message.destroy();
      message.error(jsonMsg.msg);
    };
  } else {
    message.destroy();
    message.error('网络发生错误', error);
  }
}

function handleDownloadData(response) {
  if (!response) {
    return;
  }

  // 获取返回类型
  let contentType = _.isUndefined(response.headers['content-type']) ? response.headers['Content-Type'] : response.headers['content-type'];

  // 构建下载数据
  let url = window.URL.createObjectURL(new Blob([response.data], { type: contentType }));
  let link = document.createElement('a');
  link.style.display = 'none';
  link.href = url;

  // 从消息头获取文件名
  let str = _.isUndefined(response.headers['content-disposition'])
    ? response.headers['Content-Disposition'].split(';')[1]
    : response.headers['content-disposition'].split(';')[1];

  let filename = _.isUndefined(str.split('fileName=')[1]) ? str.split('filename=')[1] : str.split('fileName=')[1];
  link.setAttribute('download', decodeURIComponent(filename));

  // 触发点击下载
  document.body.appendChild(link);
  link.click();

  // 下载完释放
  document.body.removeChild(link);
  window.URL.revokeObjectURL(url);
}

二、源码详解

1. 依赖与常量

import { message, Modal } from 'ant-design-vue'; // 用于弹出消息和模态框提示
import axios from 'axios'; // axios 请求库
import { localRead } from '/@/utils/local-util'; // 本地存储读取工具
import { useUserStore } from '/@/store/modules/system/user'; // 用户状态管理
import { decryptData, encryptData } from './encrypt'; // 加解密函数
import { DATA_TYPE_ENUM } from '../constants/common-const'; // 数据类型常量
import _ from 'lodash'; // 工具库
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js'; // 本地存储key常量

const TOKEN_HEADER = 'Authorization'; // 请求头携带token的键名

2. 创建 Axios 实例

const smartAxios = axios.create({
  baseURL: import.meta.env.VITE_APP_API_URL, // 基础请求地址从环境变量读取
});

3. 退出登录

function logout() {
  useUserStore().logout(); // 调用用户状态登出方法
  location.href = '/'; // 跳转到登录页或首页
}

4. 请求拦截器

smartAxios.interceptors.request.use(
  (config) => {
    const token = localRead(LocalStorageKeyConst.USER_TOKEN); // 从本地获取token
    if (token) {
      config.headers[TOKEN_HEADER] = 'Bearer ' + token; // 自动添加到请求头
    } else {
      delete config.headers[TOKEN_HEADER]; // 无token则删除
    }
    return config;
  },
  (error) => {
    return Promise.reject(error); // 请求错误直接抛出
  }
);

作用:每个请求发送前自动带上 token,方便后端身份校验。

5. 响应拦截器

smartAxios.interceptors.response.use(
  (response) => {
    // 判断响应内容是否是 JSON
    let contentType = response.headers['content-type'] || response.headers['Content-Type'];
    if (contentType.indexOf('application/json') === -1) {
      return Promise.resolve(response); // 非 JSON,直接返回原始响应
    }

    // 响应是 Blob 类型异常处理
    if (response.data && response.data instanceof Blob) {
      return Promise.reject(response.data);
    }

    // 解密处理
    if (response.data.dataType === DATA_TYPE_ENUM.ENCRYPT.value) {
      response.data.encryptData = response.data.data;
      let decryptStr = decryptData(response.data.data);
      if (decryptStr) {
        response.data.data = JSON.parse(decryptStr);
      }
    }

    const res = response.data;

    // 业务异常处理,code !== 1 表示失败
    if (res.code && res.code !== 1) {
      // 令牌失效或账号异地登录
      if (res.code === 30007 || res.code === 30008) {
        message.destroy();
        message.error('您没有登录,请重新登录');
        setTimeout(logout, 300);
        return Promise.reject(response);
      }

      // 安全登录提醒
      if (res.code === 30010 || res.code === 30011) {
        Modal.error({ title: '重要提醒', content: res.msg });
        return Promise.reject(response);
      }

      // 长时间未操作需重新登录
      if (res.code === 30012) {
        Modal.error({ title: '重要提醒', content: res.msg, onOk: logout });
        setTimeout(logout, 3000);
        return Promise.reject(response);
      }

      message.destroy();
      message.error(res.msg);
      return Promise.reject(response);
    } else {
      return Promise.resolve(res); // 成功返回业务数据
    }
  },
  (error) => {
    // 网络错误统一提示
    if (error.message.includes('timeout')) {
      message.destroy();
      message.error('网络超时');
    } else if (error.message === 'Network Error') {
      message.destroy();
      message.error('网络连接错误');
    } else if (error.message.includes('Request')) {
      message.destroy();
      message.error('网络发生错误');
    }
    return Promise.reject(error);
  }
);

作用:统一处理服务器响应,自动解密、鉴权失效跳转、提示错误信息。

6. 通用请求封装(GET,POST)

export const getRequest = (url, params) => {
  return request({ url, method: 'get', params });
};

export const postRequest = (url, data) => {
  return request({ url, method: 'post', data });
};

export const request = (config) => {
  return smartAxios.request(config);
};

统一通过 request 发起请求,方便未来统一处理或扩展。

7. 加密请求

export const postEncryptRequest = (url, data) => {
  return request({
    url,
    method: 'post',
    data: { encryptData: encryptData(data) }, // 发送加密后的数据
  });
};

对敏感参数统一加密,保证传输安全。

8. 文件下载封装

export const postDownload = (url, data) => {
  request({
    url,
    method: 'post',
    data,
    responseType: 'blob',
  })
    .then(handleDownloadData)
    .catch(handleDownloadError);
};

export const getDownload = (url, params) => {
  request({
    url,
    method: 'get',
    params,
    responseType: 'blob',
  })
    .then(handleDownloadData)
    .catch(handleDownloadError);
};

支持 GET/POST 下载,返回二进制流。

9. 下载结果与错误处理

function handleDownloadError(error) {
  if (error instanceof Blob) {
    const fileReader = new FileReader();
    fileReader.readAsText(error);
    fileReader.onload = () => {
      const msg = fileReader.result;
      const jsonMsg = JSON.parse(msg);
      message.destroy();
      message.error(jsonMsg.msg);
    };
  } else {
    message.destroy();
    message.error('网络发生错误', error);
  }
}

function handleDownloadData(response) {
  if (!response) return;

  let contentType = _.isUndefined(response.headers['content-type']) ? response.headers['Content-Type'] : response.headers['content-type'];
  let url = window.URL.createObjectURL(new Blob([response.data], { type: contentType }));
  let link = document.createElement('a');
  link.style.display = 'none';
  link.href = url;

  let str = _.isUndefined(response.headers['content-disposition'])
    ? response.headers['Content-Disposition'].split(';')[1]
    : response.headers['content-disposition'].split(';')[1];

  let filename = _.isUndefined(str.split('fileName=')[1]) ? str.split('filename=')[1] : str.split('fileName=')[1];
  link.setAttribute('download', decodeURIComponent(filename));

  document.body.appendChild(link);
  link.click();

  document.body.removeChild(link);
  window.URL.revokeObjectURL(url);
}

处理下载文件流,自动从响应头解析文件名并触发浏览器下载。错误时尝试读取 Blob 里的错误信息并提示。


三、总结

  • 封装 axios 实例方便集中管理基础地址、拦截器、错误处理。
  • 请求拦截器自动附带 token,保证鉴权。
  • 响应拦截器统一处理异常、token失效自动登出、数据加密解密。
  • 请求方法封装标准的 getRequestpostRequest 和加密 postEncryptRequest
  • 文件下载封装支持流下载及错误提示,提升用户体验。
  • 使用此封装,业务代码只需专注调用接口,提升开发效率和代码规范。

网站公告

今日签到

点亮在社区的每一天
去签到