【问题标题】:How do I implement caching in Redux?如何在 Redux 中实现缓存?
【发布时间】:2017-08-16 06:38:54
【问题描述】:

如果我的商店中已有数据,我希望避免调用 API 两次。

如何使用 Redux 做到这一点?

【问题讨论】:

  • SO 模组通常对“什么是最好的方法”问题不屑一顾。我知道这很愚蠢,但是如果您将其更改为“我该怎么做”,它将更有可能不被关闭。现在它被标记为被关闭的候选者,因为它“主要基于意见”
  • 我们为此编写了一个库! github.com/JumboInteractiveLimited/redux-cache 你为你的缓存时间设置了一个 TTL,然后它会去抖动更新减速器的动作

标签: javascript redux


【解决方案1】:

不要重新发明缓存,只需利用 HTTP 缓存。
您的代码实际上应该不知道缓存机制。
只需在需要数据时发起 http 请求,无论是通过 redux thunk、promise 等还是直接不使用 redux。
HTTP 缓存将为您进行缓存。
当然,要使其正常工作,您需要正确配置服务器以设置适当的缓存参数和有效性。

【讨论】:

    【解决方案2】:

    我遇到了同样的问题,我想在我的 action 和 reducer 之间添加一个缓存层。我的解决方案是创建一个中间件,在请求操作进入实际 thunk 之前缓存它,后者从 API 请求数据。

    这样做有一个优点,您不需要修改现有的 action 和 reducer。您只需添加一个中间件。下面是中间件的样子:

    const cache = store => next => action => {
      // handle FETCH action only
      if (action.type !== 'FETCH') {
        return next(action);
      }
    
      // check if cache is available
      const data = window['__data'];
      if (!data) {
        // forward the call to live middleware
        return next(action);
      }
      return store.dispatch({ type: 'RECEIVE', payload: { data: `${data} (from cache)` } });
    }
    
    export default cache;
    

    https://stackblitz.com/edit/redux-cache-middleware 试用现场演示或查看我的博客文章了解更多信息http://www.tonnguyen.com/2018/02/13/web-programming/implement-a-cache-middleware-for-redux/

    【讨论】:

    • 你为什么说有一个“缺点” - 你的意思是“缺点又名缺点”,你的意思是“好处”吗?我认为这是一个好处,你可以在应用程序范围内进行更改在中间件的一个地方。
    • 真的很抱歉,我的意思是专业人士。编辑了帖子。
    【解决方案3】:

    我专门为此建立了一个库 - redux-cached-api-middleware

    一个示例用法,成功的响应将被缓存(从存储中重复使用)10 分钟:

    import React from 'react';
    import PropTypes from 'prop-types';
    import { connect } from 'react-redux';
    import api from 'redux-cached-api-middleware';
    import Items from './Items';
    import Error from './Error';
    
    class ExampleApp extends React.Component {
      componentDidMount() {
        this.props.fetchData();
      }
    
      render() {
        const { result } = this.props;
        if (!result) return null;
        if (result.fetching) return <div>Loading...</div>;
        if (result.error) return <Error data={result.payload} />;
        if (result.payload) return <Items data={result.payload} />;
        return <div>No items</div>;
      }
    }
    
    ExampleApp.propTypes = {
      fetchData: PropTypes.func.isRequired,
      result: PropTypes.shape({}),
    };
    
    const enhance = connect(
      state => ({
        result: api.selectors.getResult(state, 'GET/my-api.com/items'),
      }),
      dispatch => ({
        fetchData() {
          return dispatch(
            api.actions.invoke({
              method: 'GET',
              headers: { Accept: 'application/json' },
              endpoint: 'https://my-api.com/items/',
              cache: {
                key: 'GET/my-api.com/items',
                strategy: api.cache
                  .get(api.constants.CACHE_TYPES.TTL_SUCCESS)
                  .buildStrategy({ ttl: 10 * 60 * 1000 }), // 10 minutes
              },
            })
          );
        },
      })
    );
    
    export default enhance(ExampleApp);
    

    您可以传递缓存 strategy 或您的自定义 shouldFetch 函数来确定何时应重新获取资源 (docs)。

    该库使用redux-thunk(用于异步操作)和redux-api-middleware(用于调用API)作为对等依赖项,并且设置相当简单:

    import { createStore, combineReducers, applyMiddleware } from 'redux';
    import thunk from 'redux-thunk';
    import { apiMiddleware } from 'redux-api-middleware';
    import api from 'redux-cached-api-middleware';
    import reducers from './reducers';
    
    const store = createStore(
      combineReducers({
        ...reducers,
        [api.constants.NAME]: api.reducer,
      }),
      applyMiddleware(thunk, apiMiddleware)
    );
    

    【讨论】:

      【解决方案4】:

      一种简单而快速的方法(尽管不推荐用于任何可扩展的东西):

      使用redux-persist 来持久化(缓存)存储。每当它补水时,您就知道您之前拥有的数据存在 - 请阅读链接中的文档以了解其工作原理和设置方法。

      为避免在远程服务器上进行不必要的数据提取,您可以将 URL (as Timos answer) 缓存到 localStorage 等,并在进行实际提取之前检查其是否存在。

      行动:

          function fetchUsers(url){
              if(isCached(url)) {
                  // The data is already in the store, thanks to redux-persist.
                  // You can select it in the state as usual.
                  dispatch({ type: 'IS_CACHED', payload: url })
              } else {
                  return fetch(url)
                         .json()
                         .then((response) => {
                             dispatch({ type: 'USERS_FETCH_SUCCESS', payload: response.data })
                             setCached(url)
                         })
                         .catch((error) => {
                             dispatch({ type: 'USERS_FETCH_FAILED', payload: error })
                         })
              }
          }
      

      简单的 url 自定义缓存:

      const CACHE_NAME = 'MY_URL_CACHE'
      
      export function getUrlCache() {
      
          var localStorage
          try {
              localStorage = window.localStorage
              // Get the url cache from the localstorage, which is an array
              return ( localStorage.getItem(CACHE_NAME) ? JSON.parse(localStorage.getItem(CACHE_NAME)) : [] )
          } catch(e){
              // Make your own then...
              throw "localStorage does not exist yo"
          }
      }
      
      export function isCached(url) {
          var urlCache = getUrlCache()
          return urlCache.indexOf(url) !== -1
      }
      
      export function setCached(url){ 
          if( isCached(url) )
              return false
      
          var urlCache = getUrlCache()
          urlCache.push(url)
      
          localStorage.setItem(CACHE_NAME, urlCache)
      
          return true
      }
      
      export function removeCached(url){
          var myCache = getUrlCache()
          var index = myCache.indexOf(url)
          if(index !== -1){
              myCache = myCache.splice(index, 1)
              localStorage.setItem(CACHE_NAME, urlCache)
              return true
          } else {
              return false
          }
      }
      

      当/如果 redux-persist 数据被刷新或其他一些使数据“旧”的事情时,您还需要删除缓存的 url。

      我建议使用带有持久化的 redux 存储来完成整个工作,而是在其上建模 reducer/action 逻辑。有很多方法可以做到,我强烈建议探索 redux、redux-sagaredux-persist 以及常见的概念/设计模式。

      关于基本示例的旁注: 你也可以使用redux-persist-transform-expiretransformer for redux-persist 让缓存的数据在某个时间点过期,同时修改它以删除相关的缓存url。

      【讨论】:

        【解决方案5】:

        在我看来,理想的解决方案是使用Reselect 选择器(https://github.com/reactjs/reselect)。这是一个人为的例子:

        import { createSelector } from 'reselect';
        
        const getObjs = state => state.objs;
        const currentObjId = state => state.currentObjId;
        
        export const getObj = createSelector(
          [ currentObjId, getObjs ],
          (id, objs) => objs.get(href)
        );
        

        这样使用:

        import { getObj } from './selectors';
        
        const ExampleComponent = ({obj}) => <div>{ obj.name }</div>;
        
        const mapStateToProps = state => ({
          obj: getObj(state)
        });
        
        export default connect(mapStateToProps)(ExampleComponent);
        

        第一次运行此程序时,将从所有objs 的列表中“选择”一个基于某些id(也处于状态)的obj。下一次,如果输入没有改变(查看重新选择文档以了解等价的定义),它将简单地返回上次的计算值。

        您还可以选择插入不同类型的缓存,例如LRU。这有点高级,但非常可行。

        Reselect 的主要优点是它允许您进行干净的优化,而无需在 redux 中手动维护额外的状态,如果对原始数据进行了更改,您必须记住要更新这些状态。 Timo 的回答很好,但我认为缺点是它不会缓存昂贵的客户端计算(我知道这不是确切的问题,但这个答案是关于一般最佳实践 redux 缓存,适用于您的问题),仅获取。您可以做一些与 Timo 建议的非常相似的事情,但合并 reselect 以获得非常整洁的解决方案。在动作创建器中,您可以有这样的东西:

        export const fetchObj = (dispatch, getState) => {
          if (hasObj(getState())) {
            return Promise.resolve();
          }
        
          return fetchSomething()
            .then(data => {
              dispatch(receiveObj(data));
              return data;
            });
        };
        

        您将有一个专门针对hasObj 的选择器,可能基于上述选择器(我在这里专门展示如何轻松地编写选择器),例如:

        export const hasObj = createSelector(
          [ getObj ],
          obj => !!obj
        );
        

        一旦你开始使用它来与 redux 交互,在 mapStateToProps 中独占使用它就开始有意义了,即使是简单的选择,这样在将来,如果计算状态的方式发生变化,你就不需要修改所有使用该状态的组件。这方面的一个例子可能是当用于在几个不同的组件中呈现一个列表时,有一个 TODO 数组。在您的应用程序开发过程的后期,您意识到您希望将 TODO 列表默认过滤为不完整的列表。您在一个地方更改选择器就完成了。

        【讨论】:

          【解决方案6】:

          我假设您正在使用 async actions 来处理您的 API 调用。

          这是我放置缓存逻辑的地方,这会产生很好的封装:

          export function fetchData(url) {   
              return function (dispatch) {
                  dispatch(requestData(url))
          
                  if (isCached(url)) {
                      const cachedData = getCached(url)
                      dispatch(receiveData(url, cachedData))
                  } else {
                      return fetch(url)
                        .then(response => response.json())
                        .then(json => {
                            dispatch(receiveData(url, json))
                            setCached(url, json)
                        })
                  }
              }
          }
          

          为您的本地存储实现isCachedgetCachedsetCached 应该相当简单。

          【讨论】:

          • 但是如果我不想/不需要将数据保存在本地存储中,我只需要当前会​​话的数据?
          • 这里所说的“本地存储”是redux,而不是一些持久化的浏览器存储。我想这就是你要问的。
          • 但是在这个解决方案中,我复制了我的存储和内存中的数据(如果我不使用本地存储)
          • 这可能会缓存 url,但它不会缓存 data。例如,假设我在某个时候有其他一些调用检索了用户列表(例如,来自 URL /api/users?someQuery=foo。现在我需要获取一个用户;该用户可能在也可能不在列表中,但知道是否或not /api/users?someQuery=foo is cached 不会告诉我这一点。只有查看实际商店才会告诉我。
          猜你喜欢
          • 2018-04-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-07-15
          • 1970-01-01
          • 2010-09-07
          • 1970-01-01
          相关资源
          最近更新 更多