Axios缓存请求

Posted by Xiaosa's Blog on April 30, 2023

第一版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import axios from 'axios'
import LRUCache from 'lru-cache'
const cache = new LRUCache({
    max: 100,
    maxAge: 10000
  });
  const useCache = config => {
    if (!config?.useCache) return config;
    // 生成缓存key
    const cacheKey = getCacheKey(config);
    // 如果缓存中有结果,则直接返回缓存结果
    const cachedResponse = cache.get(cacheKey);
    if (cachedResponse) {
      let source = axios.CancelToken.source();
      config.cancelToken = source.token;
      // @ts-ignore
      source.cancel({type: 'hit cache', data: cachedResponse})
    }
    return config;
  }
  const setCache = (response) => {
    if (!response?.config?.useCache) return response;
    console.log('resp12312312', response)
    if (response?.hitCache) {
      console.log('response123123', response)
      return response;
    }
    // 生成缓存key
    const cacheKey = getCacheKey(response.config);
    // 缓存结果
    cache.set(cacheKey, response.data);
    console.log('cacheKey setCache', cacheKey)
    console.log('cache.keys', cache)
    return response;
  }
  
  axios.interceptors.request.use(useCache);
  axios.interceptors.response.use(setCache);

上述代码可以缓存请求 但是在实际场景中存在一个致命的问题。 实际场景:在main.js 中提前发起阻塞性请求,避免业务中世纪请求时阻塞页面渲染。 而预请求的时候往往请求还没结束 就开始真正的业务请求了,这时候预请求没有结束导致缓存还没写入,就会再次发起请求,反而导致了性能的浪费。 所以希望缓存的是一个promise,这个promise在请求开始时就写入缓存,并在请求结束后settled。当第二次请求时,如果缓存中有这个promise,就直接返回这个promise,而不是再次发起请求。

第二版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import axios from 'axios';
import LRUCache from 'lru-cache';

const cache = new LRUCache({
  max: 100,
  maxAge: 10000
});

const useCache = config => {
  if (!config?.useCache) return config;

  const cacheKey = getCacheKey(config);

  const cachedResponse = cache.get(cacheKey);

  if (cachedResponse) {
    if (cachedResponse instanceof Promise) {
      return cachedResponse.then(response => {
        response.hitCache = true;
        return response;
      });
    }

    let source = axios.CancelToken.source();
    config.cancelToken = source.token;
    source.cancel({ type: 'hit cache', data: cachedResponse });
  } else {
    const pendingPromise = cache.get(cacheKey + ':pending');

    if (pendingPromise) {
      return pendingPromise;
    }

    const pending = axios(config).then(response => {
      setCache(response);
      return response;
    });

    cache.set(cacheKey + ':pending', pending);

    return pending;
  }

  return config;
};

const setCache = response => {
  if (!response?.config?.useCache) return response;

  const cacheKey = getCacheKey(response.config);

  cache.del(cacheKey + ':pending');

  cache.set(cacheKey, response.data);

  return response;
};

const getCacheKey = config => {
  return JSON.stringify({
    method: config.method,
    url: config.url,
    params: config.params,
    data: config.data
  });
};

axios.interceptors.request.use(useCache);
axios.interceptors.response.use(setCache);

第二版代码是不能运行的 这里补充一个点 我们的interceptors return的时候 是return了一个config给axios,而且在我们刚才讨论的场景中 这个请求不能被取消。我们的缓存结果也不能直接跳过实际的axios请求直接返回给调用者。所以,我们需要在axios外边包一层,来解决这个case。

第三版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import axios from 'axios';
import LRUCache from 'lru-cache';

const cache = new LRUCache({
  max: 100,
  maxAge: 10000
});

const getCacheKey = config => {
  return JSON.stringify({
    method: config.method,
    url: config.url,
    params: config.params,
    data: config.data
  });
};

const axiosWithCache = (config, useCache = true) => {
  if (!useCache) {
    return axios.get(url);
  }

  const cacheKey = getCacheKey(config);
  const cachedResponse = cache.get(cacheKey);

  if (cachedResponse) {
    if (cachedResponse instanceof Promise) {
      return cachedResponse.then(response => {
        response.hitCache = true;
        return response;
      });
    }

    return Promise.resolve({
      data: cachedResponse,
      status: 200,
      statusText: 'OK',
      headers: {},
      config: { ...config, useCache },
      hitCache: true
    });
  }

  const pendingPromise = cache.get(cacheKey + ':pending');

  if (pendingPromise) {
    return pendingPromise;
  }

  const axiosPromise = axios(config);

  cache.set(cacheKey + ':pending', axiosPromise);

  return axiosPromise.then(response => {
    cache.set(cacheKey, response.data);
    cache.del(cacheKey + ':pending');

    response.hitCache = false;

    return {
      ...response,
      config: { ...config, useCache },
      hitCache: false
    };
  });
};

export default axiosWithCache;

第四版代码 希望能直接替换axios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import axios from 'axios';
import LRUCache from 'lru-cache';

const cache = new LRUCache({
  max: 100,
  maxAge: 10000
});

const getCacheKey = config => {
  return JSON.stringify({
    method: config.method,
    url: config.url,
    params: config.params,
    data: config.data
  });
};

const axiosWithCache = (config, useCache = true) => {
  if (!useCache) {
    return axios(config);
  }

  const cacheKey = getCacheKey(config);
  const cachedResponse = cache.get(cacheKey);

  if (cachedResponse) {
    if (cachedResponse instanceof Promise) {
      return cachedResponse.then(response => {
        response.hitCache = true;
        return response;
      });
    }

    return Promise.resolve({
      data: cachedResponse,
      status: 200,
      statusText: 'OK',
      headers: {},
      config: { ...config, useCache },
      hitCache: true
    });
  }

  const pendingPromise = cache.get(cacheKey + ':pending');

  if (pendingPromise) {
    return pendingPromise;
  }

  const axiosPromise = axios(config);

  cache.set(cacheKey + ':pending', axiosPromise);

  return axiosPromise.then(response => {
    cache.set(cacheKey, response.data);
    cache.del(cacheKey + ':pending');

    response.hitCache = false;

    return {
      ...response,
      config: { ...config, useCache },
      hitCache: false
    };
  });
};

// 创建一个新的 Axios 实例
const axiosInstance = axios.create();

// 重写 Axios 实例的方法
axiosInstance.get = (url, config = {}) => {
  return axiosWithCache({ ...config, method: 'get', url });
};

axiosInstance.post = (url, data, config = {}) => {
  return axiosWithCache({ ...config, method: 'post', url, data });
};

axiosInstance.put = (url, data, config = {}) => {
  return axiosWithCache({ ...config, method: 'put', url, data });
};

axiosInstance.patch = (url, data, config = {}) => {
  return axiosWithCache({ ...config, method: 'patch', url, data });
};

axiosInstance.delete = (url, config = {}) => {
  return axiosWithCache({ ...config, method: 'delete', url });
};

export default axiosInstance;

好的 就这样吧 累了

注:写完以后才发现虽然axios官方没有提供这个功能 但社区已经有人实现了:https://www.npmjs.com/package/axios-cache-adapter 有空看看他是怎么处理的吧