深入解析 Vue.js 项目中的 Axios 请求与响应拦截器

589 阅读8分钟

深入解析 Vue.js 项目中的 Axios 请求与响应拦截器

在现代前端开发中,Axios 是一个非常常用的库,用于处理 HTTP 请求。结合 Vue.js 框架,我们可以通过 Axios 与后端 API 进行交互,获取和提交数据。而在复杂的项目中,我们常常需要对请求和响应进行一些统一的处理,比如添加认证 Token、处理请求失败、处理文件下载等。

本文将一步步解析如何在 Vue.js 项目中使用 Axios 进行网络请求,详细介绍请求拦截器和响应拦截器,并结合具体的场景帮助你理解。


1. 引入和初始化 Axios

在使用 Axios 之前,我们首先需要安装并引入它。

npm install axios --save

在 Vue.js 项目中,我们通常会创建一个 http.jsaxios.js 的文件来集中管理请求相关的配置。

引入所需依赖

import axios from 'axios'
import { ElNotification, ElMessageBox, ElMessage, ElLoading } from 'element-plus'  // 用来显示错误提示、弹框和加载动画
import { getToken } from '@/utils/auth'  // 获取用户的认证 Token
import errorCode from '@/utils/errorCode'  // 错误代码映射,帮助我们根据状态码给出友好的错误信息
import { tansParams, blobValidate } from '@/utils/anivia.js'  // 参数转换工具和二进制文件验证
import cache from '@/plugins/cache'  // 用于存储和读取缓存
import { saveAs } from 'file-saver'  // 文件下载时使用的工具
import useUserStore from '@/store/system/user'  // 用于获取用户信息的状态管理(store)

这些依赖模块分别提供了不同的功能,比如显示弹框、获取 Token、处理错误信息等。

创建 Axios 实例

然后,我们创建一个 Axios 实例,统一配置它的请求基础路径、请求超时等设置:

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'

const service = axios.create({
  baseURL: '',  // 基础 URL,可以根据实际环境配置,例如开发、测试或生产环境的不同接口地址
  timeout: 10000 // 请求超时时间(单位:毫秒),如果请求超过 10 秒没有响应,会自动失败
})

创建实例后,我们就可以通过 service 来发起网络请求了。


2. 请求拦截器

请求拦截器的作用是:在请求发送之前,对请求进行一些修改、检查或处理。

1. 自动携带 Token

假设在我们的应用中,用户登录后会得到一个 Token(令牌),它用于验证用户身份。每次发起请求时,我们需要将 Token 携带在请求头中,确保后端能识别当前请求的用户。

const isToken = (config.headers || {}).isToken === false  // 检查请求是否不需要携带 Token
if (getToken() && !isToken) {
  config.headers['Authorization'] = 'Bearer ' + getToken()  // 将 Token 添加到请求头
}

这里 getToken() 是一个自定义函数,作用是从本地存储(如 localStorage)中读取保存的 Token。如果 config.headers 中没有显式地设置 isToken: false,我们就会将 Authorization 字段添加到请求头中。

使用场景:
在一些需要登录后才能访问的接口(比如个人信息、订单详情等)中,我们需要在每个请求中携带 Token,确保接口能够验证用户身份。

2. 防止重复提交

在一些场景中,用户可能会连续点击提交按钮,导致重复提交相同的数据。为此,我们可以使用缓存机制,在请求拦截器中检查是否已经提交过相同的数据。

const sessionObj = cache.session.getJSON('sessionObj')  // 从缓存中获取上次请求的数据
if (sessionObj === undefined || sessionObj === null) {
  cache.session.setJSON('sessionObj', requestObj)  // 如果没有缓存过数据,就缓存这次请求的数据
} else {
  const s_time = sessionObj.time
  const interval = 1000  // 设置重复提交的间隔时间为 1 秒
  if (s_data === requestObj.data && requestObj.time - s_time < interval) {
    return Promise.reject(new Error('数据正在处理,请勿重复提交'))
  }
}

使用场景:
在提交表单数据时(例如创建订单、提交评论等),如果用户快速连续点击提交按钮,就有可能重复提交相同的数据。为了防止这种情况,可以通过缓存请求数据和时间戳,防止重复提交。


3. 响应拦截器

响应拦截器的作用是:在响应返回之后,对响应数据进行统一处理,比如统一处理错误、解析错误消息等。

1. 处理错误码

不同的后端接口可能会返回不同的状态码。我们需要根据不同的状态码,做出相应的处理。例如,401 表示未授权,500 表示服务器错误,601 表示业务警告。

const code = res.data.code || 200  // 后端返回的数据中包含一个 code 字段,表示请求的状态码
const msg = errorCode[code] || res.data.msg || errorCode['default']  // 根据 code 获取对应的错误信息

根据 code 的值,我们可以判断请求是否成功:

