vue3个人最佳实践记录(4)- 精细的API取消实现方式分享

253 阅读3分钟

取消请求的实现

  1. 重复请求取消
  2. 路由跳转组件未响应的请求取消(对于后端来讲 没有意义,前端来讲可能会限制并发请求个数)
  3. keepAlive组件的切换是否要清理请求(待实现)

重复请求取消

说到重复请求就要有一个判断依据什么样的请求是重复的(网上给出的大多数方案是通过url+参数作为唯一key,并不严谨)

答案很简单:组件内同一个请求函数发起的请求就是重复的(比如列表第一页,第二页在业务上就是重复请求,如果按网上的方法去判断是两个独立请求)

怎么去做呢

// @/api/useGetUserList.js
import request from '@/util/request/index.js'
import { ref } from 'vue'

const getUserList = (params) => {
    return request.get('/v1/userList', { params })
}

export const useGetUserListApi = () {
    const userList = ref([])
    
    const getUserList = () => {
        getUserList()
            .then((res) => {
                userList.value = res
            })
    }
    
    return {
        userList,
        getUserList
    }
}
// @/views/foo.vue
<template>
    <button @click="setUserList">请求</button>
</template>

<script setup>
    import { useGetUserListApi } from '@/api/useGetUserList.js'
    import { createCancelableApi } from '@/api/createCancelableApi.js' 
    
    const { userList, getUserList } = useGetUserListApi()
    // 不改变原有函数的行为 增加它的能力怎么去做 包装函数 特别注意上面提到组件内 那就需要使用到hook
    const setUserList = createCancelableApi(getUserList)
</script>
// @/api/createCancelableApi.js
// 我们如何识别api函数 

const getId = (mark) => {
  let id = 0;
  return () => `${mark ?? ''}${id++}`
}
// 给每个请求函数加上id
export const getApiId = getId('API')

// 每次执行请求打印的id 用于调试
const getReqId = getId('REQ')

export const createCancelableApi = (api) => {
    const apiId = getApiId()
    
    // 这个时候我们就能通过id来存储cancel函数了
    
    return = (...args) => {
        return api.call(null, ...args)
    }
}

// 同时我们设想最小的操作元是组件

// @/hooks/useCancellationMap.js
import { getCurrentInstance } from "vue";

export const store = new Map()

export const useCancellationMap = () => {
  const uid = getCurrentInstance().uid
  const map = store.get(uid) ?? new Map()
  store.set(
      uid,
      map
  )
  return map
}

// 这个时候就能保证每个组件一个map


// @/api/createCancelableApi.js
import { useCancellationMap } from '@/hooks/useCancellationMap.js'

export const createCancelableApi = (api) => {
    const map = useCancelMap()
    const reqId = getReqId()
    
    // 这个时候就要给请求增加取消的能力

    return = () => {
        const config = {}
        const controller = new AbortController();
        config.signal = controller.signal
        config.abort = () => {
          console.warn('取消', reqId)
          controller.abort()
        }
        config.apiId = apiId
        config.reqId = reqId
        config.map = map
        return api.call(null, config)
    }
}

// 此时已经给请求增加了取消的能力 下一步就是在请求拦截器中 拦截重复请求了

// @/util/request/interceptors/cancel.js

export const setupCancelInterceptors = (request) => {
  request.interceptors.request.use(
      (config) => {
        const {
          map,
          apiId
          abort
        } = config
        console.warn(
            '请求',
            config.reqId
        )
        // apiId对应的请求首次请求不会有取消函数
        if (map?.has(apiId)) {
          map?.get(apiId)?.()
          // 取消后迅速删掉 用来区分是正常响应还是取消
          map?.delete(apiId)
        }
        // 将apiId对应的取消函数设置在map中
        map?.set(
            apiId,
            abort
        )
        return config
      }
  )
  request.interceptors.response.use(
      (res) => {
        console.warn(
            '响应',
            res.config?.reqId
        )
        // 响应结束的请求从map中清掉
        const {
          map,
          apiId
        } = res.config
        map?.delete(apiId)
        return res ?? null
      },
      (error) => {
        console.log(error)
      }
  )
}

// 到此我们解决了重复请求的问题 
// 但是如果仔细去思考 假设页面上有两个地方分别调用了这个函数 我们根据业务需求还要区分 该怎么做呢


// @/views/foo.vue
<template>
    <button @click="setUserList">请求</button>
    <button @click="setUserList1">请求</button>
</template>

<script setup>
    import { useGetUserListApi } from '@/api/useGetUserList.js'
    import { createCancelableApi } from '@/api/createCancelableApi.js' 
    
    const { userList, getUserList } = useGetUserListApi()
    // 还是在不改变原有函数的行为 增加它的能力 让他们变得不同
    const setUserList = createCancelableApi(getUserList)
    const setUserList1 = createCancelableApi(getUserList)
</script>

// @/api/createCancelableApi.js
import { useCancellationMap } from '@/hooks/useCancellationMap.js'

// 增加了一个位置id 从写api我们可以非常精细的感知
export const getPositionId = getId('POSITION')
export const createCancelableApi = (api) => {
    const map = useCancellationMap()
    const reqId = getReqId()
    const pId = getPositionId()
    // 这个时候apiId就不能作为判重的依据了 对应的请求拦截器里面也要使用这个key作为判断
    const reqKey = apiId + pid
    
    return = () => {
        const config = {}
        const controller = new AbortController();
        config.signal = controller.signal
        config.abort = () => {
          console.warn('取消', reqId)
          controller.abort()
        }
        config.reqKey = reqKey
        config.reqId = reqId
        config.map = map
        return api.call(null, config)
    }
}

// 至此 重复请求的判重工作完成

路由跳转组件未响应的请求取消

// 路由切换最直观的就是页面的变化 -> 映射到开发的角度就是组件的卸载
// @/hooks/useCancellationMap.js
import { getCurrentInstance, onUnmounted } from "vue";

export const store = new Map()

export const useCancellationMap = () => {
    // 当组件即将卸载时,把map中存在的所有请求依次cancel, 并从全局的数据管理中移除掉
    onUnmounted(() => {
      map.values().forEach((cancel) => {
        cancel()
      })
      map.clear()
      store.delete(uid)
    })

}

// 这个时候请求取消的全部功能完成

取消API实践场景不多,了解即可

后续会陆续分享vue3个人觉得最佳实践