项目获取最新的请求(axios和vue的watch来实现)

361 阅读3分钟

问题存在背景

假设我们项目中,存在一个搜索页面,我们在页面的搜索框中,分别输入a,b,c查询了三次,我们期待的结果,页面肯定是显示c的查询接果,正常情况是这种,但时候有时候受网络波动的影响,接口响应应可能不会按照我们预期的那样返回。这种时候如果a的查询最先响应,但是b延迟了,c的接口响应回来,b才响应,a>c>b这种就是非我们的期待的返回结果。

说下解决方案

  1. 取消之前的请求
  2. 只取最新的请求结果

取消请求

实现思路(采用axios举例)

  1. 基于axios.CancelToken来实现
  2. 使用一个requestList储存请求信息的队列<url,cancenFn>
  3. axios.interceptors.request的请求拦截器中,对config添加cancenToken并赋值new CancelToken((c) => { cancel = c })
  4. 然后判断当前url是否在requestList中存在,存在则取值,调用,不存在,则存储this.requestList.set(url, cancel)
  5. 最后在axios.interceptors.response中,在执行删除操作this.requestList.delete(url)

具体实现代码如下

GetTheLatestRequest.js

const CancelToken = axios.CancelToken
class GetTheLatestRequest {
    // 储存请求信息的队列<url,cancenFn>
    requestList = new Map()
    constructor() {
        this.addCancelFn = this.addCancelFn.bind(this)
        this.delCancenFnByUrl = this.delCancenFnByUrl.bind(this)
    }
    addCancelFn (config) {
        const { url } = config
        let cancel
        config.cancelToken = new CancelToken((c) => { cancel = c })
        if (this.requestList.has(url)) {
            const cancelFn = this.requestList.get(url)
            cancelFn && cancelFn()
        }
        this.requestList.set(url, cancel)
        return config
    }
    delCancenFnByUrl (url) {
        this.requestList.delete(url)
    }
}
export default GetTheLatestRequest

index.js

import GetTheLatestRequest from './GetTheLatestRequest.js'
const { addCancelFn, delCancenFnByUrl } = new GetTheLatestRequest()
// 添加请求拦截器
axios.interceptors.request.use((config) => {
    return addCancelFn(config)
}, (error) => {
    return Promise.reject(error)
})
// 添加响应拦截器
axios.interceptors.response.use((response) => {
    delCancenFnByUrl(response.config.url)
    return response
}, (error) => {
    return Promise.reject(error)
})
export default axios

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>获取最新的接口请求</title>
</head>
<body>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">
    import http from './index.js'
    // 测试代码
    for (let i = 1; i <= 10; i++) {
        http.get(`./data.json`).then(res => {
            console.log('then的回调')
            console.log(`获取第${i}次请求`, res)
        }).catch(err => {
            console.log('catch的回调')
            console.log(`取消第${i}请求`, err)
        })
    }
</script>
</html>

data.json

{
    "code": 0,
    "data": {
        "name": "测试拦截重复请求"
    }
}

执行结果 期待只在then回调中打印10,其他的则在catch回调中

http.png 通过运行发现,这个方案是符合我们的需求的,ok这个方案说完 来说另一种

只取最新的请求结果

实现思路(采用axios举例)

  1. 使用一个队列requestList自增id实现
  2. 使用一个requestList储存请求信息的队列<url,[id]>
  3. axios.interceptors.request的请求拦截器中,对config添加$id赋值为++this.id
  4. 然后判断当前url是否在requestList中存在,存在则继续往值里面追加this.requestList.get(url).push(config.$id),不存在,则存储this.requestList.set(url, [id])
  5. 最后在axios.interceptors.response中,通过url取值进行判断,没有则说明该接口的最新响应结果,已经被处理,直接结束,有值,进行取值判断,用数组的最后一个value.at(-1)和当前config.$id进行比较,相等,则说明是最新的请求接口,进行处理,否则还是不处理。

具体实现代码如下 GetTheLatestRequest.js

class GetTheLatestRequest {
    // 储存请求信息的队列<url,Id>
    requestList = new Map()
    id = 0
    constructor() {
        this.addId = this.addId.bind(this)
        this.isUseResponseData = this.isUseResponseData.bind(this)
    }
    addId (config) {
        const { url } = config
        config.$id = ++this.id
        if (this.requestList.has(url)) {
            const ids = this.requestList.get(url)
            ids.push(config.$id)
        } else {
            this.requestList.set(url, [config.$id])
        }
        return config
    }
    isUseResponseData (response) {
        const { url, $id } = response.config
        if (this.requestList.has(url)) {
            const value = this.requestList.get(url)
            // 因为id是累加的,所以数组的最后的末尾一定是最新的请求ID,所以只需要比较config.$id来比较即可
            if (value.at(-1) === $id) {
                this.requestList.delete(url)
                return true
            } else {
                return false
            }
        }
        return false
    }
}
export default GetTheLatestRequest

index.js

import GetTheLatestRequest from './GetTheLatestRequest.js'

