您的位置 首页 >  博文

小鹿线基础权限框架:web -- api 请求篇

介绍

本篇介绍的是 web 端的 API 封装层,该封装的内容位于src/share/request/basic

从整体的封装到使用,大致可以分为4层,或者3.5层,具体内容如下

  1. 基本请求(二次封装 axios)

  2. 对于所有请求都会涉及到的内容进行统一封装(比如 loading,错误提示,登录过期等)

  3. 参数以及返回内容的处理(主要目的在于简化使用层,比如对于不同请求参数永远是普通对象,内部会根据具体情况进行具体的转换)

  4. API 列表(简化配置以及让使用更加简洁)

最终用起来的感觉如下

async function init({
  //get
  await ApiGetRequest({ a1 })

  //post
  await ApiPostRequest({ a1 })

  //上传
  await ApiFormRequest({ a: file })

  //流相关,比如验证码
  await ApiStreamRequest({ a1 })
}

权衡

对于目前的架子来说,以上这些操作显然是属于过度封装了,复杂度也一下子上去了,但是从长远角度,或者是可持续发展的角度来考虑,或许这是一笔比较的划算的权衡也说不定

因为对于前端来说,API请求是整体架构中很重要的组成部分,前端只是展示层,数据源唯一的来源就是服务端的接口,而和接口打交道的就是请求封装相关的逻辑了,封装的质量如何,将会直接决定在使用时的复杂度,舒适度和间接性,尤其是对于大型应用来说,情况会更加复杂一些

我们先来看些和常见的,在 vue中二次封装 axios后的使用对比demo

基本使用

//平常
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)
}

参数处理

//平常
import { $http } from "api"
function request1(data{
  //处理 get
  $http({
    url"url",
    method"GET",
    params: data
  })

  //处理pist
  $http({
    url"url",
    method"POST",
    dataJSON.stringify(data)
  })

  //处理上传
  $http({
    url"url",
    method"POST",
    headers: { "Content-Type"null },//axios的坑,不解释
    data: buildFormData(data)//需要将参数转成 formData,这里用一个方法来省略
  })
}

//鹿线框架
function request2(data{
  /* 所有内容都是一行,所有使用都是一个对象,没有参数可以不传,内部会自动转格式 */
    //get
  await ApiGetRequest({ a1 })

  //post
  await ApiPostRequest({ a1 })

  //上传
  await ApiFormRequest({ a: file })

  //流相关,比如验证码
  await ApiStreamRequest({ a1 })
}

多个地方使用

如果多个地方使用时,按照平常的方式,这需要每个地方写一份,会存在许多重复性的模板式的代码,虽然可以为了简化,在统一封装下

//文件 A
export function getRequest(data{}
export function postRequest(data{}

但这样也有问题,因为虽然简化了在使用的模板化代码,但是这只处理了参数,比如 loading 是否开启,错误自动处理等等

设计上的思考

上边列举了一些例子,做了一些对比,想要表达的最核心的思想是,仅仅是普普通通的二次封装的程度是不够的!!!因为会存在相当量的模板式重复代码,以及代码耦合度高,在请求中其实会涉及多得多的问题,比如以下这些

  • 自动挂上 token

  • 接口异常处理

  • 参数处理

  • 返回值的预处理(比如流转base64)

  • 登录过期

  • 内容缓存

  • 等…

这些内容还只是能提前做处理的课预测问题,其他的诸如涉及到业务的就得视具体情况而定了,对于这些问题,因为需求是未知的,我们能做的就只有给未来留些余地,让扩展起来能更容易些而已。就算是想要把大部分已知问题都进行处理,可能还得用上一些第三方库来管理,所以

封装固然重要,但是怎么封装,怎么拆解,封到什么程度,这些都是需要考虑和权衡的

而本框架代码层面封装的维度有4个,即开头介绍所列举的四条

基本请求

这部分主要是用来管理公共请求部分的,它和常规的二次封装 axios 作用一样用来统一设置

  • 请求的 URL

  • 请求头

  • 请求超时

  • 请求自动挂载 token

  • 如果有其他需求的话,就则需设置即可

这部分应该是没有任何异议的

export const BASIC_CONFIG = {
  baseURLimport.meta.env.$BASIC_BASE_URL,
  headers: {
    "Content-Type""application/json"
  },
  timeout10000
}

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

  • 错误提示

  • 登录过期(过期要弹框,这里还除了多个请求引发的冲突问题)

  • 请求闪屏问题

  • 流处理的一部分

之所以没有干别的,是因为对于一般项目来说就已经是完全够用了,如有需要,只需要在认清是前置还是后置后,在对应的地方写逻辑即可

/* 
  普通请求包装器,用于包装普通请求,做一些所有请求的统一的处理
*/

export function basicRequestWrapper(options, { loading = true }{
  const _loading = loading ? Loading.service({ locktrue }) : { 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"确认",
            showCancelButtonfalse
          }
        ).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({ locktrue }) : { 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"确认",
            showCancelButtonfalse
          }
        ).then(() => {
          localStorage.removeItem("token")
          hasToLoginBox = false
          window.location.href = "/login"
        })
      }

      return { data, headers }
    })
    .finally(() => _loading.close())
}

