如何搭建一个项目的网络请求
1.基础请求类 baseRequest
在 baseRequest.js 文件中,我们通常会使用class
语法糖,构建一个请求类,对axios进行二次封装。
如下:
import axios from "axios";
import router from "@/router/index";
import useStore from "../store/index"
export default class FactoryRequest {
// 构造函数
constructor(baseURL = "/", requestInterCeptors) {
// 设定请求类的基础信息,同时提供一个让用户自定义请求拦截器的接口
this.baseURL = baseURL;
this.instance = null;
this.requestInterCeptors = requestInterCeptors;
// 执行初始化函数
this.init();
}
// 获取请求类中axois实例的方法
get value() {
return this.instance;
}
...
}
然后,配置请求类的初始化函数init()
init() {
// 首先创建axios实例
this.instance = axios.create({
// 请求超时时间
timeout: 5 * 1000,
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
baseUrl: this.baseUrl,
// `withCredentials` 表示跨域请求时是否需要使用凭证,默认为false
withCredentials: false,
// 自定义请求头
headers: {
"Cache-Control": "no-cache",
"Content-Type": "application/json;charset=UTF-8",
},
});
// 其次,配置请求拦截器和响应拦截器
// 请求拦截器
this.instance.interceptors.request.use(
// 第一个参数,接收一个函数,用于在发送请求之前进行一些处理
// config是axios请求的配置信息,其中包括请求头等。
(config) => {
// 如果用户自定义了拦截器,则直接使用即可
if (this.requestInterCeptors) return this.requestInterCeptors(config);
// 在请求头中加上token,校验用户权限
const store = useStore()
if (store.token){
config.headers.Authorization = "Bearer " + store.token;
}
return config;
},
// 第二个参数,接收一个函数,用于对请求错误进行处理
// error是错误信息
(error) => {
ElMessage.error({
message: "加载超时",
});
return Promise.reject(error);
},
);
// 响应拦截器
this.instance.interceptors.response.use(
// 第一个参数,接收一个函数,用于收到响应之后对响应进行一些预处理
// response是请求返回的响应。
(response) => {
if (response.data.code !== 0) {
// 响应code不为0,说明出错,首先输出错误信息
ElMessage.error(response.data.message);
// 判断错误类型,并做出相应的处理
if (response.data.code === 401) {
...
}
...
// 抛出错误信息
throw new Error(response.data.message);
} else {
// 此时code为0,说明未出错。
try {
// 进行一些预处理,例如:输出提示信息、判断响应中的某些特定字段等
...
} catch (error) {
...
}
}
return response;
},
// 第二个参数,同样接收一个函数,用于收到响应之后的错误处理
(error) => {
if (error && error.response) {
// 根据前后端约定好的错误码,设定出错文本
responseTodo(error);
} else {
// 超时出错处理
if (error.message.includes("timeout")) {
ElMessage.error("服务器响应超时,请刷新当前页");
}
error.message = "连接服务器失败";
}
ElMessage.error(error.message);
throw new Error(response.data.message);
}
)
}
可以注意到,上面的代码中提到了,需要根据前后端约定的错误码,设定出错文本。这里的错误码,是需要在编写之前与后端去确认的!
这里可以给出一个范例:
responseTodo(error) {
const backtrack = {
400: () => {
error.message = "错误请求";
},
401: () => {
error.message = "未授权,请重新登录";
},
403: () => {
error.message = "拒绝访问";
},
404: () => {
error.message = "请求错误,未找到该资源";
},
405: () => {
error.message = "请求方法未允许";
},
408: () => {
error.message = "请求超时";
},
500: () => {
error.message = "服务器端出错";
},
501: () => {
error.message = "网络未实现";
},
502: () => {
error.message = "网络错误";
},
503: () => {
error.message = "服务不可用";
},
504: () => {
error.message = "网络超时";
},
505: () => {
error.message = "http版本不支持该请求";
},
};
backtrack[error.response.status]();
}
这样,就完成了基础请求类的创建。
但是,这仅仅是一个类,要真正在项目中使用它,我们还需要创建它的实例。因此,创建一个 request.js 文件,在其中创建一个请求类的实例对象Requset
,并将其暴露出来,供后续使用。
// Request.js
import FactoryReauest from "./baseRequest";
export const baseRequest = new FactoryRequest(
import.meta.env.VITE_PROJECT_ENV === "dev" ? "" : import.meta.env.VITE_BASE_API_URL
);
// ⚠️ 本地联调无需传递参数
// export const baseRequest = new FactoryReauest();
接下来,就可以使用这个对象,对基础的get
、post
请求进行统一封装。
2.封装基础请求函数,对外统一get
、post
方法
在 api.js 文件中,对具体业务请求函数中所使用的基础请求函数进行封装。
基础请求函数,实际上是指在业务请求函数中,往往不会直接进行用 async/await 语法调用axios的get
、post
方法,设置相关配置并发送请求这些工作。因为如果这样的话,每一个业务请求函数中,都需要写一次类似的代码,这样会增加工作量,而且代码维护性也会降低。
因此,我们需要单独建立一个文件 api.js ,并在其中统一get
、post
请求的方法。
下面给出一个实例:
// api.js
// 这里的实例中,使用了Vue3工具库VueUse中提供的 useAxios API
// 其中,useAxios方法会返回多个对象,但是其中常用的只有4个,其它的可以去相关文档了解
/*
1.data --- 请求响应的数据对象
2.execute --- 实际发送请求的函数对象,其中会发送请求,在请求完成后,会更新data对象或error对象
3.isLoading --- 请求是否在加载中
4.error --- 请求出错时,execute方法会更新该对象,并将错误信息填入其中
*/
// 接下来,就可以进行请求方法封装了
------
// 首先,从request.js文件中拿到基础请求类的实例Request,并引入useAxios
import { Request } from "./request";
import { useAxios } from "@vueuse/integrations/useAxios";
// 其次,根据当前的开发环境,设置当前的请求前缀
const apiStr = import.meta.env.VITE_PROJECT_ENV === "dev" ? "/api" : "";
// Post请求封装
// 1.普通Post请求
export const commonPost = (path, postData) => {
// 返回一个Promise对象
return baseRequest.value.post(apiStr + path, {
...postData,
});
}
// 2.基于useAxios的Post请求
export const upgradedPost = async (path, postData) => {
// 调用useAxios方法,从返回对象中取出data、execute、isLoading、error4个对象
const { data, execute, isLoading, error, ...rest } = useAxios(
apiStr + path, // 请求路径
{ method: "POST" }, // 请求方式
Request.value, // 第三个参数,接收一个axios实例,这里我们将之前封装好的请求实例Request传入即可
{ immediate: false }, // 第四个参数,接收一个options对象,其中有一些用于配置axios请求的属性
);
// 将返回的信息暴露出去
return {
data,
/*
在这里再将useAxios返回的execute方法进行一次封装。
提供一个传入数据的接口,并将其与之前的数据postData拼接在一起。
*/
execute: (data) => execute({data: {...data, ...postData}}),
isLoading,
error,
...rest,
};
};
// Get请求封装
// 原理实际上与上面一样,这里就不提供例子了。
...
这样,我们就完成了对项目中get
、post
请求接口的统一。
这里说明一下为什么对于get
、post
请求,需要封装两份,即一个普通接口,一个基于useAxios方法的接口。
它们的区别在于:
- 调用普通的接口实现业务请求函数时,会直接在调用函数的同时发送请求,不会有复杂的逻辑流程,不会传递复杂的参数。因此,较为适合在较为简单的业务需求中使用,例如:列表页的列表数据请求。
- 而基于基于useAxios方法的接口则不同,调用该接口实现的业务请求函数时,其不会立即发送请求,而是返回一个对象,从中可以获取
data
、execute
、isLoading
、error
四个对象。而只有执行execute函数时,才会发送请求,并更新data
或error
对象。因此,在业务模块中,我们可以更加方便的发送请求、读取数据,更加适合复杂的业务场景。
但需要注意一点:用基于useAxios方法的接口实现的业务请求函数,只有在请求成功时,才会更新data
对象,请求失败时,会更新error
对象,而此时的data
对象仍为上一次请求成功后更新的data
!
完成以上两步的工作后,项目网络请求的基础就已经成型了,剩下的工作就是:
- 编写业务请求函数,并用文件的形式进行管理
- 根据业务需求对请求进行进一步的优化
这些内容我们在第三、四部分中继续进行说明。
3.封装业务请求函数,并用文件夹进行统一管理
在实际的项目开发中,我们一般会以文件为单位,将一个业务模块中的所有请求函数写在一个文件中,然后统一将这些文件放在一个指定的文件夹中。(当然,如果项目更为复杂的话,会进一步分层)
因此,结合上面两步的工作,项目的网络请求模块的基本结构就是下面这样:
---http
|
|---baseRequest.js
|---request.js
|---api.js
|---module
| |---login.js
| |---user.js
| |---xxx.js
| |---...
至于其中的每一个文件的编写,这里给出一个实例:
import { upgradedPost, commonPost } from "../api";
// xx相关接口
// 获取xx细节
export const getDiscount = (postData) =>
commonPost("/lqyp-cms-api/discount/getDiscount", {
...postData,
})
// 更新xx
export const updateDiscount = (postData) =>
upgradedPost("/lqyp-cms-api/discount/updateDiscount", {
...postData,
})
4.实际开发的最终解决方案
4.1 存在的问题🔍
实际上,在完成第三部分的工作后,项目的网络请求就可以实现了。
但是,在目前前端业务越来越复杂的环境下,网络请求的场景也会更加复杂,例如:
- 多次请求,每次请求时需要拿上一次请求返回的数据作为本次请求的参数
- 当某个值发生变化时,需要重新请求
- 当某个值满足了某个条件时,发送请求
- 某个请求函数需要进行防抖
- …
对于上述业务场景,如果使用简单的链式调用,即:
getData({...data},{...info}).then((res) => {
// ...
}).catch((error) => {
// ...
})
这样在整个业务模块中会有大量重复代码,代码可维护性大大降低!
而使用更为优雅的async/await
方式,似乎能够解决上述问题:
async/await
方式拿响应数据很方便✅- 第二个则可以将该值放入watch中进行监听,值发生变化时执行请求函数即可✅
- 第三个场景,可以在请求函数中加入条件判断✅
- 单独配置一个防抖函数,在请求函数中调用即可✅
- …
似乎都可以解决,但实际上,在一个业务模块中,会存在很多组件,其中都会有多个有相同或不同需求的请求,此时如果对每一个请求函数均进行以上操作,会导致大量的代码重复!
而第三部分中使用的 useAxios 并不支持这些功能,所以,单纯的 useAxios 并不能完全适用这些场景!
4.2 如何解决💡
因此,我们需要单独封装一个功能更为全面的 Hook,将请求的依赖、请求的判断条件、执行async/await
发送请求的方法等等都封装在一块儿使用,而用户只需要传递业务请求函数,和一个配置项,用于指明需要开启的功能即可。
这里给出一个简单的示例:
const useRequest = (service,option)=>{
// service就是传递进来的业务请求函数,option则是用户传递进来指明了功能需求的配置项
const data = ref()
// 真正发送请求的地方
const run = async ()=>{
const res = await service()
data.value = res
}
// 是否满足条件
if(option.ready){
run()
}
// 依赖重新请求
watch(option.deps,()=>{
run()
})
// 仅仅返回准备好的数据,和一个执行函数
return {
data,
run
}
}
而在业务模块中
<template>
<div>读取值:{{ data }}</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useRequest } from '..'
// 业务请求函数
import { getXXXData } from '..'
//...业务代码
const { data } = useRequest(
() => getXXXData(),
option: {
ready: 指明请求是否准备执行的对象
deps: [请求依赖的其他响应式对象]
}
)
</script>
在这块业务代码中,用户会直接拿到处理好的请求响应data
,它是一个响应式对象,所以业务层省去了一个声明响应式对象来保存数据的代码。
在传入的option
中,传入了ready
判断条件,业务层又被砍去一个条件判断函数;传入了依赖刷新deps
,砍去书写watch
的代码,用户只需要关注配置即可
同时,我们会发现它是通过run
方法调用触发请求的,那么,我们的防抖、节流函数只需要作用于 run 函数即可,所以这样的做法非常很简洁。
另外,这里的拓展性极强,你可以拓展loading
状态、params
、error
错误信息等出去。因为使用async/await
,所以我们封装的业务请求函数只需要返回一个promise对象即可,意味着 axios 和 request 这些都兼容,完全将请求这块的功能性的模块从业务层脱离,用户只需要关注配置和API接口!
4.3 最终的解决方案🎉
但是,这里的工作量是非常大的,我们没有必要一次次地造轮子,而工具库 Vueuse 中也没有很好地实现这些功能的模块,所以,这里我们选择使用一个Vue3的插件 — vue-hook-plus。
因此,我们最终的解决方案如下:
- 按照第一部分的方法,二次封装axios,构建基础请求类 baseRequest 和其实例。
- 按照第二部分的方法,统一项目中的
get
、post
请求。 - 按照第三部分的方法,封装业务请求函数返回Promise对象,并以文件为单位进行管理。
- 在业务层,使用请求中间层vue-hook-plus,用户仅需要传递对应的业务请求函数和配置项即可。