axios cache前端数据缓存

提示
这里讲的前端缓存是指前端对接口数据的缓存处理,而不是通过 HTTP(s)缓存 GITHUB: axios-request-cache

前言

通常会在项目中有这么些情况发生,比如每次页面切换的时候都会请求接口,如果频繁切换,也就会导致接口频繁的请求,而且在数据基本没有什么变动的情况下,这样的做法明显是浪费网络资源的。所以我们出于考虑,要实现接口的缓存,避免频繁的去请求接口。如果后端同学不给于帮助的话。。。那我们就进入今天的主题--前端缓存。(当然,能 http 缓存就 http 缓存最好了~)

怎么做?

这里我们使用axios进行接口的请求,我们要用到axios的两个功能--拦截器和cancleToken。首先我们要使用拦截器,去拦截要发送的请求,然后对比我们本地缓存池,看是否有在缓存池中存在,如果存在,则使用cancleToken直接取消请求,并从缓存池从返回数据,如果不存在则正常请求,并在响应拦截器中将该条请求存入缓存池中。当然,我们还需要一个过期时间,如果过期,则重新请求,更新缓存池的数据,避免一直在缓存池中取老数据。
具体流程图入下:


图片.png

实现

import axios from 'axios'
// 定义一个缓存池用来缓存数据
let cache = {}
const EXPIRE_TIME = 60000
// 利用axios的cancelToken来取消请求
const CancelToken = axios.CancelToken
 
// 请求拦截器中用于判断数据是否存在以及过期 未过期直接返回
axios.interceptors.request.use(config => {
  // 如果需要缓存--考虑到并不是所有接口都需要缓存的情况
  if (config.cache) {
    let source = CancelToken.source()
    config.cancelToken = source.token
    // 去缓存池获取缓存数据
    let data = cache[config.url]
    // 获取当前时间戳
    let expire_time = getExpireTime()
    // 判断缓存池中是否存在已有数据 存在的话 再判断是否过期
    // 未过期 source.cancel会取消当前的请求 并将内容返回到拦截器的err中
    if (data && expire_time - data.expire < EXPIRE_TIME) {
      source.cancel(data)
    }
  }
  return config
})
 
// 响应拦截器中用于缓存数据 如果数据没有缓存的话
axios.interceptors.response.use(
  response => {
    // 只缓存get请求
    if (response.config.method === 'get' && response.config.cache) {
      // 缓存数据 并将当前时间存入 方便之后判断是否过期
      let data = {
        expire: getExpireTime(),
        data: response.data
      }
      cache[`${response.config.url}`] = data
    }
    return response
  },
  error => {
    // 请求拦截器中的source.cancel会将内容发送到error中
    // 通过axios.isCancel(error)来判断是否返回有数据 有的话直接返回给用户
    if (axios.isCancel(error)) return Promise.resolve(error.message.data)
    // 如果没有的话 则是正常的接口错误 直接返回错误信息给用户
    return Promise.reject(error)
  }
)
 
// 获取当前时间
function getExpireTime() {
  return new Date().getTime()
}

提示
之所以缓存逻辑写在响应拦截器中是因为只有在响应拦截器中可以得到接口返回的数据,而请求拦截器中,无法做到。

使用

然后页面中接口请求如下配置:

<template>
  <div>
    i am page A
    <router-link to="/">回首页</router-link>
  </div>
</template>
 
<script>
import axios from '../utils/axios'
 
export default {
  mounted () {
    // 加上属性cache:true 则表示当前接口需要缓存(可以从缓存获取)
    axios('v2/book/1003078', {
      cache: true
    }).then(r => {
      console.log(r)
    })
  }
}
</script>

或者在统一的api接口管理文件中配置:

import axios from './axios'
 
export const getBooks = () => {
  return axios('v2/book/1003078', { cache: true })
}

简单封装

新建一个cache.js

// 缓存池
let CACHES = {}
 
export default class Cache {
  constructor(axios) {
    this.axios = axios
    this.cancelToken = axios.CancelToken
    this.options = {}
  }
 
  use(options) {
    let defaults = {
      expire: 60000, // 过期时间 默认一分钟
      storage: false, // 是否开启缓存
      storage_expire: 3600000, // 本地缓存过期时间 默认一小时
      instance: this.axios, // axios的实例对象 默认指向当前axios
      requestConfigFn: null, // 请求拦截的操作函数 参数为请求的config对象 返回一个Promise
      responseConfigFn: null, // 响应拦截的操作函数 参数为响应数据的response对象 返回一个Promise
      ...options
    }
    this.options = defaults
    this.init()
    // if (options && !options.instance) return this.options.instance
  }
 
