分步骤讲解
安装 axios、qs
yarn add axios
yarn add qs
axios 配置
封装 axios,由于 axios 是插件所以新建一个 plugins 文件夹来放置:
// ./src/plugins/axios/axiosConfigs.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import store from '@/store';
import { ElMessage } from "element-plus";
// 创建axios的实例
const service = axios.create({
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
baseURL: process.env.NODE_ENV === 'production' ? `/` : '/api',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
},
withCredentials: false, // 跨域请求时是否需要使用凭证
timeout: 30000,
// `validateStatus` 定义对于给定的 HTTP 响应状态码是 resolve 或 reject promise 。
validateStatus() {
// `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
// 使用 async-await,处理 reject 情况较为繁琐,所以全部返回 resolve,在业务代码中处理异常
return true;
},
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [(data) => {
if (typeof data === 'string' && data.startsWith('{')) {
data = JSON.parse(data);
}
return data;
}]
});
// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
// console.log('发送请求之前', config.url);
// 获取 token ,并将其添加至请求头中
let token = store.state.user.token;
if(token){
config.headers.Authorization = token;
// config.headers.Authorization = 'Bearer ' + token;
}
return config;
}, (error: any) => {
// console.log('发送请求错误', error.response, error.data);
// 错误抛到业务代码
error.data = {
message: '服务器异常,请联系管理员!'
};
return Promise.reject(error);
});
//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
// console.log('响应拦截', response.status, response);
/* 处理 http 错误,抛到业务代码 */
const status = response.status;
const decide = status < 200 || status >= 300;
if (decide) {
const message = showStatus(status);
// console.log("处理 http 错误", message);
if (typeof response.data === 'string') {
response.data = { message };
} else {
response.data.message = message;
}
ElMessage({
message,
type: 'error',
showClose: true
})
return Promise.reject(response.data);
}
return response;
}, (error: any) => {
// console.log('请求错误', error, axios.isCancel(error), error.message);
if (axios.isCancel(error)) {
// console.log('重复请求: ' + error.message);
ElMessage({
message: '请勿重复请求',
type: 'warning',
showClose: true
});
} else {
const message = '请求超时或服务器异常,请检查网络或联系管理员!';
ElMessage({
message,
type: 'error',
showClose: true
});
}
return Promise.reject(error);
});
const showStatus = (status: number) => {
let message = '';
switch (status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
break;
case 403:
message = '拒绝访问(403)';
break;
case 404:
message = '请求出错(404)';
break;
case 408:
message = '请求超时(408)';
break;
case 500:
message = '服务器错误(500)';
break;
case 501:
message = '服务未实现(501)';
break;
case 502:
message = '网络错误(502)';
break;
case 503:
message = '服务不可用(503)';
break;
case 504:
message = '网络超时(504)';
break;
case 505:
message = 'HTTP版本不受支持(505)';
break;
default:
message = `连接出错(${status})!`;
}
return message;
// return `${message},请检查网络或联系管理员!`
};
export default service;
封装 axios 请求
设置请求及响应数据格式
设置响应数据格式,设置 get 、 post 等方法的格式:
// ./src/plugins/axios/types.ts
import { AxiosResponse, AxiosRequestConfig } from 'axios';
// 网络请求响应格式,T 是具体的接口返回类型数据
interface CustomSuccessData<T> {
code: number;
msg?: string;
message?: string;
data?: T;
[keys: string]: any;
}
interface Get {
<T>(url: string, config?: AxiosRequestConfig): Promise<CustomSuccessData<T>>;
}
interface Post {
<T>(url: string, params?: string | object, config?: AxiosRequestConfig): Promise<CustomSuccessData<T>>;
}
// ... delete 等等
export {
CustomSuccessData,
Get,
Post
}
封装 axios 请求方法
封装 get 、 post 等方法,使用 request 统一调用:
// ./src/plugins/axios/request.ts
import service from '@/plugins/axios/axiosConfigs'
import { Get, Post } from './types'; // 接口泛型
// 封装 get 方法,类型为Get
const get: Get = async (url, config) => {
const response = await service.get(url, { ...config});
return response.data;
};
const post: Post = async (url, params, config) => {
const response = await service.post(url, params, {...config});
return response.data;
};
// ... delete 等等
// 使用 request 统一调用
const request = {
get,
post
};
export default request;
接口配置
配置 api 接口,新增 api 文件夹:
// ./src/api/httpUrl.ts
const config: Api = {
rootUrl: "http://localhost:8080/",
};
const httpApi: Api = {
// 测试接口
banner: config.rootUrl + 'home/banner', // banner
login: config.rootUrl + 'user/login', // 用户登录
}
export default httpApi;
请求接口配置:
// ./src/api/requestApi.ts
import request from '@/plugins/axios/request'; // axios 封装
import '@/utils/interfaces/AjaxResponse'; // 后端响应数据接口
import '@/utils/interfaces/AjaxRequest'; // 前端请求数据接口
import httpUrl from "./httpUrl"; // 接口 url
import qs from "qs";
// 获取 banner
const getBanner = async () => {
return await request.get<Array<AjaxResponse.Banner>>(httpUrl.banner);
}
// 用户登录
const login = async (params: AjaxRequest.login) => {
return await request.post<string>(httpUrl.login, qs.stringify(params));
}
const handleError = (err: any) => {
// console.log("请求错误", err);
throw err;
}
export {
getBanner,
login,
handleError
}
使用
<template>
<div class="home">
{{num}}
<el-button @click="apiTest">测试按钮</el-button>
</div>
</template>
<script lang="ts" setup>
import { getBanner, login, handleError } from '@/api/requestApi'; // 请求接口
const num = ref(100);
const apiTest = async () => {
console.log("apiTest");
// get 请求
const banner = await getBanner().catch(handleError);
console.log('banner', banner);
// post 请求
// 后续会转成 vuex actions,此处只是测试
let logintest = await login({
tel: '13430046832', // 随手输的
password: '123456'
}).catch(handleError);
console.log("logintest", logintest);
}
</script>
错误测试
把请求头设置为 'Content-Type': 'application/json;charset=utf-8',请求 login 接口,报错:
取消重复请求
设置 添加/移除/清空 请求的方法:
// ...
import qs from 'qs';
const service = axios.create({
// ...
});
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map();
/**
* @description: 添加请求
* @param {AxiosRequestConfig} config
*/
const addPending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&');
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
// console.log("请求队列中不存在当前请求,添加请求")
pending.set(url, cancel);
}
})
}
/**
* @description: 移除请求
* @param {AxiosRequestConfig} config
* @return {*}
*/
let isRemove = false
const removePending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&');
if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
// console.log("请求队列中已存在当前请求,不再重复请求");
const cancel = pending.get(url);
isRemove = true
cancel(url); // 取消请求
pending.delete(url);
}
}
/**
* @description: 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = () => {
// console.log("清空请求队列");
for (const [url, cancel] of pending) {
cancel(url); // 取消请求
}
pending.clear(); // 清空请求
}
在拦截器中加入:
// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
// console.log('发送请求之前', config.url);
removePending(config) // 在请求开始前,对之前的请求做检查取消操作
addPending(config) // 将当前请求添加到 pending 中
// ...
return config;
}, (error: any) => {
// ...
});
//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
// console.log('响应拦截', response.status, response);
removePending(response) // 在请求结束后,移除本次请求
// ...
}, (error: any) => {
if (axios.isCancel(error)) { // 取消请求
if(isRemove){ // 路由切换导致的取消请求,不提示
ElMessage({
message: '请勿重复请求',
type: 'warning',
showClose: true
});
isRemove = false
}
} else {
// ...
}
// ...
});
在路由跳转时撤销所有请求
在路由跳转时撤销所有请求。在路由文件 ./src/router/index.ts 中加入:
// ./src/router/index.ts
import { clearPending } from "@/plugins/axios/axiosConfigs";
import store from '@/store'
// ...
// 路由守卫
router.beforeEach((to, from, next) => {
clearPending(); // 在跳转路由之前,先清除所有的请求
if(to.meta.isAuth){
const token = store.state.user.token;
if(token){
next();
}else{
next({
name: 'login'
});
}
}else{
next();
}
});
// ...
axios 配置完整代码
// ./src/plugins/axios/axiosConfigs.ts
/*
* @Author: una
* @Date: 2021-06-16 17:16:12
* @LastEditors: una
* @LastEditTime: 2021-07-09 14:51:28
* @Description: 封装 axios
*/
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import store from '@/store';
import { ElMessage } from "element-plus";
import qs from 'qs';
// 创建axios的实例
const service = axios.create({
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL
baseURL: process.env.NODE_ENV === 'production' ? `/` : '/api',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 'Content-Type': 'application/json;charset=utf-8'
},
withCredentials: false, // 跨域请求时是否需要使用凭证
timeout: 30000,
// `validateStatus` 定义对于给定的 HTTP 响应状态码是 resolve 或 reject promise 。
validateStatus() {
// `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
// 使用 async-await,处理 reject 情况较为繁琐,所以全部返回 resolve,在业务代码中处理异常
return true;
},
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [(data) => {
if (typeof data === 'string' && data.startsWith('{')) {
data = JSON.parse(data);
}
return data;
}]
});
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map();
/**
* @description: 添加请求
* @param {AxiosRequestConfig} config
*/
const addPending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&');
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
// console.log("请求队列中不存在当前请求,添加请求")
pending.set(url, cancel);
}
});
}
/**
* @description: 移除请求
* @param {AxiosRequestConfig} config
* @return {*}
*/
let isRemove = false
const removePending = (config: AxiosRequestConfig) => {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&');
if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
// console.log("请求队列中已存在当前请求,不再重复请求");
const cancel = pending.get(url);
isRemove = true
cancel(url); // 取消请求
pending.delete(url);
}
}
/**
* @description: 清空 pending 中的请求(在路由跳转时调用)
*/
export const clearPending = () => {
// console.log("清空请求队列");
for (const [url, cancel] of pending) {
cancel(url); // 取消请求
}
pending.clear(); // 清空请求
}
// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
// console.log('发送请求之前', config.url);
removePending(config); // 在请求开始前,对之前的请求做检查取消操作
addPending(config); // 将当前请求添加到 pending 中
// 获取token,并将其添加至请求头中
const token = store.state.user.token;
if(token){
config.headers.Token = token;
// config.headers.Authorization = 'Bearer ' + token;
}
return config;
}, (error: any) => {
// console.log('发送请求错误', error.response, error.data);
// 错误抛到业务代码
error.data = {
message: '服务器异常,请联系管理员!'
};
return Promise.reject(error);
});
//添加响应拦截器
service.interceptors.response.use((response: AxiosResponse) => {
// console.log('响应拦截', response.status, response);
removePending(response); // 在请求结束后,移除本次请求
/* 处理 http 错误,抛到业务代码 */
const status = response.status;
const decide = status < 200 || status >= 300;
if (decide) { // http 错误
const message = showStatus(status);
// console.log("处理 http 错误", message);
if (typeof response.data === 'string') {
response.data = { message };
} else {
response.data.message = message;
}
ElMessage({
message,
type: 'error',
showClose: true
});
return Promise.reject(response.data);
}else { // 接口连接成功
if(response.data.code == 200){
return response.data
}else { // 接口报错
if(response.config.url){
if(response.config.url.indexOf('login') > -1){
store.commit('user/SET_LOGIN_ERR_MSG', response.data.data)
store.commit('user/SET_TOKEN', '')
}else{
ElMessage({
message: response.data.data || response.data.message,
type: 'error',
showClose: true
});
}
throw response;// 抛出错误
}
}
}
}, (error: any) => {
// console.log('请求错误', error, axios.isCancel(error), error.message);
if (axios.isCancel(error)) { // 取消请求
if(isRemove){ // 路由切换导致的取消请求,不提示
// console.log('重复请求: ' + error.message);
ElMessage({
message: '请勿重复请求',
type: 'warning',
showClose: true
});
isRemove = false
}
} else {
const message = '请求超时或服务器异常,请检查网络或联系管理员!';
ElMessage({
message,
type: 'error',
showClose: true
});
}
return Promise.reject(error);
});
const showStatus = (status: number) => {
let message = '';
switch (status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
break;
case 403:
message = '拒绝访问(403)';
break;
case 404:
message = '请求出错(404)';
break;
case 408:
message = '请求超时(408)';
break;
case 500:
message = '服务器错误(500)';
break;
case 501:
message = '服务未实现(501)';
break;
case 502:
message = '网络错误(502)';
break;
case 503:
message = '服务不可用(503)';
break;
case 504:
message = '网络超时(504)';
break;
case 505:
message = 'HTTP版本不受支持(505)';
break;
default:
message = `连接出错(${status})!`;
}
return message;
// return `${message},请检查网络或联系管理员!`
};
export default service;
扩展:设置全局变量
自定义属性添加到每个组件实例中,可以通过 this 访问(不建议使用):
import { createApp } from 'vue'
import App from './App.vue'
import route from './route'
// 配置请求数据
import { AxiosInstance } from "axios";
import Axios from "axios";
// 全局配置Axios
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
}
}
let app = createApp(App)
app.config.globalProperties.$axios = Axios; // this.Axios
app.use(route)
app.mount('#app')
使用:
import httpUrl from "@/api/httpUrl"; // 接口 url
// ...
mounted(){
const apiTest = () => {
// eslint-disable-next-line no-unexpected-multiline
(this as any).$axios.get(httpUrl.banner).then((res: any) => {
let data = res.data.data
console.log("banner", data)
}).catch((error: any) => {
console.log('获取首页banner 失败', error.)
})
}
apiTest()
}
此处只作为一个扩展,不推荐使用这种方式。