Vite + Vue3 封装 Axios 并做防重复提交 同一接口 [请求未返回结果、2秒内禁止重复提交](超详细)

一、axios ⚡️ ⚡️ ⚡️

1、是什么 ❓ ❓ ❓

axios是一个基于Promise的HTTP网络请求库,可以用于浏览器和node.js。在服务端它使用原生node.js http模块, 而在客户端 (浏览端) 则使用XMLHttpRequest。它可以帮助我们更轻松、简单地发出 AJAX 请求。

2、为什么选择它 🙋 🙋 🙋

1、它支持Promise API,能够处理异步请求,降低了回调地狱的问题。

2、它支持取消请求,可以在请求未完成时取消请求,避免浪费资源。

3、高开发效率,简化代码逻辑。

4、它提供了拦截器可以拦截请求和响应,可以在发送请求或接收响应之前对它们进行拦截和修改,从而实现统一处理或添加公共参数等功能。比如我们要做的同一接口防重复提交功能。

二、安装axios 💪 💪 💪

这里我的项目已经提前搭建好了,还没有搭建的小伙伴可以看看👉👉Vite4+Pinia2+vue-router4+ElmentPlus搭建Vue3项目(组件、图标等按需引入)

yarn add axios

三、axios的使用方法 😜 😜 😜

1、创建一个axios实例的方法如下 👇 👇

import axios from 'axios';

const instance = axios.create({
  // 你的api地址 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  baseURL: 'http:********/api/',
  // 请求超时的毫秒数(0 表示无超时时间)
  timeout: 1000,
  // 自定义请求头
  headers: {'X-Custom-Header': 'foobar'}
});

2、拦截器的使用 👇 👇

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

3、使用 cancel token 取消请求 👇 👇

可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// cancel the request
cancel();

四、封装axios 👍 👍 👍

为什么还要封装axios,因为它默认情况下不知道我们的业务需求,所以封装它最大的目的是适应项目需求。其次封装它可以将重复的代码抽象成一个方法,减少代码量,提高代码的可读性和可维护性以及复用性。统一处理响应错误、全局loading、设置请求头、请求超时处理等。

1、新建文件src/utils/http/request.ts 👇 👇

代码中基本上该注释的地方我都有写注释,因为我这里是demo,有些代码我并没有去实现。比如判断用户有没有登录,将X-Access-Token添加到每个请求等,都会在相应的位置写上注释,自己根据自己项目的需求去完成就好。

import axios, {AxiosError, AxiosInstance, AxiosResponse, Canceler, InternalAxiosRequestConfig} from 'axios';
import {errorCodeType} from "@/utils/http/error-code-type";


