Javascript 如何在 Redux 中实现缓存?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/42980613/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 01:33:57  来源:igfitidea点击:

How do I implement caching in Redux?

javascriptredux

提问by ng2user

I would like to avoid calling an API twice if I already have the data in my store.

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

How do I do this with Redux?

我如何使用 Redux 做到这一点?

回答by dpwrussell

The ideal solution to this in my opinion is to use Reselectselectors (https://github.com/reactjs/reselect). Here is a contrived example:

在我看来,理想的解决方案是使用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)
);

Used like this:

像这样使用:

import { getObj } from './selectors';

const ExampleComponent = ({obj}) => <div>{ obj.name }</div>;

const mapStateToProps = state => ({
  obj: getObj(state)
});

export default connect(mapStateToProps)(ExampleComponent);

The first time you run this, one objbased on some id(also in the state) will be "selected" from the list of all objs. The next time, if the inputs have not changed (look at reselect documentation for the definition of equivalence) it will simply return the computed value from last time.

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

You also have the option to plug-in a different type of cache, e.g. LRU. That's a bit more advanced, but very doable.

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

The major advantage of Reselect is that it allows you to cleanly optimise without manually maintaining extra state in redux that you would then have to remember to update if a change to the original data was made. Timo's answer is good, but I would argue that the weakness is that it doesn't cache expensive client side computation (I know this wasn't the exact question, but this answer is about best practice redux caching in general, applied to your problem), only fetching. You can do something very similar to what Timo suggests, but incorporate reselect for a very tidy solution. In an action creator you could have something like this:

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;
    });
};

You would have a selector specifically for hasObjpotentially based on the above selector (I do so here specifically to show how you can compose selectors easily), like:

您将有一个专门用于hasObj可能基于上述选择器的选择器(我这样做是为了展示如何轻松地组合选择器),例如:

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

Once you start using this to interface with redux, it starts to make sense to use it exclusively in mapStateToPropseven for simple selects so that at a future time, if the way that state is computed changes, you do not need to modify allof the components which use that state. An example of this might be something like having an array of TODOs when is used to render a list in several different components. Later in your application development process you realise you want to filter that list of TODOs by default to only ones that are incomplete. You change the selector in one place and you are done.

一旦您开始使用它与 redux 进行交互,mapStateToProps即使对于简单的选择,也开始专门使用它变得有意义,这样在未来的时间,如果计算状态的方式发生变化,您就不需要修改所有组件哪个使用那个状态。这方面的一个例子可能是当用于在几个不同的组件中呈现列表时拥有一个 TODO 数组。稍后在您的应用程序开发过程中,您意识到您希望默认过滤该 TODO 列表以仅过滤不完整的列表。您可以在一处更改选择器,然后就完成了。

回答by Timo

I'm assuming you are using async actionsto handle your API calls.

我假设您正在使用异步操作来处理您的 API 调用。

This is the place where I would put the caching logic, which results in a nice encapsulation:

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

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


Implementing isCached, getCachedand setCachedfor your local storage should be rather straightforward.

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

回答by Ton Nguyen

I came up with the same problem, where I wanted to add a cache layer between my action and reducer. My solution was to create a middleware, to cache the Request action before it goes to the actual thunk, which request data from the API.

我想出了同样的问题,我想在我的操作和减速器之间添加一个缓存层。我的解决方案是创建一个中间件,在请求操作进入实际 thunk 之前对其进行缓存,后者从 API 请求数据。

Doing this has a pros that you don't need to modify your existing action and reducer. You just add a middleware. Here is how the middleware look like:

这样做的优点是您不需要修改现有的 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;

Try out the live demo at https://stackblitz.com/edit/redux-cache-middlewareor check out my blog post for more info http://www.tonnguyen.com/2018/02/13/web-programming/implement-a-cache-middleware-for-redux/

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

回答by pakman

Don't reinvent caching, just leverage the HTTP cache.
Your code should be practically unaware of the caching mechanism.
Simply make the http request when you need the data, it doesn't matter if it is through redux thunks, promises, etc or directly without redux.
The HTTP cache will do the caching for you.
Of course for this to work you need to properly configure your server to set the appropriate caching parameters and validity.

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

回答by Oyvind Andersson

A simple and fast way to do it (although not recommended for anything scalable):

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

Use redux-persistto persist (cache) the store. Whenever it rehydrates, you know the data you had previously is present - read the docs in the link for how it works and how to setup.

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

To avoid unnessecary data-fetches on the remote server, you can cache the URLs (as Timos answer) to the localStorage or such, and simply check for its existence before doing the actual fetch.

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

Action:

行动:

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

Simple custom-cache for urls:

简单的 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
    }
}

You would also need to remove the cached url when/if the redux-persist data is flushed or some other thing that makes the data "old".

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

I recommend doing the whole thing using the redux store with persisting, and rather model the reducer/action logic on it. There are many ways to do it, and I highly recommend exploring redux, redux-sagaand redux-persistand common concepts/design patterns.

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

Sidenote on basic example:You can also use redux-persist-transform-expiretransformer for redux-persist to let cached data expire at some point in time, and modify it to remove the relevant cached url while doing so.

关于基本示例的旁注:您还可以使用redux-persist-transform-expire转换器 for redux-persist 让缓存数据在某个时间点过期,并在这样做时修改它以删除相关的缓存 url。

回答by Karolis ?arapnickis

I built a library specifically for this - redux-cached-api-middleware.

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

An example usage, where successful response would be cached (re-used from store) for 10 minutes:

一个示例用法,其中成功的响应将被缓存(从商店重新使用)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);

You can pass a caching strategyor your custom shouldFetchfunction to determine when the resource should be re-fetched (docs).

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

The library uses redux-thunk(for async actions) and redux-api-middleware(for invoking APIs) as peer dependency, and the setup fairly simple:

该库使用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)
);