从零开始使用 Webpack 搭建 Vue 3 脚手架工程(四)

Vue 3 项目中的 HTTP 请求管理:封装 Axios 实现全局 HTTP 方法

准备工作:下载axios依赖,在项目src目录创建utils文件夹,创建http.js, 目录结构:src/utils/http.js

npm install axios
1. 创建 Axios 实例
什么是 Axios 实例?

Axios 是一个基于 Promise 的 HTTP 库,它允许我们发送请求并接收响应。在封装 Axios 时,我们首先需要创建一个 Axios 实例,便于对所有请求应用统一配置。

代码解析
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router'; // 根据实际项目调整路径

/**
 * 创建axios实例
 */
const http = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || '/', // 配置基础路径
  timeout: 10000, // 超时时间
  headers: {
    'Content-Type': 'application/json', // 默认的请求内容类型
  },
});

解释
  • baseURL:是请求的基础 URL。process.env.VUE_APP_BASE_API 是我们项目中通过 .env 文件设置的环境变量,默认值是 ‘/’。它指的是请求的基础路径。

  • timeout:设置请求的超时时间。这里设置为 10 秒,意味着如果请求在 10 秒内没有响应,将会中断该请求并抛出错误。

  • headers:设置请求头,这里我们默认使用 ‘application/json’ 类型,告诉服务器请求体的数据格式是 JSON。

通过这种方式,我们能够在项目中统一配置 Axios 请求的基础参数,减少代码重复。

2. 请求拦截器
什么是请求拦截器?

请求拦截器是 Axios 提供的一种功能,它允许我们在请求发送前对请求进行修改或者添加一些额外的逻辑。通常在请求拦截器中我们可以进行身份验证、设置请求头、取消请求等操作。

代码解析
/**
 * 请求拦截器
 */
http.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    const isLoginRequest = config.url.includes('/login');

    // 如果没有 Token 且不是登录请求,跳转到登录页
    if (!token && !isLoginRequest) {
      ElMessage.error('登录已过期,请重新登录');
      router.push('/login');
      return Promise.reject(new Error('未授权,请登录'));
    }

    // 如果有 Token,添加到请求头
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }

    // 添加取消请求功能
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;

    cancelTokens.set(config.url, source); // 存储取消令牌

    return config;
  },
  (error) => {
    ElMessage.error('请求发送失败');
    return Promise.reject(error);
  }
);

解释
  • Token 验证:在每个请求中,我们从 localStorage 中获取 Token,若 Token 不存在且请求的 URL 不包含 /login,则说明用户未登录,跳转到登录页并返回一个拒绝的 Promise。

  • 设置请求头:如果存在 Token,则在请求头中添加 Authorization 字段,这样后端就能识别用户身份,进行权限验证。

  • 取消请求:使用 Axios 提供的 CancelToken 来为每个请求添加取消功能,防止多个相同的请求并发执行,特别是在路由切换时取消未完成的请求。

通过请求拦截器,我们能够统一处理请求中的 Token 校验和请求取消,减少重复代码并提高系统的可维护性。

3. 响应拦截器
什么是响应拦截器?

响应拦截器是 Axios 提供的另一个功能,它允许我们在收到响应后对响应数据进行统一处理。响应拦截器主要用于检查响应状态、处理错误信息等。

代码解析
/**
 * 响应拦截器
 */
http.interceptors.response.use(
  (response) => {
    const { data } = response;

    // 检查业务状态码(根据后端实际约定调整)
    if (data.code !== 200) {
      ElMessage.error(data.message || '未知错误');
      return Promise.reject(new Error(data.message || '未知错误'));
    }

    return data;
  },
  (error) => {
    if (axios.isCancel(error)) {
      console.warn('请求被取消:', error.message);
      return Promise.reject(error);
    }

    if (error.response) {
      const { status, data } = error.response;

      switch (status) {
        case 401:
          ElMessage.error('未授权或登录过期,请重新登录');
          router.push('/login');
          break;
        case 403:
          ElMessage.error('没有权限访问');
          break;
        case 404:
          ElMessage.error('请求接口不存在');
          break;
        default:
          ElMessage.error(data.message || '服务器错误');
          break;
      }
    } else {
      ElMessage.error(error.message || '网络错误');
    }
    return Promise.reject(error);
  }
);

解释
  • 业务状态码检查:后端返回的数据通常会包含一个 code 字段,用于表示请求的执行状态。我们根据返回的 code 来判断请求是否成功。如果 code !== 200,则说明请求失败,展示错误信息并拒绝 Promise。

  • 错误处理:如果响应中返回了错误(如 401 未授权,403 无权限,404 接口不存在等),我们会根据不同的状态码展示对应的错误信息。

  • 请求被取消:通过 axios.isCancel(error) 检测错误是否为请求被取消。如果是请求取消错误,则输出警告并返回。