这里以包装器的方式写了两份,里面不乏有重复的部分,但这是合理的,basicRequestWrapper 专门为了普通请求用的,streamRequestWrapper 则是处理流的,以后可能还会有处理一些其他分类的请求拦截,这么拆分可以很好的区分类型以进行解耦,方便以后的扩展

参数和数据处理

这里没什么好说的,只是做了参数的处理,以及处理了另一部分的流的内容

//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, dataJSON.stringify(data), method"POST" },
    { loading }
  )
}

//上传
export const formRequest = ({ url, data = {}, loading }) => {
  return basicRequestWrapper(
    {
      url,
      dataObject.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

对于所有请求来说,可能有的需要开启加载动画,而有的就不需要,所以需要一些配置化的能力,如果正常写也要写的到处都是

所以,这就是意义所在

//获取图形验证码
export const ApiCaptchaImageCode = data => {
  return fileRequest({
    url"/captcha/imageCode",
    loadingfalse,
    data
  })
}
//登录
export const ApiLogin = data => {
  return postRequest({
    url"/u/loginByJson",
    data,
    loadingtrue
  })
}

扩展和维护

通过上面一步步的过程和思考,会发现对于所有基本情况都做了分类,然后以此为维度进行了拆分,但是未来是未知的,你永远也猜不透产品哪天冒出来的新奇想法

为了产品能更好的长久维护下去,请在以上的思想基础上进行按需添加

  • 比如哪天要处理某些特殊的业务请求,请在公共拦截除,新增拦截策略,然后让外部选择性的去使用

  • 比如哪天要支持根据业务,要按需请求不同的服务器了,请把 basic 目录粘需要的份数新的出来,这样做将会存在大量的重复代码,但是代价其实是值的!!这里不妨思考一下,为什么会需要请求不同的服务器?抛开业务只谈技术而言,这意味着不同的后端服务,所以你就不能指望不同的服务的使用方式,会是和当前一样的,即便他们目前可能一模一样,所以代价是值得的

请不要因为觉得麻烦而想要省事而简化某些步骤,或者是省略某些步骤,尤其是对于多人合作的项目而言

因为最初的构建和写代码代价是很低的,但是后期的维护和弥补问题的代价是巨大的,因为你不知道你的一个改动的影响范围会有多广,那么将只能继续错上加错的缝缝补补直至成为人人骂的屎山

日期: 2022-08-29

作者: @gxs/usagisah (gxs是顾弦笙的缩写,顾弦笙 和 usagisah 是我全网通用的网名)

邮箱: 1286791152@qq.com(有问题欢迎邮箱发问题给我)

github: https://github.com/gxs114


关于作者: 王俊南(Jonas)

昨夜寒蛩不住鸣。惊回千里梦,已三更。起来独自绕阶行。人悄悄,帘外月胧明。 白首为功名。旧山松竹老,阻归程。欲将心事付瑶琴。知音少,弦断有谁听。

热门文章