第十章 请求配置 (Axios请求和响应拦截)
前言
在Vue
项目中使用 axios
是常见的实践,因为 axios
是一个流行的基于Promise
的 HTTP
客户端,可用于浏览器和 Node.js
环境。它提供了许多优点,包括易用性、可靠性和在处理 HTTP
请求时的灵活性。
使用 axios
可以轻松地执行 HTTP
请求,包括 GET
、POST
等,还可以简化对响应数据的处理。此外,axios
还提供了拦截器(interceptors)
、取消请求、全局配置等功能,使其成为在 Vue
项目中进行 HTTP
通信的强大工具。
axios
相比其他 HTTP
客户端库具有几个优势:
- 基于 Promise:
axios
利用Promise
对象来处理请求和响应,使得代码更加清晰、可读,并且更容易处理异步操作。
- 基于 Promise:
- 浏览器和Node.js兼容:
axios
可以在浏览器和Node.js
环境中使用,这使得它非常灵活,可以在各种环境中进行HTTP
通信。
- 浏览器和Node.js兼容:
- 易用性:
axios
的API
设计非常直观和简单,使其易于学习和使用。它提供了丰富的配置选项和拦截器接口,以满足各种复杂的HTTP
请求场景。
- 易用性:
- 错误处理:
axios
提供了丰富的错误处理机制,包括全局的错误处理和局部的错误处理,使得在处理HTTP请求过程中能够更好地应对各种异常情况。
- 错误处理:
- 拦截器支持:
axios
允许你在发送请求或接收响应前对它们进行拦截和处理,这为添加公共头部、身份验证等操作提供了便利。
- 拦截器支持:
一、安装和配置
1、安装
# npn 安装
npm install axios
# yarn 安装
yarn add axios
2、配置
axios
不用想其他依赖库需要在 main.ts
中 import
,只需要在文件中请求,就可以使用,不过为了更方便的在项目中使用,我们需要通过拦截器为 axios
做统一的封装,在个在下一节会说到。
在 src 目录在创建 api 文件夹,并创建 userApi.ts 文件
import axios from "axios";
// 登陆
export function userLogin(data = {}) {
return axios.post({
url: "/login",
data,
});
}
二、拦截器介绍和使用
1、拦截器介绍
axios
是一个基于 Promise
的 HTTP
客户端,可用于浏览器和 Node.js
。它允许您在发送请求或接收响应时使用拦截器来执行特定的操作。axios
拦截器分为请求拦截器和响应拦截器。
请求拦截器允许您在发送请求之前对其进行修改或添加特定的配置。您可以通过请求拦截器来添加认证信息、设置请求头、转换请求数据等操作。
2、拦截器的使用
请求拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
config.headers.Authorization = 'Bearer token'
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
响应拦截器
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error)
})
三、axios 请求封装
在 utils 文件下创建 service 文件夹,新建四个文件,分别是:config.ts、http.ts、index.ts、types.ts
1、config.ts文件
该文件为 url 等相关配置
const TEST_BASE_URL = '/mock'
const API_BASE_URL = '/api'
const AUTH_BASE_URL = '/auth'
const TIME_OUT = 30 * 1000
export {
TEST_BASE_URL,
API_BASE_URL,
AUTH_BASE_URL,
TIME_OUT
}
2、types.ts文件
该文件为ts 的类型声明
import type { AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'
export interface BaseRequestInterceptors<T = AxiosResponse<any>> {
requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
responseInterceptor?: (res: T) => T
responseInterceptorCatch?: (error: any) => any
}
export interface BaseRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: BaseRequestInterceptors<T>
loading?: boolean | string
noToken?: boolean
}
3、http.ts文件
该文件将请求拦截和相应拦截以及相关逻辑全部封装
定义了一个基于 axios
封装的 BaseRequest
类,用于发送 http
请求。具体功能有:
- 创建
axios
实例,并允许用户自定义配置; - 支持使用拦截器对请求和响应进行处理;
- 支持在请求中添加
token
; - 支持在请求中显示
loading
; - 提供一系列方法用于发送
get、post、put、delete、patch
请求; - 对不同的
http
错误状态码进行处理,并在错误时显示对应的错误信息。
import axios from "axios";
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
import { BaseRequestConfig, BaseRequestInterceptors } from "./types";
import { ElLoading, ElMessage, ElMessageBox } from "element-plus";
import { LoadingInstance } from "element-plus/es/components/loading/src/loading";
import { getToken, logout } from "@/utils/tools/user";
const DEFAULT_LOADING = false;
let isRelogin = false;
class BaseRequest {
// axios 实例
instance: AxiosInstance;
interceptors?: BaseRequestInterceptors;
showLoading: boolean | string;
loading?: LoadingInstance;
constructor(config: BaseRequestConfig) {
this.instance = axios.create(config);
this.showLoading = config.loading ?? DEFAULT_LOADING;
this.interceptors = config.interceptors;
// 使用拦截器
// 1. 从config中取出的拦截器是对应的实例的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
);
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
);
// 2.添加所有的实例都有的拦截器
this.instance.interceptors.request.use(
(config) => {
if (config.params && config.params.responseType) {
config.responseType = config.params.responseType;
delete config.params.responseType;
}
if (
(config.method === "post" || config.method === "put") &&
config.params
) {
if (config.params.formData) {
config.data = config.params.formData;
} else {
config.data = { ...config.params };
}
delete config.params;
}
// console.log('所有的实例都有的拦截器: 请求成功拦截')
if (this.showLoading) {
const loadingText =
typeof this.showLoading == "boolean" ? "加载中" : this.showLoading;
this.loading = ElLoading.service({
lock: true,
text: loadingText + "...",
background: "rgba(0, 0, 0, 0.5)",
});
}
return config;
},
(err) => {
console.log("所有的实例都有的拦截器: 请求失败拦截");
return err;
}
);
this.instance.interceptors.response.use(
(res) => {
// 将loading移除
this.loading?.close();
if (res.status !== 200) return res;
return res.data;
},
(err) => {
// console.log('所有的实例都有的拦截器: 响应失败拦截')
// 将loading移除
this.loading?.close();
// 例子: 判断不同的HttpErrorCode显示不同的错误信息
switch (err.response.status) {
case 400:
ElMessage.error("请求错误(400)");
break;
case 401:
ElMessage.error("未授权,请重新登录(401)");
logout();
break;
case 403:
ElMessage.error("拒绝访问(403)");
break;
case 404:
ElMessage.error("请求出错(404)");
break;
case 408:
ElMessage.error("请求超时(408)");
break;
case 500:
ElMessage.error("服务器错误(500)");
break;
case 501:
ElMessage.error("服务未实现(501)");
break;
case 502:
ElMessage.error("网络错误(502)");
break;
case 503:
ElMessage.error("服务不可用(503)");
break;
case 504:
ElMessage.error("网络超时(504)");
break;
case 505:
ElMessage.error("HTTP版本不受支持(505)");
break;
default: {
ElMessage.error(`连接出错(${err.response.status})!`);
}
}
return err;
}
);
}
request<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 1.单个请求对请求config的处理
if (config.interceptors?.requestInterceptor) {
// 对config 进行转化
config = config.interceptors.requestInterceptor(
config as InternalAxiosRequestConfig
);
}
// 携带token的拦截
if (!config.noToken && getToken()) {
config.headers = {
Authorization: getToken(),
};
}
// 2.判断是否需要显示loading
if (config.loading) {
this.showLoading = config.loading;
}
this.instance
.request<any, T>(config)
.then((res: any) => {
// 1.单个请求对数据的处理
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res);
}
// 2.将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
if (res.status !== 200) {
if (config.responseType == "blob") {
resolve(res);
return;
}
let errMsg = res.message || "请求失败";
if (res.code == "501") errMsg = "数据中存在敏感词汇, 请修改!";
if (res.status == 401) {
if (!isRelogin) {
isRelogin = true;
// prettier-ignore
errMsg = res.message || '登录状态已过期,您可以继续留在该页面,或者重新登录'
ElMessageBox.confirm(errMsg, "系统提示", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
isRelogin = false;
logout();
})
.catch(() => {
isRelogin = false;
});
}
reject(res);
return;
}
ElMessage({
message: errMsg,
type: "error",
});
reject(res);
} else {
resolve(res);
}
})
.catch((err) => {
// 将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEFAULT_LOADING;
reject(err);
return err;
});
});
}
get<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "GET" });
}
post<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "POST" });
}
put<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PUT" });
}
delete<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "DELETE" });
}
patch<T = any>(config: BaseRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: "PATCH" });
}
}
export default BaseRequest;
4、index.ts文件
使用已经封装好的 BaseRequest
import BaseRequest from './http'
import { TIME_OUT } from './config'
const testRequest = new BaseRequest({
timeout: TIME_OUT,
interceptors: {
requestInterceptor: config => {
return config
},
requestInterceptorCatch: err => {
// console.log('请求失败的拦截')
return err
},
responseInterceptor: res => {
// console.log('响应成功的拦截')
return res
},
responseInterceptorCatch: err => {
const errRes = err.response
if (errRes) {
return {
status: errRes.status,
message: errRes.data.message
}
}
return err
}
}
})
export default testRequest
5、使用封装
修改 login 接口
import testRequest from "@/utils/service";
import { TEST_BASE_URL } from "@/utils/service/config";
// 登陆
export function userLogin(data = {}) {
return testRequest.post({
url: TEST_BASE_URL + "/login",
data,
});
}
四、mock 的使用
1、什么是 mockjs
Mock.js
是一个用于生成模拟数据的 JavaScript
库,它能帮助前端开发人员模拟后端接口数据,尤其适用于前后端分离开发中。通过 Mock.js
,开发人员可以轻松地创建和配置虚拟接口,从而使前端能够在后端接口尚未完成或不易获取时进行开发和测试。
Mock.js
提供了丰富的数据模拟功能,包括生成随机的文本、数字、日期、布尔值、数组等等,还能模拟不同情况下的数据返回。使用 Mock.js
可以大大提高前端开发效率,促进前后端协作,是一个强大而实用的前端开发工具。
2、安装
# yarn 安装
yarn add mockjs -D
# 这里要特别注意版本,如果版本过高会报错 require 找不到
yarn add vite-plugin-mock@2.9.6 -D
3、配置
在根目录创建 mock 文件,并在其中创建 index.ts 文件,增加测试的请求
import { MockMethod } from "vite-plugin-mock";
const mockItems: MockMethod[] = [
{
url: "/mock/login",
method: "post",
response: () => {
return {
status: 200,
data: {
"access_token": "abc",
},
message: "success",
};
},
},
];
export default mockItems;
在 vite.config.ts 文件中
import { viteMockServe } from "vite-plugin-mock";
plugins: [
vue(),
nodePolyfills(),
setupExtend(),
// 新增代码
viteMockServe({
mockPath: "./mock",
localEnabled: command === "serve"
}),
// ...
]
4、界面测试 mock
1. 修改登录按钮逻辑
views文件下 login文件登录逻辑修改如下
const loginClick = () => {
loading.value = true;
const accessToken = getToken();
if (!accessToken) {
userLogin()
.then((res) => {
setToken(res.data.access_token);
})
.then(() => {
userLoginFunc();
})
.catch((err) => {
ElMessage.error(err);
loading.value = false;
});
} else {
userLoginFunc();
}
};
2. 在登录页面点击登录,触发接口
总结
本文介绍了在 Vue
项目中使用 axios
进行 HTTP
通信的方法。首先详细介绍了 axios
的安装和配置方法,包括 npm
或 yarn
安装以及文件请求的简单示例。然后讲解了 axios
拦截器的作用和使用方法,分别介绍了请求拦截器和响应拦截器的使用。接着讲解了对 axios
请求的封装,包括 config
、types
和http
三个文件的具体内容,以及如何使用封装好的 BaseRequest
类进行请求。最后,介绍了 mockjs
的作用和安装配置方法,展示了 mock
的使用测试请求。上文中的配置代码可在 github
仓库中直接 copy
,仓库路径:https://github.com/SmallTeddy/ProjectConstructionHub。