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

Rxjs 在redux observable中,如何在任何其他操作之前触发一个操作,rxjs,redux-observable,Rxjs,Redux Observable,背景: 我使用epics来管理请求 对于我发送的每个请求,都会发送一个令牌,该令牌可能会过期,但可以在宽限期内刷新 我正在为每个请求使用令牌,但在发送任何请求之前,我想检查令牌是否过期,如果过期且有宽限期,则首先刷新令牌,然后继续执行相应的操作 所有的请求都有自己的史诗 现在,我尝试的是对所有操作进行预挂接,以检查令牌并刷新它,然后继续操作 希望这能解释 // epics for querying data // these epics are using a token, that is st

背景:

我使用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

它不可能真正阻止一个动作到达你的还原器——实际上在它被发送到你的epics之前它已经通过了还原器——相反,你可以发送一个动作,该动作发出获取意图的信号,但实际上不是触发它的原因。e、 g.UI分派FETCH_某物,epic看到它,确认存在有效的刷新令牌(或获取新令牌),然后发出另一个操作来实际触发FETCH,例如,使用令牌FETCH_某物

在这种特殊的情况下,虽然你可能会有许多相同要求的史诗,但这样做可能会变得单调乏味。有很多方法可以让这更容易。以下是几点:

在助手中包装提取 您可以编写一个助手来为您进行检查,如果需要刷新,它将请求并等待刷新后再继续。我个人会在单独的专用epic中处理实际刷新,这样可以防止多个并发刷新请求和其他类似的事情

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))
      )
    );
“超级史诗” 编辑:这是我最初的建议,但比上面的建议复杂得多。它也有一些好处,但是上面的一个会更容易使用和维护

你可以改为创作一部“超级史诗”,这部史诗本身就是一部史诗,并代表其他史诗。根史诗是超级史诗的一个例子。(我现在刚编好这个词……哈哈)

我们可能要做的一件事是区分任何随机操作和需要身份验证令牌的操作——您不希望检查身份验证令牌,并为每个已调度的操作刷新它。一种简单的方法是在操作中包含一些元数据,比如
{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);


如前所述,有很多方法可以解决这个问题。我可以通过invision创建一些辅助功能,您可以在epics中使用这些功能,这些功能需要令牌,而不是这种“超级epic”方法。做对你来说不那么复杂的事情。

不可能真正阻止一个动作到达你的减速器——实际上,在它发送给你的史诗之前,它已经通过了减速器——相反,你可以发送一个动作,它表示要取的意图,但实际上不是触发它的原因。e、 g.UI分派FETCH_某物,epic看到它,确认存在有效的刷新令牌(或获取新令牌),然后发出另一个操作来实际触发FETCH,例如,使用令牌FETCH_某物

在这种特殊的情况下,虽然你可能会有许多相同要求的史诗,但这样做可能会变得单调乏味。有很多方法可以让这更容易。以下是几点:

在助手中包装提取 您可以编写一个助手来为您进行检查,如果需要刷新,它将请求并等待刷新后再继续。我个人会在单独的专用epic中处理实际刷新,这样可以防止多个并发刷新请求和其他类似的事情

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))
      )
    );
“超级史诗” 编辑:这是我最初的建议,但比上面的建议复杂得多。它也有一些好处,但是上面的一个会更容易使用和维护

你可以改为创作一部“超级史诗”,这部史诗本身就是一部史诗,并代表其他史诗。根史诗是超级史诗的一个例子。(我现在刚编好这个词……哈哈)

我们可能要做的一件事是区分任何随机操作和需要身份验证令牌的操作——您不希望检查身份验证令牌,并为每个已调度的操作刷新它。一种简单的方法是在操作中包含一些元数据,比如
{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);


如前所述,有很多方法可以解决这个问题。我可以通过invision创建一些辅助功能,您可以在epics中使用这些功能,这些功能需要令牌,而不是这种“超级epic”方法。做对你来说不那么复杂的事情。

你能不能对这个问题更精确一点,最好用“这就是我目前所拥有的”和“这就是我被困的地方”来显示代码。@RichardMatsen对此表示抱歉。我现在已经提供了一些虚拟代码,请您对这个问题更精确一点,最好用“这就是我目前所拥有的”和“这就是我被卡住的地方”来显示代码。@RichardMatsen对此表示抱歉。我已经提供了一些虚拟代码,谢谢。谢谢你抽出时间回复。除了fetch之外,我还有一个使用相同令牌的套接字包装器。除了在redux中设置令牌之外,刷新令牌操作也会产生副作用(设置本地存储),从fetch/socket包装器启动操作让我感觉不舒服。另外,有时,当这些请求同时是多极请求时,fetch包装器可能会使用其中的每一个刷新令牌