export class Interceptors {
    requestQueue: {
        createTime: number
        url: string
        method: string
        cancel: Canceler
    }[] = [];
    instance: AxiosInstance;
    constructor() {
        this.instance = axios.create({
            // 你的api地址 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
            // 这里的话我的baseURL是存放在项目环境变量文件中
            // vite获取env环境变量的方式是import.meta.env
            baseURL: import.meta.env.VITE_APP_BASE_API,
            // 请求超时的毫秒数(0 表示无超时时间)
            timeout: import.meta.env.VITE_APP_PREVENT_DUPLICATE_SUBMISSIONS
        });
        this.init();
    }
    init() {
        // 添加请求拦截器
        this.instance.interceptors.request.use(
            (config:InternalAxiosRequestConfig) => {
                // 在这里的话你就可以去设置自己的请求头
                // 比如用户登录以后就能拿到一个token 这个token比如格式是data: {token: '*****'}
                // if (data.token) {
                //     config.headers['Authorization'] = `Bearer ${data.token}`
                //     config.headers['X-Access-Token'] = data.token
                // }

                // 防止一个接口在没有响应之前进行重新提交即重复提交验证,默认不校验 duplicateRequestValidation为true时开启
                if (config.url && config.duplicateRequestValidation) {
                    this.removeRequestQueue(config)
                    this.addRequestQueue(config)
                }
                return config;
            },
            (error) => {
                // 对请求错误做些什么 直接抛出错误
                Promise.reject(error)
            }
        );
        // 添加响应拦截器
        this.instance.interceptors.response.use((response: AxiosResponse) => {
            // 在这里的话你就可以去处理你响应成功的自定义逻辑

            // 根据后端返回的code值。比如约定的是20000代表登录过期
            // const res: any = response.data // 获取响应值
            // if (res.code === 20000) {
            //     // 清楚token 跳转登录页面
            // }

            // 比如10000表示请求成功,约定40000~50000不做拦截
            // const filterCode = Math.abs(parseInt(res.code)) >= 40000 && Math.abs(parseInt(res.code)) < 50000
            // if (res.code !== 10000 && !filterCode) {
            //     // 这里去处理请求失败的逻辑
            // } else {
            //     return response.data
            // }

            this.removeRequestQueue(response.config)
            return response.data;
        },(error: AxiosError) => {
            // 对响应错误做点什么
            // 一般响应错误后端都会返回一个错误码和错误信息比如404 401等等
            // 为了让用户更能直观的知道是什么原因  你可以把常见的错误做一个转换然后提示一下 404就是访问资源不存在,401就是没有权限等等
            // 我演示的接口使用的是http://www.7timer.info/全球天气预测系统的接口

            // 判断重复提交
            // 转换错误编码为文字 进行提示让客户有更好的体验 超时要进行一个单独的处理
            let message:string = error.message
            if (message.includes("Duplicate request")) {
                ElMessage({
                    type: 'error',
                    showClose: true,
                    message: '禁止重复提交',
                    duration: 3 * 1000
                })
                return Promise.reject(error);
            } else if (message.includes("timeout of")) {
                message = "系统接口请求超时"
                this.removeOverTimeRequest()
            } else if (error.response?.status) {
                message = errorCodeType(error.response?.status)
            }
            ElMessage({
                type: 'error',
                showClose: true,
                message,
                duration: 3 * 1000
            })
            return Promise.reject(error);
        });
    }
    private addRequestQueue(config: InternalAxiosRequestConfig) {
        // 如果是初次的话就直接push
        if (this.requestQueue.length === 0) {
            config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
                this.requestQueue.push({
                    url: config.url!,
                    method: config.method!,
                    cancel,
                    createTime: Date.now()
                })
            })
        } else {
            // 这里做循环处理,如果正在请求中存在路径一样方法一样的情况,就直接取消请求
            // 这里也可以根据自己的需求去扩展  比如参数不一样的话就通过等等
            for (const [index, p] of Object.entries(this.requestQueue)) {
                if (p.url === config.url && p.method === config.method) {
                    config.cancelToken = new axios.CancelToken((cancel: Canceler) => {
                        cancel('Duplicate request')
                    })
                }
            }
        }
    }
    private removeRequestQueue(target: InternalAxiosRequestConfig) {
        for (const [index, p] of Object.entries(this.requestQueue)) {
            // 只有在指定的时间到了以后才会取消控制  继续请求 否则终止
            if (p.url === target.url && p.method === target.method && p.createTime && (Date.now() - p.createTime > (target.duplicateRequestValidationTime || 0))) {
                p.cancel(`Duplicate request`)
                this.requestQueue.splice(Number(index), 1)
            }
        }
    }
    private removeOverTimeRequest() {
        const nowDate = Date.now()
        for (const p in this.requestQueue) {
            const { createTime } = this.requestQueue[p]
            const time = nowDate - createTime
            if (time >= (import.meta.env.VITE_APP_TIMEOUT || 10000)) {
                this.requestQueue.splice(Number(p), 1)
            }
        }
    }
    // 返回一下
    getInterceptors() {
        return this.instance;
    }
}

上面import.meta.env.**的配置文件,根据环境的不同获取不同的参数,自己根据自己项目的需求去修改相应的值就好。

.env.development

VITE_NODE_ENV=development
VITE_APP_SERVER_URL=http://www.7timer.info/
VITE_APP_BASE_API=/api
VITE_APP_TIMEOUT=10000
VITE_APP_PREVENT_DUPLICATE_SUBMISSIONS=2000

.env.production

VITE_NODE_ENV=production
VITE_APP_BASE_API=/api
VITE_APP_PREVENT_DUPLICATE_SUBMISSIONS=2000

2、新建src/utils/http/shims.axios.d.ts文件 👇 👇

