【场景方案】如何去设计并二次封装一个好用的axios,给你提供一个另类写法,另加一些思考

文章介绍了在Vue3项目中如何对axios进行深度封装,包括文件结构设计、接口管理、请求和响应拦截器的配置,以及如何处理token超时和环境配置。此外,文章提到了使用typescript设置后端返回数据类型,并推荐了在React中使用第三方库ahooks简化异步请求处理。
摘要由CSDN通过智能技术生成

前言

以下演示基于vue3与element-plus,react可以仿造这个写,原理都是一样的。

之前在b站上的前端三十up主那里看来的,感觉写的挺有意思的。特此写篇文章记录下,还加上自己的一些思考。

我们平时封装的axios函数其实网上一搜一大把,但这个up给我们展示了一种“另类”的封装方法,我重新按自己想法重构了遍,来看看咋写的


设计

文件结构建议

封装的文件结构

在这里插入图片描述

把二次封装axios所有有关的代码全部放在request文件夹中,其中init.js是二次封装的初始配置,index.js是对init.js再进行一次功能拓展封装,我们真正使用时也是用的这个文件,utils.js是存放相关的工具函数,webConfig.js是一些全局的变量。

为什么这么设计:

  • 代码能够与其他功能解耦
  • 方便直接拷贝整个文件夹放到一个新的项目中直接使用

接口管理文件

在这里插入图片描述
接口都单独放在api文件夹中,并且以后端的业务服务模块进行分类。


二次封装axios的初始配置

init文件

也就是src/request/init.js文件:

// 配置全局得基础配置
import axios from "axios";

// 配置中心
import webConfig from "./webConfig.js"
// 需要的工具函数
import { isCheckTimeout } from "./utils";

// 相关库
import router from "@/router";
import store from "@/store";
import { ElMessage } from "element-plus";

let request = axios.create({
    //1,基础配置
    baseURL: process.env.VUE_APP_BASE_API, // process.env.VUE_APP_BASE_API就是我们在根目录创建对应的环境变量文件里的配置,
    timeout: 5000, // 设置超时时间为5s
})

// 设置请求拦截器
request.interceptors.request.use((config) => {
    // 1 token添加在请求头中,看看项目的token适合放在localStorage里还是vuex中
    let token = localStorage.getItem("token");
    // let token = store.getters.token;

    // 2 首先有些接口是不需要设置token的,那就不用设置token了
    let whiteList = webConfig.whiteListApi // 获取不需要设置token的接口白名单
    let url = config.url
    if (whiteList.indexOf(url) === -1 && token) {
        if (isCheckTimeout()) {
            // 超时了做登出操作
            store.dispatch("user/logout");
            return Promise.reject(new Error("token 失效"));
        }
        // 注入token到请求头中
        config.headers.Authorization = `Bearer ${token}`;
    }

    // 3 配置接口国际化
    config.headers["Accept-Language"] = store.getters.language;

    // 4 配置响应数据类型
    config.headers.responseType = "json"

    return config; // 必须返回配置
}, error => {
    return Promise.reject(new Error(error))
})

// 响应拦截器(处理返回的数据)
request.interceptors.response.use((res) => {
    //响应得统一处理
    let { status, message, data } = res.data; // status 这里的状态码是后端定义的,也就是每次请求都是200,但是后端根据情况返回401,500等状态码
    if (status === 401) {
        router.push("/noAuth")
        return Promise.reject(new Error(message)); // 抛出可以自定义错误提示
    }
    if (status !== 200) {
        ElMessage.error("错误码" + status + ":" + message);
        return Promise.reject(new Error(message));
    }
    return res.data; // res 里面其实有很多东西下面截图
}, error => {
    // 例如断网、跨域、状态码问题的报错
    ElMessage.error(error.message);
    // error.response.status 这里可以拿到接口真正的状态码,还是要看后端是怎么设计接口的
    return Promise.reject(new Error(error));
})

export default request;

其中响应数据res为:

在这里插入图片描述
响应失败的error内容有:

在这里插入图片描述

所需其他的文件

utils文件

utils.js:

/* 处理token的时效性 */

// 常量设置
// token 时间戳
export const TIME_STAMP = "timeStamp";
// token超时时长(毫秒) 两小时
export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000;

/**
 * 获取时间戳
 */
export function getTimeStamp() {
  return window.localStorage.getItem(TIME_STAMP);
}
/**
 * 设置时间戳(就是当前登陆时获取token的时间)
 */
export function setTimeStamp() {
    window.localStorage.setItem(TIME_STAMP, Date.now());
}
/**
 * 是否超时
 */
export function isCheckTimeout() {
  // 当前时间戳
  var currentTime = Date.now();
  // 缓存时间戳
  var timeStamp = getTimeStamp();
  return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE;
}

webConfig文件

webConfig.js:

export default {
    whiteListApi: ["/a", "/b"],
    pageSize: [20, 40, 80] // table的页码
}

第三次封装(重点)

首先要知道为什么还需要进行一次封装,因为单单只对axios做简单的初始化封装还是太过于简陋,未来随着业务的拓展,我们有可能会对接口请求的api做一些功能的拓展。

