【工程化】如何搭建一个项目的网络请求

如何搭建一个项目的网络请求

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();

接下来,就可以使用这个对象,对基础的getpost请求进行统一封装。

2.封装基础请求函数,对外统一getpost方法

在 api.js 文件中,对具体业务请求函数中所使用的基础请求函数进行封装。

基础请求函数,实际上是指在业务请求函数中,往往不会直接进行用 async/await 语法调用axiosgetpost方法,设置相关配置并发送请求这些工作。因为如果这样的话,每一个业务请求函数中,都需要写一次类似的代码,这样会增加工作量,而且代码维护性也会降低。

因此,我们需要单独建立一个文件 api.js ,并在其中统一getpost请求的方法。

下面给出一个实例:

// 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请求封装
// 原理实际上与上面一样,这里就不提供例子了。
...

这样,我们就完成了对项目中getpost请求接口的统一。

这里说明一下为什么对于getpost请求,需要封装两份,即一个普通接口,一个基于useAxios方法的接口

它们的区别在于:

  • 调用普通的接口实现业务请求函数时,会直接在调用函数的同时发送请求,不会有复杂的逻辑流程,不会传递复杂的参数。因此,较为适合在较为简单的业务需求中使用,例如:列表页的列表数据请求。
  • 而基于基于useAxios方法的接口则不同,调用该接口实现的业务请求函数时,其不会立即发送请求,而是返回一个对象,从中可以获取dataexecuteisLoadingerror四个对象。而只有执行execute函数时,才会发送请求,并更新dataerror对象。因此,在业务模块中,我们可以更加方便的发送请求、读取数据,更加适合复杂的业务场景。

但需要注意一点:用基于useAxios方法的接口实现的业务请求函数,只有在请求成功时,才会更新data对象,请求失败时,会更新error对象,而此时的data对象仍为上一次请求成功后更新的data

完成以上两步的工作后,项目网络请求的基础就已经成型了,剩下的工作就是:

  1. 编写业务请求函数,并用文件的形式进行管理
  2. 根据业务需求对请求进行进一步的优化

这些内容我们在第三、四部分中继续进行说明。

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状态、paramserror错误信息等出去。因为使用async/await,所以我们封装的业务请求函数只需要返回一个promise对象即可,意味着 axios 和 request 这些都兼容,完全将请求这块的功能性的模块从业务层脱离,用户只需要关注配置和API接口

4.3 最终的解决方案🎉

但是,这里的工作量是非常大的,我们没有必要一次次地造轮子,而工具库 Vueuse 中也没有很好地实现这些功能的模块,所以,这里我们选择使用一个Vue3的插件 — vue-hook-plus

因此,我们最终的解决方案如下:

  1. 按照第一部分的方法,二次封装axios,构建基础请求类 baseRequest 和其实例
  2. 按照第二部分的方法,统一项目中的getpost请求
  3. 按照第三部分的方法,封装业务请求函数返回Promise对象,并以文件为单位进行管理。
  4. 在业务层,使用请求中间层vue-hook-plus,用户仅需要传递对应的业务请求函数配置项即可。
补充:vue-hook-plus 文档

VueHook Plus | VueHook Plus (gitee.io)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值