前端緩存

前端緩存

提示
這里講的前端緩存是指前端對接口數(shù)據(jù)的緩存處理,而不是通過 HTTP(s)緩存

前言

通常會在項目中有這么些情況發(fā)生,比如每次頁面切換的時候都會請求接口,如果頻繁切換,也就會導(dǎo)致接口頻繁的請求,而且在數(shù)據(jù)基本沒有什么變動的情況下,這樣的做法明顯是浪費網(wǎng)絡(luò)資源的。所以我們出于考慮,要實現(xiàn)接口的緩存,避免頻繁的去請求接口。如果后端同學(xué)不給于幫助的話。。。那我們就進入今天的主題--前端緩存。(當然,能 http 緩存就 http 緩存最好了~)

怎么做?

思路

這里我們使用axios進行接口的請求,我們要用到axios的兩個功能--攔截器cancleToken。首先我們要使用攔截器,去攔截要發(fā)送的請求,然后對比我們本地緩存池,看是否有在緩存池中存在,如果存在,則使用cancleToken直接取消請求,并從緩存池從返回數(shù)據(jù),如果不存在則正常請求,并在響應(yīng)攔截器中將該條請求存入緩存池中。當然,我們還需要一個過期時間,如果過期,則重新請求,更新緩存池的數(shù)據(jù),避免一直在緩存池中取老數(shù)據(jù)。

流程

具體流程圖入下:


流程圖

提示
上圖右側(cè)的倆響應(yīng)攔截器其實同屬一個攔截器,這里為了區(qū)分,所以拆分成倆。

實現(xiàn)

import axios from 'axios'
// 定義一個緩存池用來緩存數(shù)據(jù)
let cache = {}
const EXPIRE_TIME = 60000
// 利用axios的cancelToken來取消請求
const CancelToken = axios.CancelToken

// 請求攔截器中用于判斷數(shù)據(jù)是否存在以及過期 未過期直接返回
axios.interceptors.request.use(config => {
  // 如果需要緩存--考慮到并不是所有接口都需要緩存的情況
  if (config.cache) {
    let source = CancelToken.source()
    config.cancelToken = source.token
    // 去緩存池獲取緩存數(shù)據(jù)
    let data = cache[config.url]
    // 獲取當前時間戳
    let expire_time = getExpireTime()
    // 判斷緩存池中是否存在已有數(shù)據(jù) 存在的話 再判斷是否過期
    // 未過期 source.cancel會取消當前的請求 并將內(nèi)容返回到攔截器的err中
    if (data && expire_time - data.expire < EXPIRE_TIME) {
      source.cancel(data)
    }
  }
  return config
})

// 響應(yīng)攔截器中用于緩存數(shù)據(jù) 如果數(shù)據(jù)沒有緩存的話
axios.interceptors.response.use(
  response => {
    // 只緩存get請求
    if (response.config.method === 'get' && response.config.cache) {
      // 緩存數(shù)據(jù) 并將當前時間存入 方便之后判斷是否過期
      let data = {
        expire: getExpireTime(),
        data: response.data
      }
      cache[`${response.config.url}`] = data
    }
    return response
  },
  error => {
    // 請求攔截器中的source.cancel會將內(nèi)容發(fā)送到error中
    // 通過axios.isCancel(error)來判斷是否返回有數(shù)據(jù) 有的話直接返回給用戶
    if (axios.isCancel(error)) return Promise.resolve(error.message.data)
    // 如果沒有的話 則是正常的接口錯誤 直接返回錯誤信息給用戶
    return Promise.reject(error)
  }
)

// 獲取當前時間
function getExpireTime() {
  return new Date().getTime()
}

提示
之所以緩存邏輯寫在響應(yīng)攔截器中是因為只有在響應(yīng)攔截器中可以得到接口返回的數(shù)據(jù),而請求攔截器中,無法做到。

使用

然后頁面中接口請求如下配置:

<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>

或者在統(tǒng)一的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, // 請求攔截的操作函數(shù) 參數(shù)為請求的config對象 返回一個Promise
      responseConfigFn: null, // 響應(yīng)攔截的操作函數(shù) 參數(shù)為響應(yīng)數(shù)據(jù)的response對象 返回一個Promise
      ...options
    }
    this.options = defaults
    this.init()
    // if (options && !options.instance) return this.options.instance
  }

  init() {
    // 初始化
    let options = this.options
    if (options.storage) {
      // 如果開啟本地緩存 則設(shè)置一個過期時間 避免時間過久 緩存一直存在
      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()
        // 判斷緩存數(shù)據(jù)是否存在 存在的話 是否過期 沒過期就返回
        if (data && expire - data.expire < this.options.expire) {
          source.cancel(data)
        }
      }
      return config
    })
  }

  response(cb) {
    // 響應(yīng)攔截器
    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 => {
        // 返回緩存數(shù)據(jù)
        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
        // 如果過期 則重新設(shè)定過期時間 并清空緩存
        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()
}

// 設(shè)置緩存
function setStorage(key, cache) {
  localStorage.setItem(key, JSON.stringify(cache))
}

// 獲取緩存
function getStorage(key) {
  let data = localStorage.getItem(key)
  return JSON.parse(data)
}

// 設(shè)置過期時間
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 => {
    // 響應(yīng)攔截的自定義操作
    if (!res.data.code) {
      // 需要將 res 通過 Promise 返回
      return Promise.resolve(res)
    }
  }
})

export default instance

發(fā)布NPM

該工具已經(jīng)打包至 NPM 庫,可通過包命令安裝:

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

# yarn
yarn add axios-request-cache

axios-request-cache

總結(jié)

  • cache.js可能還有些情況未考慮進去
  • requestConfigFnresponseConfigFn能操作的空間可能也不夠大

后續(xù)還會繼續(xù)優(yōu)化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容