2

我正在尝试缓存通过 redux-observable 史诗调用的 ajax 数据。

我的目标是仅在第一次 dispatch 时调用 API LOAD_DATA_REQUEST,然后第二次返回缓存的数据。

下面是我尝试过的代码,但数据没有缓存,每当我 dispatch 时都会调用 API LOAD_DATA_REQUEST

const loadDataEpic =
        action$ => action$.ofType(LOAD_DATA_REQUEST)
            .mergeMap(action => getData$(action.criteria)
                .map(x => loadDataSuccess(x.response))
                .catch(error => Observable.of(loadDataFailure(error.xhr)))
            );

const getData$ = criteria => Observable.ajax.post('some-url', criteria)
    .publishLast()
    .refCount();

export default combineEpics(
    loadDataEpic 
);

我也试过这个:

const getData$ = criteria => Observable.ajax.post('some-url', criteria)
    .publishReplay(1)
    .refCount();

const getData$ = criteria => Observable.ajax.post('some-url', criteria)
    .shareReplay();
4

2 回答 2

7

正如 ddcc3432 所提到的,我个人会推迟将缓存的结果存储在 redux 存储本身中。这是它最自然的地方。

这是一个这样做的一般示例,假设您也保持某种加载状态。如果您不需要加载状态,那么在从缓存提供服务时调度一些操作不是必需的吗?您可以通过过滤器忽略LOAD_DATA_REQUESTddcc3432 提到的。

const yourDataCache = (state, action) => {
  switch (action.type) {
    case LOAD_DATA_REQUEST:
      return {
        ...state,
        // however you index your requests
        [action.criteria]: {
          isLoading: true
        }
      };

    case LOAD_DATA_SUCCESS:
      return {
        ...state,
        // however you index your responses
        // here I'm assuming criteria is a string and is
        // also included in the response. Change as needed
        // for your real data
        [action.criteria]: {
          isLoading: false,
          ...action.response
        }
      };

    case LOAD_DATA_CACHED:
      return {
        ...state,
        // however you index your responses
        [action.criteria]: {
          isLoading: false, // just change the loading state
          ...state[action.criteria] // keep existing cache!
        }
      };

    default:
      return state;
  }
};

const loadDataEpic = (action$, store) => 
  action$.ofType(LOAD_DATA_REQUEST)
    .mergeMap(action => {
      const { yourDataCache } = store.getState();

      // If the data is already cached, we don't need to
      // handle errors. All this code assumes criteria is
      // a simple string!
      if (yourDataCache[action.criteria]) {
        return Observable.of({
          type: LOAD_DATA_CACHED,
          criteria: action.criteria
        });

      } else {
      return getData(action.criteria)
        .map(x => loadDataSuccess(x.response))
        .catch(error => Observable.of(loadDataFailure(error.xhr)))
      }
    });

您可能会发现将加载(例如)状态存储在它自己的减速器中更容易isLoading,因此您不需要将它与实际响应有效负载进行额外的合并——我个人这样做了,但在这个例子中我没有因为我大多数都没有,它有时会把它们扔掉。


但是,您澄清说您希望使用 RxJS 重播,所以这是一种方法

(先看我对你的回答的评论)

如果您想根据“标准”进行缓存,您可以创建自己的小助手来执行此操作:

const cache = new Map();

const getData = criteria => {
  if (cache.has(criteria)) {
    return cache.get(criteria);
  } else {
    // Using publishReplay and refCount so that it keeps the results
    // cached and ready to emit when someone subscribes again later
    const data$ = Observable.ajax.post('some-url', criteria)
      .publishReplay(1)
      .refCount();

    // Store the resulting Observable in our cache
    // IMPORTANT: `criteria` needs to be something that will later
    // have reference equallity. e.g. a string
    // If its an object and you create a new version of that object every time
    // then the cache will never get a hit, since cache.has(criteria) will return
    // false for objects with different identities. `{ foo: 1 } !== { foo: 1}`
    cache.set(criteria, data$);
    return data$;
  }
};

const loadDataEpic = action$ =>
  action$.ofType(LOAD_DATA_REQUEST)
    .mergeMap(action =>
      getData(action.criteria)
        .map(x => loadDataSuccess(x.response))
        .catch(error => Observable.of(
          loadDataFailure(error.xhr)
        ))
    );

但是,至关重要criteria是,在相同意图的情况下,始终具有严格的引用相等性。如果它是一个对象并且您每次都创建一个新对象,那么它们将永远不会获得缓存命中,因为它们不是相同的引用,它们具有不同的身份——无论它们是否具有相同的内容。

let a = { foo: 1 };
let b = { foo: 1 };
a === b;
// false, because they are not the same object!

如果您需要使用对象并且不能以其他方式关闭某些原语(如 ID 字符串),您将需要一些方法来序列化它们。

JSON.stringify({ foo: 1, bar: 2 }) === JSON.stringify({ foo: 1, bar: 2 })
// true, but only if the keys were defined in the same order!!

JSON.stringify({ bar: 2, foo: 1 }) === JSON.stringify({ foo: 1, bar: 2 })
// false, in most browsers JSON.stringify is not "stable" so because
// the keys are defined in a different order, they serialize differently
// See https://github.com/substack/json-stable-stringify

尽量使用唯一的 ID 并将其发送到服务器,而不是复杂的 JSON。有时因为各种原因无法避免,但要努力!正如您在缓存中看到的那样,它会让您的生活更轻松。


您可能需要考虑缓存驱逐。当窗口打开时,这个缓存是否总是无限期地保留结果?那不好吗?根据频率、大小等,这可能会导致严重的内存泄漏。小心:)

于 2017-04-21T23:18:50.010 回答
0

首先你需要一个 redux reducer,它会监听来自 loadDataSuccess 的动作并缓存 state 中的数据。

其次,在您的史诗过滤流中并检查是否有任何数据处于状态。您可以使用 store.getState() 访问状态中的任何值。

const loadDataEpic = (action$, store) => 
  action$.ofType(LOAD_DATA_REQUEST)
    .filter(() => !store.getState().yourDataCache)
    .mergeMap(action => getData$(action.criteria)
      .map(x => loadDataSuccess(x.response))
      .catch(error => Observable.of(loadDataFailure(error.xhr)))
    );
于 2017-04-21T05:19:59.917 回答