if (code === 401) {
  // 如果是 401 错误,表示会话已过期,需要重新登录
  if (!isRelogin.show) {
    isRelogin.show = true
    ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
      confirmButtonText: '重新登录',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => {
      isRelogin.show = false
      useUserStore().logOut().then(() => {
        location.href = '/index'  // 退出后跳转到登录页面
      })
    }).catch(() => {
      isRelogin.show = false
    });
  }
  return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
  ElMessage({ message: msg, type: 'error' })
  return Promise.reject(new Error(msg))
} else if (code === 601) {
  ElMessage({ message: msg, type: 'warning' })
  return Promise.reject(new Error(msg))
} else if (code !== 200) {
  ElNotification.error({ title: msg })
  return Promise.reject('error')
}

使用场景:
假设用户登录后访问需要权限的页面,如果 Token 已经过期,后端会返回 401 错误。这时我们可以通过弹框提示用户重新登录。如果发生服务器错误(500),我们则展示错误消息。

2. 处理 Blob 数据(文件下载)

如果请求返回的是二进制数据(比如文件),我们需要处理 Blob 类型的响应,进行文件下载操作。

if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
  return res.data  // 直接返回 Blob 数据
}

如果返回的不是文件,而是错误信息,我们会解析并展示错误提示:

const isBlob = blobValidate(data)
if (isBlob) {
  const blob = new Blob([data])
  saveAs(blob, filename)  // 使用 file-saver 库下载文件
} else {
  const resText = await data.text()
  const rspObj = JSON.parse(resText)
  const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
  ElMessage.error(errMsg)
}

使用场景:
在一些需要下载文件的接口中(比如导出报表、下载附件等),我们需要处理文件下载。如果响应是一个文件(二进制数据),我们会将其保存到本地。


4. 通用下载方法

我们封装了一个 download 函数,供需要下载文件的地方使用。这个函数会根据接口返回的文件数据,自动处理下载,并显示加载动画。

export function download(url, params, filename, config) {
  downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)" })
  return service.post(url, params, {
    transformRequest: [(params) => { return tansParams(params) }],
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    responseType: 'blob',  // 指定响应类型为 blob,表示文件数据
    ...config
  }).then(async (data) => {
    const isBlob = blobValidate(data)
    if (isBlob) {
      const blob = new Blob([data])
      saveAs(blob, filename)  // 下载文件
    } else {
      const resText = await data.text()
      const rspObj = JSON.parse(resText)
      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
      ElMessage.error(errMsg)
    }
    downloadLoadingInstance.close()  // 下载完成后关闭 loading 动画
  }).catch((r) => {
    ElMessage.error('下载文件出现错误,请联系管理员!')
    downloadLoadingInstance.close()
  })
}

使用场景:
当用户点击导出报表按钮时,我们可以调用 download 方法,自动发起请求并下载文件。如果请求失败或出错,会展示错误信息。


5. 完整代码

import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/anivia.js'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/system/user'

let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: '',
  // 超时
  timeout: 10000
})

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  // 是否需要防止数据重复提交
  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
      time: new Date().getTime()
    }
    const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
    const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
    if (requestSize >= limitSize) {
      console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
      return config;
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    } else {
      const s_url = sessionObj.url;                // 请求地址
      const s_data = sessionObj.data;              // 请求数据
      const s_time = sessionObj.time;              // 请求时间
      const interval = 1000;                       // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)
        return Promise.reject(new Error(message))
      } else {
        cache.session.setJSON('sessionObj', requestObj)
      }
    }
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if (res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer') {
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
          isRelogin.show = false;
          useUserStore().logOut().then(() => {
            location.href = '/index';
          })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      ElMessage({ message: msg, type: 'error' })
      return Promise.reject(new Error(msg))
    } else if (code === 601) {
      ElMessage({ message: msg, type: 'warning' })
      return Promise.reject(new Error(msg))
    } else if (code !== 200) {
      ElNotification.error({ title: msg })
      return Promise.reject('error')
    } else {
      return  Promise.resolve(res.data)
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
    return Promise.reject(error)
  }
)

// 通用下载方法
export function download(url, params, filename, config) {
  downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
  return service.post(url, params, {
    transformRequest: [(params) => { return tansParams(params) }],
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    responseType: 'blob',
    ...config
  }).then(async (data) => {
    const isBlob = blobValidate(data);
    if (isBlob) {
      const blob = new Blob([data])
      saveAs(blob, filename)
    } else {
      const resText = await data.text();
      const rspObj = JSON.parse(resText);
      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
      ElMessage.error(errMsg);
    }
    downloadLoadingInstance.close();
  }).catch((r) => {
    console.error(r)
    ElMessage.error('下载文件出现错误,请联系管理员!')
    downloadLoadingInstance.close();
  })
}

export default service

5. 总结

本文详细解析了 Vue.js 项目中如何使用 Axios 进行网络请求,介绍了请求拦截器和响应拦截器的使用方法,包括自动携带 Token、防止重复提交、处理文件下载、响应错误处理等场景。通过统一的请求和响应管理,我们可以提升代码的可维护性,并确保用户在操作过程中获得流畅的体验。

通过这种方式,你可以确保应用的健壮性和用户的良好体验,特别是在处理复杂的业务需求时。