例如到了某个阶段,我们需要给接口返回的参数添加前端数据缓存。又到了某个阶段,需要阻止接口短时间内重复请求的功能等等。

这时候就很考验我们的代码设计了。

重点来了!!!!!!

推荐一种链式调用函数的方式去封装,举个例子:

function p() {
    let promise = Promise.resolve()

    function fn1(result) { // 功能封装1
        console.log('fn1');
        return Promise.resolve('fn1')
    }

    function fn2(result) { // 功能封装2
        console.log('fn2');
        return Promise.resolve('fn2')
    }

    let arr = [fn1, fn2]

    while (arr.length) {
        promise = promise.then(arr.shift())
    }

    return promise
}

p('1') // fn1 fn2 轮流执行

好好研究你就会发现,我们是相当于把一个一个功能封装函数当做是一个节点,只需要把传入项经过节点处理后,就得到一个封装过的方法。

按照这个思路我们来试试 index.js 怎么写

import request from "./init";

import { ElLoading } from 'element-plus'

// 要点 1 把同步的方法也统一以异步调用处理,保证所有方法的顺序 2 可通过配置关闭制定节点处理

// 默认配置
//{ 
//     nendCache: false, // 是否直接拿取接口的上一个缓存数据(谨慎开启)
//     nendLoading: true, // 全局加载
//     needRequestingOnlyOne: false, // 是否开启,当前接口在请求时,禁止重复请求的情况(谨慎开启),因为有时候我们需要并发请求
// }

let requestApi = (function () {
    let mapCache = new Map(); // 接口返回的数据缓存
    let requestingUrl = []; // 正在发送的请求
    let loadingInstance = null // 全局加载组件

    return function (config) {
        let { url, nendCache, nendLoading, needRequestingOnlyOne } = config
        let promise = Promise.resolve() // 获取一个成功状态的promise

        // 节点:全局加载
        function nodeLoading() {
            if (nendLoading === undefined || nendLoading === true) {
                loadingInstance = ElLoading.service({ fullscreen: true })
            }
            return Promise.resolve({ keepGoing: true, type: 'then' })
        }

        // 节点:是否直接拿取接口的上一个缓存数据
        function nodeCache() {
            if (nendCache === true) {
                // 如果之前已经请求过了,就直接返回缓存的,下面的节点都不用走了ß
                if (mapCache.has(url)) {
                    if (loadingInstance) loadingInstance.close() // 关闭全局加载
                    return Promise.resolve({ keepGoing: false, type: 'then', data: mapCache.get(url) })
                } else {
                    return Promise.resolve({ keepGoing: true, type: 'then' })
                }
            }
            return Promise.resolve({ keepGoing: true, type: 'then' })
        }

        // 节点:接口还在请求过程中,又发了一次,阻止这种情况
        function nodeRequestingOnlyOne() {
            if (needRequestingOnlyOne === undefined || needRequestingOnlyOne === true) {
                // 看看是不是在请求中
                if (requestingUrl.indexOf(url) !== -1) {
                    return Promise.resolve({ keepGoing: false, type: 'catch', data: '请求已经提交' })
                } else {
                    return Promise.resolve({ keepGoing: true, type: 'then' })
                }
            }
            return Promise.resolve({ keepGoing: true, type: 'then' })
        }

        // 节点:发起请求
        async function nodeRequest() {
            let resData = await request({ ...config }) // 发送请求,等待获取数据
            return Promise.resolve({ keepGoing: true, type: 'then', data: resData.data })
        }
		
		// 添加新节点

        // 节点:请求完成后的收尾工作
        function nodeRequestedFinalDo(result = { keepGoing: true }) {

            // 需要抛出错误的情况
            if (result.type === 'catch') {
                return Promise.reject(result.data)
            }

            // 剔除掉正在请求中的接口的标识
            requestingUrl = requestingUrl.filter(item => {
                if (item !== url) {
                    return item
                }
            })

            // 写入接口缓存数据
            mapCache.set(url, result.data)

            // 关闭全局加载
            if (loadingInstance) loadingInstance.close()

            return Promise.resolve(result.data)
        }

        let _handleArr = [nodeLoading, nodeCache, nodeRequestingOnlyOne, nodeRequest, nodeRequestedFinalDo] // 需要经过处理的节点

        function startNodeList() {
            while (_handleArr.length > 0) {
                let fn = _handleArr.shift() // 获取当前节点
                function _final(result = { keepGoing: true }) { // keepGoing 为false表示当前节点不执行,跳到下一个节点
                    if (!result.keepGoing) { // 后面keepGoing为false的节点都走这里
                        return Promise.resolve(result.data || result) // 如果是中途有keepGoing为false的需要取data
                    }
                    // keepGoing为true节点都走这里
                    return fn.call(this, result)
                }
                promise = promise.then(_final)
            }
        }
        startNodeList()
        return promise
    }
})()
export {
    requestApi as request, // 进过三次封装的
    request as initRequest // 不进过第三次封装的
}