通过响应拦截器,我们统一处理了各种错误情况,使得前端能够及时反馈给用户。

4. 请求重试机制
为什么需要请求重试?

在一些网络不稳定或者服务器偶尔超时的情况下,我们希望请求能够自动重试,而不是直接失败。我们通过封装请求重试逻辑来实现这一点。

代码解析
/**
 * 封装重试逻辑
 */
const retryRequest = async (error, retryCount) => {
  const config = error.config;

  if (!config || retryCount <= 0) {
    return Promise.reject(error);
  }

  ElMessage.info(`请求重试中... 剩余尝试次数:${retryCount}`);
  return new Promise((resolve) => {
    setTimeout(() => resolve(http(config)), 1000); // 延迟 1 秒后重试
  }).catch((err) => retryRequest(err, retryCount - 1));
};

解释
  • 重试次数:如果请求失败且重试次数大于 0,我们会等待 1 秒后再次发起请求。

  • 递归重试:每次请求失败时,我们会递归调用 retryRequest 函数,直到达到最大重试次数为止。

通过这个机制,能够提高请求的可靠性,尤其是在网络波动时。

5. 封装 HTTP 请求方法

我们将常见的 HTTP 请求方法封装为一个对象,简化了代码结构,方便在各个组件中调用。

代码解析
/**
 * 封装HTTP方法
 */
const httpMethods = {
  get: (url, params, options = {}) => http.get(url, { params, ...options }),
  post: (url, data, options = {}) => http.post(url, data, { ...options }),
  put: (url, data, options = {}) => http.put(url, data, { ...options }),
  patch: (url, data, options = {}) => http.patch(url, data, { ...options }),
  delete: (url, params, options = {}) => http.delete(url, { params, ...options }),
  options: (url, options = {}) => http.options(url, { ...options }),
  head: (url, options = {}) => http.head(url, { ...options }),

  // 文件下载
  download: async (url, options = {}) => {
    try {
      const response = await http.get(url, {
        responseType: 'blob',
        ...options,
      });

      const disposition = response.headers['content-disposition'];
      let filename = 'downloaded-file';
      if (disposition) {
        const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;\n]*)/);
        if (match) {
          filename = decodeURIComponent(match[1].replace(/["']/g, ''));
        }
      }

      const blob = new Blob([response.data]);
      const downloadUrl = window.URL.createObjectURL(blob);

      const link = document.createElement('a');
      link.href = downloadUrl;
      link.download = filename;
      link.click();

      window.URL.revokeObjectURL(downloadUrl);
      ElMessage.success('文件下载成功');
    } catch (error) {
      ElMessage.error('文件下载失败');
      console.error(error);
      throw error;
    }
  },

  // 取消请求
  cancel: (url) => {
    const source = cancelTokens.get(url);
    if (source) {
      source.cancel(`取消请求:${url}`);
      cancelTokens.delete(url);
    }
  },

  // 请求方法支持重试
  retryableRequest: async (method, url, data, options = {}, retryCount = 1) => {
    try {
      const response = await httpMethods[method](url, data, options);
      return response;
    } catch (error) {
      if (retryCount > 0) {
        return retryRequest(error, retryCount);
      }
      throw error;
    }
  },
};



解释
  • 封装 HTTP 方法:我们通过 httpMethods 对象封装了常见的 GET, POST, PUT, PATCH, DELETE 请求方法,简化了每个请求的调用。

  • 文件下载:download 方法支持从服务器下载文件,处理了响应头中的文件名和 Blob 数据。

  • 请求取消:通过 cancel 方法可以取消指定 URL 的请求。

支持重试的请求:retryableRequest 方法允许我们在请求失败时自动重试。

6. http各种情况下使用

以下是针对你提到的各个场景的测试用例,每个 demo 都是如何使用封装后的 HTTP 请求工具的方法。

1. 重试机制的 Demo

在请求失败时,自动重试请求,最多重试 3 次。

代码示例:
import httpMethods from '@/utils/http';

export default {
  async mounted() {
    try {
      const response = await httpMethods.retryableRequest('get', '/api/data', null, {}, 3);
      console.log('请求成功:', response);
    } catch (error) {
      console.error('请求失败:', error);
    }
  },
};

解释:
  • 使用 retryableRequest 方法,第二个参数为 URL,第三个参数为请求参数(这里为 null),最后一个参数为重试次数。
  • 如果请求失败且重试次数大于 0,会进行重试。