因为InternalAxiosRequestConfig是没有duplicateRequestValidation属性的,TypeScript会报错,所以需要我们手动加一下。

import { AxiosRequestConfig } from 'axios'
declare module 'axios' {
    export interface AxiosRequestConfig {duplicateRequestValidation?: boolean, duplicateRequestValidationTime?: number
    }
}

3、新建src/utils/http/error-code-type.ts文件 👇 👇

export const errorCodeType = function(code:number):string{
    let message = '未知错误,请联系管理员处理!'
    switch (code) {
        case 302:
            message = '接口重定向了!'
            break
        case 400:
            message = '(错误请求)Bad Request请求包含语法错误!'
            break
        case 401:
            message = '未授权,当前请求需要用户验证!'
            break
        case 403:
            message = '服务器已理解请求,但拒绝访问,您可能没有权限操作!'
            break
        case 404:
            message = '请求错误,服务器未找到该资源!'
            break
        case 405:
            message = '请求方法未允许!'
            break
        case 408:
            message = '服务器等候请求时发生超时!'
            break
        case 409:
            message = '系统已存在相同数据!'
            break
        case 410:
            message = '该资源已被删除!'
            break
        case 413:
            message = '请求实体过大!'
            break
        case 414:
            message = '请求的 URI 过长!'
            break
        case 500:
            message = '服务器端出错!'
            break
        case 501:
            message = '服务器不具备完成请求的功能!'
            break
        case 502:
            message = '错误网关!'
            break
        case 503:
            message = '由于临时的服务器维护或者过载,服务器当前无法处理请求!'
            break
        case 504:
            message = '网络超时!'
            break
        case 505:
            message = '服务器不支持请求中所用的 HTTP 协议版本!'
            break
        default:
            message = `其他错误 -- ${code}`
    }
    return message
}

4、新建src/utils/http/index.ts文件 👇 👇

import {AxiosHeaders, AxiosPromise, AxiosResponse, InternalAxiosRequestConfig} from "axios";
import { Interceptors } from "./request";
import {Method, RawAxiosRequestHeaders} from "axios";

interface RequestConfigType {
    url?: string;
    method?: Method | string;
    headers?: RawAxiosRequestHeaders | AxiosHeaders;
    params?: any;
    data?: any;
    duplicateRequestValidation?: boolean;
    duplicateRequestValidationTime?: number;
}

// 请求配置
export class HttpServer {
    axios: any;
    // 初始化对象 获取axios实例
    constructor() {
        this.axios = new Interceptors().getInterceptors();
    }
    // 简单封装一下方法
    request(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios(config as InternalAxiosRequestConfig).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }

    post(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios.post(config.url, config.data, config as InternalAxiosRequestConfig).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }

    get(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios.get(config.url, config as InternalAxiosRequestConfig).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }

    delete(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios.delete(config.url, config as InternalAxiosRequestConfig).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }

    put(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios.put(config.url, config.data, config as InternalAxiosRequestConfig).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }

    patch(config:RequestConfigType): AxiosPromise {
        return new Promise((resolve, reject) => {
            this.axios.patch(config.url, config.data, config).then((res: AxiosResponse) => {
                resolve(res);
            }).catch((err: any) => {
                reject(err)
            });
        });
    }
}

const http = new HttpServer()

export default http

五、使用 🍄 🍄 🍄

1、vite.config.ts文件中配置代理 👇 👇

这个代理的话我们只会在开发环境中去使用,所以前端开发的小伙伴不用考虑后续发布会用到这里的配置。后面部署到nginx只会和我们的baseURL有关联。

server: {
  host: '0.0.0.0', // 指定服务器应该监听哪个 IP 地址
  port: 9527, // 指定开发服务器端口
  strictPort: true, // 若端口已被占用则会直接退出
  open: false, // 启动时自动在浏览器中打开应用程序
  proxy: {
    '/api': {
      target: loadEnv(mode, process.cwd()).VITE_APP_SERVER_URL, // 后端服务实际地址
      changeOrigin: true, //开启代理
      rewrite: (path) => path.replace(/^\/api/, '')
    }
  }
},

2、根目录下修改.env.development 👇 👇