  init() {
    // 初始化
    let options = this.options
    if (options.storage) {
      // 如果开启本地缓存 则设置一个过期时间 避免时间过久 缓存一直存在
      this._storageExpire('expire').then(() => {
        if (localStorage.length === 0) CACHES = {}
        else mapStorage(localStorage, 'get')
      })
    }
    this.request(options.requestConfigFn)
    this.response(options.responseConfigFn)
  }
 
  request(cb) {
    // 请求拦截器
    let options = this.options
    options.instance.interceptors.request.use(async config => {
      // 判断用户是否返回 config 的 promise
      let newConfig = cb && (await cb(config))
      config = newConfig || config
      if (config.cache) {
        let source = this.cancelToken.source()
        config.cancelToken = source.token
        let data = CACHES[config.url]
        let expire = getExpireTime()
        // 判断缓存数据是否存在 存在的话 是否过期 没过期就返回
        if (data && expire - data.expire < this.options.expire) {
          source.cancel(data)
        }
      }
      return config
    })
  }
 
  response(cb) {
    // 响应拦截器
    this.options.instance.interceptors.response.use(
      async response => {
        // 判断用户是否返回了 response 的 Promise
        let newResponse = cb && (await cb(response))
        response = newResponse || response
        if (response.config.method === 'get' && response.config.cache) {
          let data = {
            expire: getExpireTime(),
            data: response
          }
          CACHES[`${response.config.url}`] = data
          if (this.options.storage) mapStorage(CACHES)
        }
        return response
      },
      error => {
        // 返回缓存数据
        if (this.axios.isCancel(error)) {
          return Promise.resolve(error.message.data)
        }
        return Promise.reject(error)
      }
    )
  }
 
  // 本地缓存过期判断
  _storageExpire(cacheKey) {
    return new Promise(resolve => {
      let key = getStorage(cacheKey)
      let date = getExpireTime()
      if (key) {
        // 缓存存在 判断是否过期
        let isExpire = date - key < this.options.storage_expire
        // 如果过期 则重新设定过期时间 并清空缓存
        if (!isExpire) {
          removeStorage()
        }
      } else {
        setStorage(cacheKey, date)
      }
      resolve()
    })
  }
}
 
/**
 * caches: 缓存列表
 * type: set->存 get->取
 */
function mapStorage(caches, type = 'set') {
  Object.entries(caches).map(([key, cache]) => {
    if (type === 'set') {
      setStorage(key, cache)
    } else {
      // 正则太弱 只能简单判断是否是json字符串
      let reg = /\{/g
      if (reg.test(cache)) CACHES[key] = JSON.parse(cache)
      else CACHES[key] = cache
    }
  })
}
 
// 清除本地缓存
function removeStorage() {
  localStorage.clear()
}
 
// 设置缓存
function setStorage(key, cache) {
  localStorage.setItem(key, JSON.stringify(cache))
}
 
// 获取缓存
function getStorage(key) {
  let data = localStorage.getItem(key)
  return JSON.parse(data)
}
 
// 设置过期时间
function getExpireTime() {
  return new Date().getTime()
}

然后在自定义的axios.js中引入

import axios from 'axios'
import Cache from './cache2'
 
// axios的自定义实例
let instance = axios.create({
  baseURL: ''
})
 
let cache = new Cache(axios) // 将当前 axios 对象传入 Cache 中
cache.use({
  expire: 30000,
  storage: true,
  instance, // 如果有自定义axios实例 比如上面的instance 需要将其传入instance 没有则不传
  requestConfigFn: config => {
    // 请求拦截自定义操作
    if (config.header) {
      config.header.token = 'i am token'
    } else {
      config.header = { token: 'i am token' }
    }
    // 需要将config对象通过 Promise 返回 cache 中 也可以使用new Promise的写法
    return Promise.resolve(config)
  },
  responseConfigFn: res => {
    // 响应拦截的自定义操作
    if (!res.data.code) {
      // 需要将 res 通过 Promise 返回
      return Promise.resolve(res)
    }
  }
})
 
export default instance

发布NPM

该工具已经打包至 NPM 库,可通过包命令安装:

# npm
npm install axios-request-cache --save
 
# yarn
yarn add axios-request-cache

axios-request-cache

总结

cache.js可能还有些情况未考虑进去
requestConfigFn和responseConfigFn能操作的空间可能也不够大

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容