2. 文件下载的 Demo

通过封装的 download 方法从后端下载文件,并处理文件的保存。

代码示例:
import httpMethods from '@/utils/http';

export default {
  methods: {
    async downloadFile() {
      try {
        const url = '/api/file/download';
        await httpMethods.download(url);
      } catch (error) {
        console.error('文件下载失败:', error);
      }
    },
  },
};

解释:

调用 httpMethods.download 方法,传入文件下载的接口 URL 即可自动处理文件下载,

注: 可根据前后端定义类型进行适当调整http中的下载方案。

3. Form 表单提交的 Demo

使用封装的 postForm 方法提交表单数据,处理表单的提交逻辑。

代码示例:
import httpMethods from '@/utils/http';

export default {
  data() {
    return {
      formData: {
        username: '',
        password: '',
      },
    };
  },
  methods: {
    async submitForm() {
      try {
        const response = await httpMethods.postForm('/api/user/login', this.formData);
        console.log('登录成功:', response);
      } catch (error) {
        console.error('登录失败:', error);
      }
    },
  },
};

解释:

使用 postForm 方法提交表单数据。该方法会使用 application/x-www-form-urlencoded 格式发送请求。

4. GET、POST、PUT、PATCH、DELETE 的 Demo

我们可以根据不同的 HTTP 请求类型,使用封装好的方法进行 API 调用。

代码示例:
import httpMethods from '@/utils/http';

export default {
  methods: {
    async getData() {
      try {
        const response = await httpMethods.get('/api/data', { id: 123 });
        console.log('GET 请求成功:', response);
      } catch (error) {
        console.error('GET 请求失败:', error);
      }
    },

    async postData() {
      try {
        const response = await httpMethods.post('/api/data', { name: 'Test' });
        console.log('POST 请求成功:', response);
      } catch (error) {
        console.error('POST 请求失败:', error);
      }
    },

    async putData() {
      try {
        const response = await httpMethods.put('/api/data/123', { name: 'Updated' });
        console.log('PUT 请求成功:', response);
      } catch (error) {
        console.error('PUT 请求失败:', error);
      }
    },

    async patchData() {
      try {
        const response = await httpMethods.patch('/api/data/123', { name: 'Patched' });
        console.log('PATCH 请求成功:', response);
      } catch (error) {
        console.error('PATCH 请求失败:', error);
      }
    },

    async deleteData() {
      try {
        const response = await httpMethods.delete('/api/data/123');
        console.log('DELETE 请求成功:', response);
      } catch (error) {
        console.error('DELETE 请求失败:', error);
      }
    },
  },
};

解释:
  • GET 请求:用于获取资源,通过 httpMethods.get 方法传入 URL 和参数。

  • POST 请求:用于提交数据,通过 httpMethods.post 方法传入 URL 和数据。

  • PUT 请求:用于更新资源,通过 httpMethods.put 方法传入 URL 和数据。

  • PATCH 请求:部分更新资源,使用 httpMethods.patch。

  • DELETE 请求:删除资源,通过 httpMethods.delete 方法。
    每个方法的参数分别为:

  • GET 请求:URL 和查询参数(params)。

  • POST、PUT、PATCH 请求:URL 和请求数据(data)。

  • DELETE 请求:URL 和查询参数(params)。

7. 总结

通过封装 Axios,我们在 Vue 3 项目中实现了以下功能:

  • Token 验证与请求拦截:确保每个请求都有合法的 Token,避免用户未授权访问。
  • 响应拦截与错误处理:根据不同的响应状态码提供相应的错误提示。
  • 请求重试机制:在网络不稳定时自动重试请求,确保请求成功。
  • 文件下载支持:支持下载文件并自动处理文件名和内容。
  • 请求取消与性能优化:避免重复请求,提升性能。

通过这种方式,HTTP 请求的处理更加集中化、模块化,使得前端代码更加简洁且易于维护。

完整的http.js代码:
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router'; // 根据实际项目调整路径

/**
 * 创建axios实例
 */
const http = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || '/', // 配置基础路径
  timeout: 10000, // 超时时间
  headers: {
    'Content-Type': 'application/json',
  },
});

/**
 * 请求取消 Token Map
 */
const cancelTokens = new Map();

/**
 * 请求拦截器
 */
http.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    const isLoginRequest = config.url.includes('/login') || config.params?.type === 'login';

    // 如果没有 Token 且不是登录请求,跳转到登录页
    if (!token && !isLoginRequest) {
      ElMessage.error('登录已过期,请重新登录');
      router.push('/login');
      return Promise.reject(new Error('未授权,请登录'));
    }

    // 如果有 Token 且不是登录请求,添加到请求头
    if (token && !isLoginRequest) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }

    // 添加取消请求功能
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;
    cancelTokens.set(config.url, source);

    return config;
  },
  (error) => {
    ElMessage.error('请求发送失败');
    return Promise.reject(error);
  }
);