const { addId, isUseResponseData } = new GetTheLatestRequest()
// 添加请求拦截器
axios.interceptors.request.use((config) => {
    return addId(config)
}, (error) => {
    return Promise.reject(error)
});

// 添加响应拦截器
axios.interceptors.response.use((response) => {
    const value = isUseResponseData(response)
    return value ? response : Promise.reject('不是最新请求')
}, (error) => {
    return Promise.reject(error)
})

export default axios

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>获取最新的接口请求</title>
</head>
<body>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">
    import http from './index.js'
    // 测试代码
    for (let i = 1; i <= 10; i++) {
        http.get(`./data.json`).then(res => {
            console.log('then的回调')
            console.log(`获取第${i}次请求`, res)
        }).catch(err => {
            console.log('catch的回调')
            console.log(`取消第${i}请求`, err)
        })
    }
</script>
</html>

data.json

{
    "code": 0,
    "data": {
        "name": "测试拦截重复请求"
    }
}

执行结果 期待只在then回调中打印10,其他的则在catch回调中

zuixin.png

基于axios的解决方案就说完了,再说了另外一个

大家都知道vue的watchEffect函数吧

function watchEffect( effect: (onCleanup: OnCleanup) => void, options?: WatchEffectOptions ): StopHandle

我们来看下官网的对它的描述

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求。

用通俗的话说,假如你的watchEffect,首行打印变量,然后在onclearup函数中,再次打印了变量,后面使用延时器(1s)修改这个变量,然后页面查看,你会发现首行的代码打印了两次,onclearup第二次才打印

import { watchEffect, ref } from 'vue'
const count = ref(10)
setTimeout(() => {
    count.value--
}, 1000)
watchEffect((onCleanup) => {
    console.log(count.value)
    onCleanup(() => {
        console.log('我是上次注册的回调函数')
    })
})

微信截图_20240519194544.png

其实就是我们() => { console.log('我是上次注册的回调函数') }我们传入的这个回调函数,没有被立即执行,而是保存了起来,下次才会触发,我们用这个机制就可以实现,我们的请求只拿最新的请求结果。 大概实现思路就是:就是在watchEffect声明一个变量标识,默认是true,在onclearup的回调里面修改为false,然后在接口响应判断,当前标识为true,我们就接收当前的响应结果,否则不处理

验证方案

我们声明一个变量requestCount设置为5,模拟5次请求,加上watcheffect是一个立即运行函数,所以预期是发送6次请求,我们只拿到第6次请求,其他过滤 ,具体代码如下
index.vue

<template>
    <div class="block">
        看控制台
    </div>
</template>
<script lang="ts" setup>
import useRequest from '../../hooks/useRequest'
import type { ResponseData } from '../../hooks/useRequest'
let requestCount = 5
let index = 1
let timer: any
const request = (): Promise<ResponseData> => {
    return new Promise((resolve) => {
        let currentIndex = index
        index++
        console.log(`第${currentIndex}个发送的请求`, `响应的时长${1000 * requestCount}`)
        setTimeout(() => {
            resolve({
                msg: `我是第${currentIndex}个的响应的请求`,
                code: 0,
                data: {
                    list: [1],
                    total: 20
                }
            })
        }, 1000 * requestCount)
    })
}

const { pagination } = useRequest(request)
timer = setInterval(() => {
    pagination.pageNum = requestCount
    requestCount--
    if (requestCount === 0) {
        clearInterval(timer)
    }
}, 1000)
</script>

useRequest.ts

import { shallowRef, ref, reactive, watchEffect } from 'vue'
export interface ResponseData {
    msg: string
    code: number,
    data: {
        list: Array<unknown>
        total: number
    }
}
interface Pagination {
    pageSize: number,
    pageNum: number
}
type Key = keyof Pagination
interface Api {
    (val: Pagination): Promise<ResponseData>
}
const useRequest = (api: Api) => {
    const dataList = shallowRef<Array<unknown>>([])
    const sumTotal = ref(0)
    const count = ref(10)
    const pagination = reactive({
        pageSize: 1,
        pageNum: 10
    })
    const handleRequest = (flag: { value: boolean }) => {
        api(pagination).then(res => {
            if (flag.value) {
                console.log(res)
                const { total, list } = res.data
                dataList.value = list
                sumTotal.value = total
            }
        })
    }
    watchEffect((onCleanup) => {
        // 告诉监听器,我使用了pagination的所有属性,一旦属性发生变化,就可以触发
        const keys = Object.keys(pagination) as Array<Key>
        keys.forEach((key) => pagination[key])
        let isUseCurrentRequestDataFlag = true
        onCleanup(() => {
            isUseCurrentRequestDataFlag = false
        })
        handleRequest({
            get value() {
                return isUseCurrentRequestDataFlag
            }
        })
    })
    return {
        count,
        dataList,
        sumTotal,
        pagination
    }
}
export default useRequest

预期接口应该是handleRequest这个函数的console.log(res)只打印一次

我们来看执行代码

微信截图_20240519201051.png

符合预期,至此完成了