如何使用 redux-observable 缓存 ajax 数据 - RxJS 5

How to cache ajax data with redux-observable - RxJS 5

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

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

下面是我试过的代码,但是数据没有被缓存,每当我发送 LOAD_DATA_REQUEST.

时都会调用 API
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();

首先,您需要一个 redux reducer,它将侦听来自 loadDataSuccess 的操作并将数据缓存在状态中。

其次,在你的史诗中过滤流并检查是否有任何数据处于状态。您可以使用 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)))
    );

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

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

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)状态存储在其自己的 reducer 中更容易,因此您不需要将其与实际响应有效载荷进行额外合并——我个人这样做那个,但我在这个例子中没有,因为我大多数人都没有,它有时会把它们扔掉。


但是,您澄清说您希望改用 RxJS 重播,所以这是一种实现方式

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

如果您想基于 "criteria" 进行缓存,您可以创建自己的小帮手来执行此操作:

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 是在给定相同意图的情况下始终具有严格引用相等性的东西。如果它是一个对象,而你每次都创建一个新对象,它们将永远不会命中缓存,因为它们不相同 reference,它们具有不同的身份——无论它们是否具有相同的内容。

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。有时候因为各种原因无法逃避,但还是努力吧!正如您看到的缓存内容,它会让您的生活更轻松。


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