在redux observable中,如何在任何其他操作之前触发操作

时间:2018-01-18 04:25:47

标签: rxjs redux-observable

背景:

我正在使用史诗来管理请求。

对于我发送令牌的每个请求,该请求可能会过期,但可以在宽限期内刷新。

我正在为每个请求使用令牌,但在发送任何请求之前我想检查令牌是否已过期,如果已过期且有宽限期,则首先刷新令牌然后继续执行相应的操作< / p>

所有请求都有自己的史诗。

现在我尝试的是检查令牌的所有操作的预挂钩,可能会刷新它然后继续操作。

希望这能解释。

// epics for querying data
// these epics are using a token, that is stored in redux state.

const getMenuEpic = action$ => ....

const getFoodListEpic = action$ => ....

const getFoodItemEpic = action$ => ....

...


// helper function to check 
// if token has expired

const hasTokenExpired = (token) => .....

// refresh token 
// this returns a new token in the promise

const refreshToken = fetch('http://.../refresh-toekn')

// i am trying to make an epic, that will fire 
// before any actions in the application
// stop the action (let's call it action A)
// get token from redux state, 
// verify if is valid or not
// if invalid call refresh token (async process), 
// and when refresh token finished, proceed with the incoming action A
// if the token was valid then continue with action A.

const refreshEpic = (action$, store) => 
 action$.map(() => store.getState().token)
  .filter(Boolean)
  .filter(token => hasTokenExpired(token))
  .mergeMap(() => refreshToken()) ...

 ......

但这种方法不适用于refreshEpic

1 个答案:

答案 0 :(得分:6)

不可能真正阻止某个行动到达你的减刑者 - 它实际上已经通过了它们才能被你的史诗给予 - 而你可以发出一个动作表示获取的意图,但实际上并不触发它。例如UI调度FETCH_SOMETHING并且epic看到它,确认有一个有效的刷新令牌(或获取一个新的令牌),然后发出另一个动作以实际触发获取,例如FETCH_SOMETHING_WITH_TOKEN。

在这种特殊情况下,虽然你可能会有很多具有相同要求的史诗,但这样做可能会很乏味。有很多方法可以让这更容易。这是一对夫妇:

将fetch包装在帮助器中

您可以编写一个帮助程序来执行检查,如果需要刷新,它将在继续之前请求并等待它。我个人会在单独的专用史诗中处理实际的刷新,以防止多个并发请求刷新以及其他类似的事情。

const requireValidToken = (action$, store, callback) => {
  const needsRefresh = hasTokenExpired(store.getState().token);

  if (needsRefresh) {
    return action$.ofType(REFRESH_TOKEN_SUCCESS)
      .take(1)
      .takeUntil(action$.ofType(REFRESH_TOKEN_FAILED))
      .mergeMap(() => callback(store.getState().token))
      .startWith({ type: REFRESH_TOKEN });
  } else {
    return callback(store.getState().token);
  }
};

const getMenuEpic = (action$, store) =>
  action$.ofType(GET_MENU)
    .switchMap(() =>
      requireValidToken(action$, store, token =>
        actuallyGetMenu(token)
          .map(response => getMenuSuccess(response))
      )
    );

&#34;超史诗&#34;

  

编辑:这是我原来的建议,但它比上面的复杂得多。它也有一些好处,但上面的IMO将更容易使用和维护。

你可以创作一部超级史诗&#34;这是一部史诗,它本身就是其他史诗的作曲和代表。根史诗是超级史诗的一个例子。 (我现在刚刚完成那个术语......大声笑)

我们可能想要做的一件事是区分任何随机操作和需要身份验证令牌的操作 - 您不想检查身份验证令牌并为每一次操作刷新它出动。一种简单的方法是在操作中包含一些元数据,例如{ meta: { requiresAuth: true } }

这要复杂得多,但也比其他解决方案更具优势。这里对我正在谈论的内容有一个粗略的了解,但它没有经过考验,可能不是100%经过深思熟虑。考虑它的灵感而不是复制意大利面。

// action creator helper to add the requiresAuth metadata
const requiresAuth = action => ({
  ...action,
  meta: {
    ...action.meta,
    requiresAuth: true
  }
});
// action creators
const getMenu = id => requiresAuth({
  type: 'GET_MENU',
  id
});
const getFoodList = () => requiresAuth({
  type: 'GET_FOOD_LIST'
});

// epics
const getMenuEpic = action$ => stuff
const getFoodListEpic = action$ => stuff

const refreshTokenEpic = action$ =>
  action$.ofType(REFRESH_TOKEN)
    // If there's already a pending refreshToken() we'll ignore the new
    // request to do it again since its redundant. If you instead want to
    // cancel the pending one and start again, use switchMap()
    .exhaustMap(() =>
      Observable.from(refreshToken())
        .map(response => ({
          type: REFRESH_TOKEN_SUCCESS,
          token: response.token
        }))
        // probably should force user to re-login or whatevs
        .catch(error => Observable.of({
          type: REFRESH_TOKEN_FAILED,
          error
        }))
    );


// factory to create a "super-epic" which will only
// pass along requiresAuth actions when we have a
// valid token, refreshing it if needed before.
const createRequiresTokenEpic = (...epics) => (action$, ...rest) => {
  // The epics we're delegating for
  const delegatorEpic = combineEpics(...epics);
  // We need some way to emit REFRESH_TOKEN actions
  // so I just hacked it with a Subject. There is
  // prolly a more elegant way to do this but #YOLO
  const output$ = new Subject();

  // This becomes action$ for all the epics we're delegating
  // for. This will hold off on giving an action to those
  // epics until we have a valid token. But remember,
  // this doesn't delay your *reducers* from seeing it
  // as its already been through them!
  const filteredAction$ = action$
    .mergeMap(action => {
      if (action.meta && action.meta.requiresAuth) {
        const needsRefresh = hasTokenExpired(store.getState().token);

        if (needsRefresh) {
          // Kick off the refreshing of the token
          output$.next({ type: REFRESH_TOKEN });

          // Wait for a successful refresh
          return action$.ofType(REFRESH_TOKEN_SUCCESS)
            .take(1)
            .mapTo(action)
            .takeUntil(action$.ofType(REFRESH_TOKEN_FAILED));
            // Its wise to handle the case when refreshing fails.
            // This example just gives up and never sends the
            // original action through because presumably
            // this is a fatal app state and should be handled
            // in refreshTokenEpic (.e.g. force relogin)
        }
      }

      // Actions which don't require auth are passed through as-is
      return Observable.of(action);
    });

  return Observable.merge(
    delegatorEpic(filteredAction$, ...rest),
    output$
  );
};

const requiresTokenEpic = createRequiresTokenEpic(getMenuEpic, getFoodList, ...etc);

如上所述,有很多方法可以解决这个问题。我可以在你的史诗中创建某种辅助函数,这些函数需要代币而不是这个&#34;超级史诗&#34;做法。为你做的事情似乎不那么复杂。