这片代码对js的基础要求较高,属于高阶用法了(时间久了我估计也不会写了哈哈),多看看!


环境配置

之前写init.js的时候,需要配置接口请求基本地址的环境变量。需要在目录中创建:

在这里插入图片描述
然后分别为

# 标志
ENV = 'development'

# base api
VUE_APP_BASE_API = '/api'
# 标志
ENV = 'production'

# base api
VUE_APP_BASE_API = 'https://mock.mengxuegu.com/mock/625c05ab66abf914b1f1bf10/crm'

使用例子

ok都写完后就可以使用了,在api/user.js中举例子:

import { initRequest, request } from "../request"

// 用二次封装的get请求,参数以对象的形式传入,axios会自动帮我们拼接在请求地址中
export const getUsersAllType = (params) => { 
    return initRequest({
        url: "/usersAllType",
        method: "get",
        params: {
            ...params
        },
    })
}

// 用三次封装的post请求
export const getUserProfile = (params) => { 
    return request({
        url: "/profile",
        method: "post",
        params: {
            ...params
        },
        needRequestingOnlyOne: true // 开启具体某项功能
    })
}

建议

如果你想要让你的js功底再迈向进阶一小步的话,可以尝试一下,但是如果平时项目时间很紧,还是用你最熟悉的方式去封装吧,私底下再去研究哈哈。

如果你用react,那么恭喜你可以直接借助第三方库,也能有很好的体验。


借助第三方库就不用封三层了(推荐)

在react中,可以直接考虑把二次封装的异步请求结合hooks去使用,例如第三方hook库ahooks,里面有个useRequest的hook,可以把异步请求放里面。然后能够返回很多状态去做判断。

例如官方上的一个例子:

const { loading, run, runAsync } = useRequest(service, {
  manual: true
});

<button onClick={run} disabled={loading}>
  {loading ? 'Loading' : 'Edit'}
</button>

你看如果我们要实现按钮的请求中防止点击,直接能通过返回的loading变量判断了,不用自己再去维护一个响应式的变量。

还有很多细节可以参考官方文档:ahooks

真的非常推荐!!!! 这样你就不用撒费苦心的二次封装那么多功能了。


结合TS后怎么设置后端返参类型

这是我之前学ts时候最好奇的问题了,好在也看到了答案!

// response 拦截:统一处理 errno 和 msg
instance.interceptors.response.use(res => {
  const resData = (res.data || {}) as ResType
  const { errno, data, msg } = resData

  if (errno !== 0) {
    // 错误提示
    if (msg) {
      message.error(msg)
    }

    throw new Error(msg)
  }

  return data as any
})

export default instance

// 后端返回的大对象最外层类型,这根据公司实际情况定制
export type ResType = {
  errno: number
  data?: ResDataType
  msg?: string
}
// 对于拿不准的data数据,用这个写法
export type ResDataType = {
  [key: string]: any
}

api这样写:

import axios, { ResDataType } from '../request/init'

// 获取用户信息
export async function getUserInfoService(): Promise<ResDataType> {
  const url = '/api/user/info'
  const data = (await axios.get(url)) as ResDataType
  return data
}

完美,如果后端返回的大对象多了字段没关系,此时浏览器上运行的已经是js代码,不会报错啥的。


token无感知刷新方案

我放在另一篇文章里了,可以参考下:【场景方案】关于前端对接口行为的控制合集:轮番查询、并发请求、服务端通知、token无感刷新

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Axios一个基于 Promise 的 HTTP 请求库,它可以用于浏览器和 Node.js 环境中。Axios 既支持简单的 GET 请求,也支持复杂的 RESTful API 请求。为了更方便地使用 Axios,我们可以对其进行二次封装。 以下是一个简单的 Axios 二次封装示例: ```javascript import axios from 'axios'; // 创建一个 Axios 实例 const instance = axios.create({ baseURL: 'http://api.example.com', // 设置请求的 base URL timeout: 10000, // 设置请求超时时间 }); // 请求拦截器 instance.interceptors.request.use( (config) => { // 在发送请求之前做些什么 // 添加 token 等操作 return config; }, (error) => { // 对请求错误做些什么 return Promise.reject(error); } ); // 响应拦截器 instance.interceptors.response.use( (response) => { // 对响应数据做点什么 const data = response.data; if (data.code !== 200) { // 处理错误 alert(data.msg); return Promise.reject(data); } return data.data; }, (error) => { // 对响应错误做点什么 alert(error.message); return Promise.reject(error); } ); export default instance; ``` 在以上代码中,我们首先通过 `axios.create` 方法创建了一个 Axios 实例,然后对其进行了一些配置,例如设置了请求的 `baseURL` 和超时时间。 接着,我们分别定义了请求拦截器和响应拦截器。请求拦截器可以在发送请求之前,对请求进行一些处理,例如添加 token 等操作。而响应拦截器则可以对响应数据进行一些处理,例如统一处理错误信息。 最后,我们将封装好的 Axios 实例导出,以便在其他模块中使用。 在实际项目中,我们可以根据具体需求对以上代码进行修改和扩展,以满足不同的请求场景
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值