前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >小鹿线基础权限框架:web -- api 请求篇

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

作者头像
I Teach You 我教你
发布2023-07-18 18:07:08
4090
发布2023-07-18 18:07:08
举报
文章被收录于专栏:王二麻子IT技术交流园地

介绍

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

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

  1. 基本请求(二次封装 axios)
  2. 对于所有请求都会涉及到的内容进行统一封装(比如 loading,错误提示,登录过期等)
  3. 参数以及返回内容的处理(主要目的在于简化使用层,比如对于不同请求参数永远是普通对象,内部会根据具体情况进行具体的转换)
  4. 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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/08/29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 权衡
    • 基本使用
      • 参数处理
        • 多个地方使用
        • 设计上的思考
          • 基本请求
            • 公共的拦截处理
              • 参数和数据处理
                • API 列表
                • 扩展和维护
                相关产品与服务
                验证码
                腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档