在 redux observable 中,我如何在任何其他动作之前触发一个动作

in redux observable, how do i fire an action before any other action

背景:

我正在使用 epics 来管理请求。

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

我正在为每个请求使用令牌,但是在发送任何请求之前我想检查令牌是否过期,如果过期并且有宽限期,那么首先刷新令牌然后继续相应的动作

所有请求都有自己的 epics。

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

希望这能解释清楚。

// 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

不可能真正阻止 action 到达您的 reducers——实际上在它被提供给您的 epics 之前已经通过了它们——相反,您可以发送一个表示获取意图的 action,但是实际上并不是触发它的原因。例如UI 调度 FETCH_SOMETHING 并且一个史诗看到它,确认有一个有效的刷新令牌(或获得一个新令牌),然后发出另一个操作来实际触发获取,例如FETCH_SOMETHING_WITH_TOKEN.

在这种特殊情况下,虽然您可能会有许多 epics 具有相同的要求,但这样做可能会很乏味。有很多方法可以使这更容易。这里有一对:

将提取包装在帮助程序中

您可以编写一个帮助程序来为您进行检查,如果它需要刷新,它将请求并等待它,然后再继续。我会亲自处理单独的专用史诗中的实际刷新,以便您防止多个并发请求刷新和其他类似的事情。

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

"super-epics"

EDIT: This was my original suggestion but it's much more complicated than the one above. It has some benefits too, but IMO the one above will be easier to use and maintain.

您可以改为创建一个 "super-epic",一个自己创作并委托给其他 epics 的史诗。根史诗是 super-epic 的一个例子。 (我刚刚编造了这个词...哈哈)

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

这要复杂得多,但也比其他解决方案有优势。这是我所说内容的粗略概念,但它未经测试,可能不是 100% thought-out。将其视为灵感而非 copy-pasta.

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

如前所述,有很多方法可以解决这个问题。我可以设想创建某种你在 epics 中使用的辅助函数,它需要令牌而不是这种 "super-epic" 方法。做对您来说不那么复杂的事情。