/**
 * 响应拦截器
 */
http.interceptors.response.use(
  (response) => {
    // 请求成功后移除对应的取消 Token
    cancelTokens.delete(response.config.url);

    const { data } = response;

    // 检查业务状态码(根据后端实际约定调整)
    if (data.code !== 200) {
      ElMessage.error(data.message || '未知错误');
      return Promise.reject(new Error(data.message || '未知错误'));
    }

    return data;
  },
  (error) => {
    if (axios.isCancel(error)) {
      console.warn('请求被取消:', error.message);
      return Promise.reject(error);
    }

    // 响应失败时,清理对应的取消 Token
    cancelTokens.delete(error.config?.url);

    if (error.response) {
      const { status, data } = error.response;

      switch (status) {
        case 401:
          ElMessage.error('未授权或登录过期,请重新登录');
          localStorage.removeItem('token');
          router.push('/login');
          break;
        case 403:
          ElMessage.error('没有权限访问');
          break;
        case 404:
          ElMessage.error('请求接口不存在');
          break;
        default:
          ElMessage.error(data.message || '服务器错误');
          break;
      }
    } else {
      ElMessage.error(error.message || '网络错误');
    }
    return Promise.reject(error);
  }
);

/**
 * 封装重试逻辑
 */
const retryRequest = async (error, retryCount, showError = true) => {
  const config = error.config;

  if (!config || retryCount <= 0) {
    // 最后一次重试失败,提示错误
    if (showError) {
      ElMessage.error(error.message || '网络请求失败');
    }
    return Promise.reject(error);
  }

  return new Promise((resolve) => {
    setTimeout(() => resolve(http(config)), 1000); // 延迟 1 秒后重试
  }).catch((err) => retryRequest(err, retryCount - 1, retryCount === 1));
};

/**
 * 路由切换时取消未完成的请求
 */
router.beforeEach((to, from, next) => {
  if (from.fullPath !== to.fullPath) {
    cancelTokens.forEach((source, url) => {
      source.cancel(`路由切换取消请求:${url}`);
    });
    cancelTokens.clear();
  }
  next();
});

/**
 * 封装HTTP方法
 */
const httpMethods = {
  get: (url, params, options = {}) => http.get(url, { params, ...options }),
  post: (url, data, options = {}) => http.post(url, data, { ...options }),
  put: (url, data, options = {}) => http.put(url, data, { ...options }),
  patch: (url, data, options = {}) => http.patch(url, data, { ...options }),
  delete: (url, params, options = {}) =>
    http.delete(url, { params, ...options }),
  options: (url, options = {}) => http.options(url, { ...options }),
  head: (url, options = {}) => http.head(url, { ...options }),

  // 表单请求
  postForm: (url, data, options = {}) => http.postForm(url, data, { ...options }),
  putForm: (url, data, options = {}) => http.putForm(url, data, { ...options }),
  patchForm: (url, data, options = {}) =>
    http.patchForm(url, data, { ...options }),

  // 文件下载
  download: async (url, options = {}) => {
    try {
      const response = await http.get(url, {
        responseType: 'blob',
        ...options,
      });

      const disposition = response.headers['content-disposition'];
      let filename = 'downloaded-file';
      if (disposition) {
        const match = disposition.match(/filename\*?=(?:UTF-8'')?([^;\n]*)/);
        if (match) {
          filename = decodeURIComponent(match[1].replace(/["']/g, ''));
        }
      }

      const blob = new Blob([response.data]);
      const downloadUrl = window.URL.createObjectURL(blob);

      const link = document.createElement('a');
      link.href = downloadUrl;
      link.download = filename;
      link.click();

      window.URL.revokeObjectURL(downloadUrl);
      ElMessage.success('文件下载成功');
    } catch (error) {
      ElMessage.error('文件下载失败');
      console.error(error);
      throw error;
    }
  },

  // 取消请求
  cancel: (url) => {
    const source = cancelTokens.get(url);
    if (source) {
      source.cancel(`取消请求:${url}`);
      cancelTokens.delete(url);
    }
  },

  // 请求方法支持重试
  retryableRequest: async (method, url, data, options = {}, retryCount = 1) => {
    try {
      const response = await httpMethods[method](url, data, options);
      return response;
    } catch (error) {
      return retryRequest(error, retryCount);
    }
  },
};

export default httpMethods;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值