介绍
本篇介绍的是 web 端的 API 封装层,该封装的内容位于src/share/request/basic
下
从整体的封装到使用,大致可以分为4层,或者3.5层,具体内容如下
- 基本请求(二次封装 axios)
- 对于所有请求都会涉及到的内容进行统一封装(比如 loading,错误提示,登录过期等)
- 参数以及返回内容的处理(主要目的在于简化使用层,比如对于不同请求参数永远是普通对象,内部会根据具体情况进行具体的转换)
- API 列表(简化配置以及让使用更加简洁)
最终用起来的感觉如下
代码语言:javascript复制async function init() {
//get
await ApiGetRequest({ a: 1 })
//post
await ApiPostRequest({ a: 1 })
//上传
await ApiFormRequest({ a: file })
//流相关,比如验证码
await ApiStreamRequest({ a: 1 })
}
权衡
对于目前的架子来说,以上这些操作显然是属于过度封装了,复杂度也一下子上去了,但是从长远角度,或者是可持续发展的角度来考虑,或许这是一笔比较的划算的权衡也说不定
因为对于前端来说,API请求是整体架构中很重要的组成部分,前端只是展示层,数据源唯一的来源就是服务端的接口,而和接口打交道的就是请求封装相关的逻辑了,封装的质量如何,将会直接决定在使用时的复杂度,舒适度和间接性,尤其是对于大型应用来说,情况会更加复杂一些
我们先来看些和常见的,在 vue
中二次封装 axios后的使用对比demo
基本使用
代码语言:javascript复制//平常
import { $http } from "api"
function request1(data) {
this.loading = true
$http({
url: "url",
method: "get",
data
})
.then(res => {
//dosoming..
})
.catch(err => {
this.$message.error(err.msg)
})
.finally(() => {
this.loading = false
})
}
//鹿线框架
import { ApiGetRouters } from "@api"
async function request2(data) {
//这里自动处理 loading,出错后的错误提示,用起来只有一行
const res = await ApiGetRoutes(data)
}
参数处理
代码语言:javascript复制//平常
import { $http } from "api"
function request1(data) {
//处理 get
$http({
url: "url",
method: "GET",
params: data
})
//处理pist
$http({
url: "url",
method: "POST",
data: JSON.stringify(data)
})
//处理上传
$http({
url: "url",
method: "POST",
headers: { "Content-Type": null },//axios的坑,不解释
data: buildFormData(data)//需要将参数转成 formData,这里用一个方法来省略
})
}
//鹿线框架
function request2(data) {
/* 所有内容都是一行,所有使用都是一个对象,没有参数可以不传,内部会自动转格式 */
//get
await ApiGetRequest({ a: 1 })
//post
await ApiPostRequest({ a: 1 })
//上传
await ApiFormRequest({ a: file })
//流相关,比如验证码
await ApiStreamRequest({ a: 1 })
}
多个地方使用
如果多个地方使用时,按照平常的方式,这需要每个地方写一份,会存在许多重复性的模板式的代码,虽然可以为了简化,在统一封装下
代码语言:javascript复制//文件 A
export function getRequest(data) {}
export function postRequest(data) {}
但这样也有问题,因为虽然简化了在使用的模板化代码,但是这只处理了参数,比如 loading 是否开启,错误自动处理等等
设计上的思考
上边列举了一些例子,做了一些对比,想要表达的最核心的思想是,仅仅是普普通通的二次封装的程度是不够的!!!因为会存在相当量的模板式重复代码,以及代码耦合度高,在请求中其实会涉及多得多的问题,比如以下这些
- 自动挂上 token
- 接口异常处理
- 参数处理
- 返回值的预处理(比如流转base64)
- 登录过期
- 内容缓存
- 等…
这些内容还只是能提前做处理的课预测问题,其他的诸如涉及到业务的就得视具体情况而定了,对于这些问题,因为需求是未知的,我们能做的就只有给未来留些余地,让扩展起来能更容易些而已。就算是想要把大部分已知问题都进行处理,可能还得用上一些第三方库来管理,所以
封装固然重要,但是怎么封装,怎么拆解,封到什么程度,这些都是需要考虑和权衡的
而本框架代码层面封装的维度有4个,即开头介绍所列举的四条
基本请求
这部分主要是用来管理公共请求部分的,它和常规的二次封装 axios 作用一样用来统一设置
- 请求的 URL
- 请求头
- 请求超时
- 请求自动挂载 token
- 如果有其他需求的话,就则需设置即可
这部分应该是没有任何异议的
代码语言:javascript复制export const BASIC_CONFIG = {
baseURL: import.meta.env.$BASIC_BASE_URL,
headers: {
"Content-Type": "application/json"
},
timeout: 10000
}
export const request = axios.create(BASIC_CONFIG)
// 挂 token
request.interceptors.request.use(
config => {
if (localStorage.getItem("token")) {
config.headers.Authorization = localStorage.getItem("token")
}
return config
},
err => Promise.reject(err)
)
公共的拦截处理
这部分主要目的是,为了把真实请求,和使用层隔开,即当我们调用方法去请求时,如果想要做一些拦截处理的话,就可以在这里进行处理
之所以不用 axios 自带的拦截器的主要原因在于,不自由,因为的控制权其实是在 axios 那里的
又因为 axios 是基于 promise
封装来的,所以利用 promise
的特性,对于后置拦截,我们只需要去不断的 .then
挂处理函数,就可以以扁平的方式来进行扩展,对于前置拦截就直接写在调用实际请求函数之前即可
本框架只做了如下几方面事
- loading
- 错误提示
- 登录过期(过期要弹框,这里还除了多个请求引发的冲突问题)
- 请求闪屏问题
- 流处理的一部分
之所以没有干别的,是因为对于一般项目来说就已经是完全够用了,如有需要,只需要在认清是前置还是后置后,在对应的地方写逻辑即可
代码语言:javascript复制/*
普通请求包装器,用于包装普通请求,做一些所有请求的统一的处理
*/
export function basicRequestWrapper(options, { loading = true }) {
const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
return request(options)
.then(res => {
const { code, msg, data } = res.data
if (code === "200") {
return data
}
if (["50001", "50002", "50003"].includes(code)) {
if (hasToLoginBox) {
return
}
Loading.closeAll()
hasToLoginBox = true
return ElMessageBox.confirm(
"登录状态已过期,点击确定按钮去重新登录。",
"系统提示",
{
type: "error",
confirmButtonText: "确认",
showCancelButton: false
}
).then(() => {
localStorage.removeItem("token")
hasToLoginBox = false
window.location.href = "/login"
})
}
if (msg) {
ElMessage({ type: "error", message: msg })
}
throw res.data
})
.finally(() => _loading.close())
}
/*
二进制流包装器,适用于文件下载,图片等
*/
/**
* @param {import("axios").AxiosRequestConfig} options
* @param {{ loading: boolean }}
*/
export function streamRequestWrapper(options, { loading, fileType }) {
const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
return request({ ...options, responseType: "arraybuffer" })
.then(res => {
const { data, headers } = res
if (
typeof data === "object" &&
data.code &&
["50001", "50002", "50003"].includes(data.code)
) {
if (hasToLoginBox) {
return
}
Loading.closeAll()
hasToLoginBox = true
return ElMessageBox.confirm(
"登录状态已过期,点击确定按钮去重新登录。",
"系统提示",
{
type: "error",
confirmButtonText: "确认",
showCancelButton: false
}
).then(() => {
localStorage.removeItem("token")
hasToLoginBox = false
window.location.href = "/login"
})
}
return { data, headers }
})
.finally(() => _loading.close())
}
这里以包装器的方式写了两份,里面不乏有重复的部分,但这是合理的,basicRequestWrapper
专门为了普通请求用的,streamRequestWrapper
则是处理流的,以后可能还会有处理一些其他分类的请求拦截,这么拆分可以很好的区分类型以进行解耦,方便以后的扩展
参数和数据处理
这里没什么好说的,只是做了参数的处理,以及处理了另一部分的流的内容
代码语言:javascript复制//get
export const getRequest = ({ url, data = {}, loading }) => {
return basicRequestWrapper(
{
url: url resolveURLQuery(data),
method: "GET"
},
{ loading }
)
}
//post
export const postRequest = ({ url, data = {}, loading }) => {
return basicRequestWrapper(
{ url, data: JSON.stringify(data), method: "POST" },
{ loading }
)
}
//上传
export const formRequest = ({ url, data = {}, loading }) => {
return basicRequestWrapper(
{
url,
data: Object.entries(data).reduce((data, [key, value]) => {
Array.isArray(value)
? value.forEach(file => data.append(key, file))
: data.append(key, value)
return data
}, new FormData()),
method: "POST",
headers: { "Content-Type": null }
},
{ loading }
)
}
//下载
export const fileRequest = ({ url, data = {}, loading }) => {
return streamRequestWrapper(
{ url: url resolveURLQuery(data) },
{ loading }
).then(({ data, headers }) => {
return new Promise(resolve => {
const type = headers["content-type"].split(";")[0].trim()
const blob = new Blob([data], { type })
let reader = new FileReader()
reader.onload = function (e) {
resolve(e.target.result)
}
reader.readAsDataURL(blob)
})
})
}
API 列表
这一步是为了简化使用而做的抽象
因为一个请求可能会被多个文件所使用,所以如果正常写则需要在每个地方都写上 url
对于所有请求来说,可能有的需要开启加载动画,而有的就不需要,所以需要一些配置化的能力,如果正常写也要写的到处都是
所以,这就是意义所在
代码语言:javascript复制//获取图形验证码
export const ApiCaptchaImageCode = data => {
return fileRequest({
url: "/captcha/imageCode",
loading: false,
data
})
}
//登录
export const ApiLogin = data => {
return postRequest({
url: "/u/loginByJson",
data,
loading: true
})
}
扩展和维护
通过上面一步步的过程和思考,会发现对于所有基本情况都做了分类,然后以此为维度进行了拆分,但是未来是未知的,你永远也猜不透产品哪天冒出来的新奇想法
为了产品能更好的长久维护下去,请在以上的思想基础上进行按需添加
- 比如哪天要处理某些特殊的业务请求,请在公共拦截除,新增拦截策略,然后让外部选择性的去使用
- 比如哪天要支持根据业务,要按需请求不同的服务器了,请把 basic 目录粘需要的份数新的出来,这样做将会存在大量的重复代码,但是代价其实是值的!!这里不妨思考一下,为什么会需要请求不同的服务器?抛开业务只谈技术而言,这意味着不同的后端服务,所以你就不能指望不同的服务的使用方式,会是和当前一样的,即便他们目前可能一模一样,所以代价是值得的
请不要因为觉得麻烦而想要省事而简化某些步骤,或者是省略某些步骤,尤其是对于多人合作的项目而言
因为最初的构建和写代码代价是很低的,但是后期的维护和弥补问题的代价是巨大的,因为你不知道你的一个改动的影响范围会有多广,那么将只能继续错上加错的缝缝补补直至成为人人骂的屎山
日期: 2022-08-29
作者: @gxs/usagisah
(gxs是顾弦笙的缩写,顾弦笙 和 usagisah 是我全网通用的网名)
邮箱: 1286791152@qq.com
(有问题欢迎邮箱发问题给我)
github: https://github.com/gxs114