前端接口请求支持内容缓存和过期时间

前端接口请求支持内容缓存和过期时间

支持用户自定义缓存时间,在规则时间内读取缓存内容,超出时间后重新请求接口

首先封装一下 axios,这一步可做可不做。但是在实际开发场景中都会对 axios 做二次封装,我们在二次封装的 axios 基础上继续封装,增加支持缓存功能

request.js

js 复制代码
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import cache from '@/plugins/cache'
import qs from 'qs'

// 本地开发环境需要加请求头
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
axios.defaults.headers['lang'] = 'CN'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: process.env.VUE_APP_BASE_API,
  // 超时
  timeout: 100000,
})

// request拦截器
service.interceptors.request.use(
  (config) => {
    // 是否需要设置 token
    const isToken = (config.headers || {}).isToken === false
    // 是否需要防止数据重复提交
    const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
    if (getToken() && !isToken) {
      config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    // get请求映射params参数
    if (config.method === 'get' && config.params) {
      let url = config.url + '?' + qs.stringify(config.params)
      url = url.slice(0, -1)
      config.params = {}
      config.url = url
    }
    if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
      const requestObj = {
        url: config.url,
        data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
        time: new Date().getTime(),
      }
      const sessionObj = cache.session.getJSON('sessionObj')
      if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
        cache.session.setJSON('sessionObj', requestObj)
      } else {
        // 忽略重复请求的地址
        const exUrls = []
        const s_url = sessionObj.url // 请求地址
        const s_data = sessionObj.data // 请求数据
        const s_time = sessionObj.time // 请求时间
        const interval = 3000 // 间隔时间(ms),小于此时间视为重复提交
        if (
          s_data === requestObj.data &&
          requestObj.time - s_time < interval &&
          s_url === requestObj.url &&
          !exUrls.includes(config.url)
        ) {
          const message = '数据正在处理,请勿重复提交'
          return Promise.reject(new Error(message))
        } else {
          cache.session.setJSON('sessionObj', requestObj)
        }
      }
    }
    return config
  },
  (error) => {
    Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  (res) => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || '0'
    // 获取错误信息
    const msg = res.data.message
    // 二进制数据则直接返回
    if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
      return res.data
    }
    if (code === 401 || code === '10006') {
      MessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
        confirmButtonText: '重新登录',
        cancelButtonText: '取消',
        type: 'warning',
      }).then(() => {
        store.dispatch('LogOut').then(() => {
          location.href = '/login'
        })
      })
    } else if (code !== '0') {
      Message({
        message: msg || '接口请求异常',
        type: 'error',
      })
      return Promise.reject(new Error(msg))
    } else {
      return res.data
    }
  },
  (error) => {
    let { message } = error
    if (message === 'Network Error') {
      message = '后端接口连接异常'
    } else if (message.includes('timeout')) {
      message = '系统接口请求超时'
    } else if (message.includes('Request failed with status code')) {
      message = '系统接口' + message.substr(message.length - 3) + '异常'
    }
    Message({
      message: message,
      type: 'error',
      duration: 5 * 1000,
    })
    return Promise.reject(error)
  }
)

export default service

新建 catchAjax.js ,当我们想用接口缓存时,就用 catchAjax 方法,不想用时还用上面的 request 文件,互不影响

js 复制代码
const cacheMap = new Map()
// 定义状态池
const statusMap = new Map()
// 回调callbackMap
const callbackMap = new Map()
// 引入axios
import myAxios from '@/utils/request'
// qs用于序列化对象,将对象序列化为用&拼接的参数
import qs from 'qs'

// 一般只缓存GET接口
function generateCacheKey(request) {
  return request.url + '?' + qs.stringify(request.params)
}

// 返回指定分钟后的时间戳 过期时间
function generateExpTime(minutes) {
  // 获取当前时间戳
  let now = new Date()
  // 添加分钟数
  now.setMinutes(now.getMinutes() + minutes)
  // 返回未来的时间戳
  return now.getTime()
}

// 导出请求方法
export function cacheRequest(request) {
  if (request.method && request.method.toUpperCase() !== 'GET') {
    throw new Error('cacheRequest 仅支持GET请求')
  }
  if (request.expTime && !/^\d+$/.test(request.expTime)) {
    throw new Error('expTime 必须是正整数')
  }
  // 用当前请求的 url + 参数 来当做缓存的key
  const cacheKey = generateCacheKey(request)
  // 判断状态池中是否有数据
  if (statusMap.has(cacheKey)) {
    // 获取当前的状态
    const currentStatus = statusMap.get(cacheKey)
    // 如果接口已经在缓存中,则进入这里
    if (currentStatus === 'complete') {
      // 判断是否过期
      let nowTime = new Date().getTime()
      // 已经过期的数据不能从缓存中取,设置这个状态是pending,重新走接口
      if (nowTime >= cacheMap.get(cacheKey).expTime) {
        statusMap.set(cacheKey, 'pending')
      } else {
        // 没有过期则从缓存中返回数据
        return Promise.resolve(cacheMap.get(cacheKey)?.data)
      }
    }

    if (currentStatus === 'pending') {
      // 判断回调池中是否有数据
      return new Promise((resolve, reject) => {
        if (callbackMap.has(cacheKey)) {
          callbackMap.get(cacheKey).push({
            onSuccess: resolve,
            onError: reject,
          })
        } else {
          callbackMap.set(cacheKey, [
            {
              onSuccess: resolve,
              onError: reject,
            },
          ])
        }
      })
    }
  }
  // 设置接口状态
  statusMap.set(cacheKey, 'pending')

  // 判断是否需要缓存,并且缓存池中有数据时,返回缓存池中的数据
  return myAxios(request)
    .then((res) => {
      // 接口响应成功后吧当前的请求状态设置为complete,下次请求时就会走缓存,不会走网络
      statusMap.set(cacheKey, 'complete')
      // 往缓存中塞数据,同时设置过期时间
      cacheMap.set(cacheKey, {
        data: res,
        // 默认缓存5分钟
        expTime: generateExpTime(request.expTime || 5),
      })
      // 判断在接口响应期间是否有请求,如果有请求,则遍历所有的回调并执行
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey).forEach((callback) => {
          callback.onSuccess(res)
        })
        // 响应完数据后吧回调删除
        callbackMap.delete(cacheKey)
      }
      // 返回真实的接口数据
      return res
    })
    .catch((error) => {
      statusMap.delete(cacheKey)
      if (callbackMap.has(cacheKey)) {
        callbackMap.get(cacheKey).forEach((callback) => {
          callback.onError(error)
        })
        callbackMap.delete(cacheKey)
      }
      return Promise.resolve(error)
    })
}

使用方法

html 复制代码
<template>
  <div>
    <el-button type="primary" @click="cacheAxios">测试</el-button>
  </div>
</template>

<script>
import { cacheRequest } from '@/utils/catchAjax'

const getArticleList = (params) => {
  return cacheRequest({
    url: 'http://localhost:10086/order/list',
    method: 'get',
    params,
    expTime: 1, // 缓存一分钟
  })
}

export default {
  name: 'index',
  methods: {
    cacheAxios() {
      getArticleList({
        pageNum: 1,
        pageSize: 10,
      }).then((res) => {
        console.log(res)
      })
    },
  },
}
</script>

我们在 1 分钟内连续点击按钮,发现只会走一次接口,但是控制台可以打印多次数据

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js