VITE_NODE_ENV=development
VITE_APP_SERVER_URL=http://www.7timer.info/
VITE_APP_BASE_API=/
VITE_APP_TIMEOUT=10000
VITE_APP_PREVENT_DUPLICATE_SUBMISSIONS=2000

3、vue页面中直接使用 👇 👇

失败 😣 😣 😣

<template>
  <div>
    <el-button @click="submit">提交</el-button>
  </div>
</template>

<script lang="ts">
import http from "@/utils/http";

export default defineComponent({
  setup() {

    const submit = () => {
      http.post({
        url: 'api/bin1/astro.php?lon=113.2&lat=23.1&ac=0&unit=metric&output=json&tzshift=0',
        duplicateRequestValidation: true
      }).then(ts => {
        console.log(ts)
      }).catch(e => {
        console.error(e.message)
        console.log()
      })
    }

    return {
      submit
    }
  }
})
</script>

成功!就更改url为正确的路径 👍 👍 👍

<template>
  <div>
    <el-button @click="submit">提交</el-button>
  </div>
</template>

<script lang="ts">
import http from "@/utils/http";

export default defineComponent({
  setup() {

    const submit = () => {
      http.post({
        url: 'api/bin/astro.php?lon=113.2&lat=23.1&ac=0&unit=metric&output=json&tzshift=0',
        duplicateRequestValidation: true
      }).then(ts => {
        console.log(ts)
      }).catch(e => {
        console.error(e.message)
        console.log()
      })
    }

    return {
      submit
    }
  }
})
</script>

六、重复请求验证演示  🍄 🍄 🍄

1、统一在一个页面 👇 👇 👇

这里的话换成自己的接口信息

<template>
    <div>
        <el-button @click="addUser">添加用户</el-button>
        <el-button @click="addUserTime">添加用户带校验时间</el-button>
    </div>
</template>

<script lang="ts">
import http from "@/utils/http";
export default defineComponent({
    setup() {

        const addUser = () => {
            http.request({
                url: 'user/add',
                method: 'post',
                duplicateRequestValidation: true,
                data: {
                    userName: 'Etc.End',
                    passWord: '12345678'
                }
            })
        }
        const addUserTime = () => {
            http.request({
                url: 'user/add',
                method: 'post',
                duplicateRequestValidation: true,
                duplicateRequestValidationTime: 2000,
                data: {
                    userName: 'Etc.End',
                    passWord: '12345678'
                }
            })
        }

        return {
            addUser,
            addUserTime
        }
    }
})
</script>

<style scoped lang="scss">
</style>

2、没有时间限制 👇 👇 👇

3、限制两秒内同一接口禁止重复请求 👇 👇 👇

我是Etc.End。如果文章对你有所帮助,能否帮我点个免费的赞和收藏😍。

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
vite+vue3项目中,你可以通过封装axios来处理网络请求。下面是一个简单的示例: 1. 首先,安装axios依赖: ```bash npm install axios ``` 2. 创建一个`api.js`文件来封装axios: ```javascript import axios from 'axios'; // 创建一个axios实例 const instance = axios.create({ baseURL: 'https://api.example.com', // 设置接口的基础url timeout: 5000 // 请求时时间 }); // 请求拦截器 instance.interceptors.request.use( config => { // 在发送请求之前做一些处理,例如添加token等 return config; }, error => { // 请求错误处理 console.error(error); return Promise.reject(error); } ); // 响应拦截器 instance.interceptors.response.use( response => { // 对响应数据做一些处理,例如解析返回的json数据 return response.data; }, error => { // 响应错误处理 console.error(error); return Promise.reject(error); } ); export default instance; ``` 3. 在需要发送请求的地方引入`api.js`并使用: ```javascript import api from './api.js'; // 发送GET请求 api.get('/users') .then(response => { console.log(response); }) .catch(error => { console.error(error); }); // 发送POST请求 api.post('/users', { name: 'John Doe' }) .then(response => { console.log(response); }) .catch(error => { console.error(error); }); ``` 这样,你就可以在vite+vue3项目中使用封装好的axios进行网络请求了。当然,你还可以根据项目的需要进行更多的封装和配置。希望对你有帮助!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Etc.End

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值