深入解析 Vue.js 项目中的 Axios 请求与响应拦截器
在现代前端开发中,Axios 是一个非常常用的库,用于处理 HTTP 请求。结合 Vue.js 框架,我们可以通过 Axios 与后端 API 进行交互,获取和提交数据。而在复杂的项目中,我们常常需要对请求和响应进行一些统一的处理,比如添加认证 Token、处理请求失败、处理文件下载等。
本文将一步步解析如何在 Vue.js 项目中使用 Axios 进行网络请求,详细介绍请求拦截器和响应拦截器,并结合具体的场景帮助你理解。
1. 引入和初始化 Axios
在使用 Axios 之前,我们首先需要安装并引入它。
npm install axios --save
在 Vue.js 项目中,我们通常会创建一个 http.js 或 axios.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、防止重复提交、处理文件下载、响应错误处理等场景。通过统一的请求和响应管理,我们可以提升代码的可维护性,并确保用户在操作过程中获得流畅的体验。
通过这种方式,你可以确保应用的健壮性和用户的良好体验,特别是在处理复杂的业务需求时。