如何处理Redux中复杂的副作用?

Tha*_*hai 45 javascript flux redux

我一直在努力寻找解决这个问题的方法......

我正在开发一个带有在线记分牌的游戏.玩家可以随时登录和注销.完成游戏后,玩家将看到记分牌,并查看自己的排名,并自动提交分数.

记分牌显示了玩家的排名和排行榜.

截图

当用户完成播放(提交分数)以及用户只想查看他们的排名时,使用记分板.

这是逻辑变得非常复杂的地方:

  • 如果用户已登录,则将首先提交分数.保存新记录后,将加载记分板.

  • 否则,将立即加载记分板.玩家将获得登录或注册的选项.之后,将提交分数,然后记分板将再次刷新.

  • 但是,如果没有要提交的分数(只需查看高分表).在这种情况下,只需下载播放器的现有记录.但由于此操作不会影响记分牌,因此记分牌和玩家记录应同时下载.

  • 有无限数量的级别.每个级别都有不同的记分牌.当用户查看记分板时,用户正在"观察"该记分板.当它关闭时,用户停止观察它.

  • 用户可以随时登录和注销.如果用户注销,则用户的排名应该消失,如果用户以另一个帐户登录,则应该获取并显示该帐户的排名信息.

    ...但是,此信息只应针对用户当前正在观察的记分板进行.

  • 对于查看操作,结果应缓存在内存中,以便如果用户重新订阅同一记分板,则不会提取.但是,如果提交了分数,则不应使用缓存.

  • 任何这些网络操作都可能失败,并且播放器必须能够重试它们.

  • 这些操作应该是原子的.所有状态都应该一次更新(没有中间状态).

目前,我能够使用Bacon.js(功能性反应式编程库)来解决这个问题,因为它提供了原子更新支持.代码非常简洁,但现在它是一个混乱的不可预测的意大利面条代码.

我开始看Redux.所以我尝试构建商店,并想出了类似的东西(用YAMLish语法):

user: (user information)
record:
  level1:
    status: (loading / completed / error)
    data:   (record data)
    error:  (error / null)
scoreboard:
  level1:
    status: (loading / completed / error)
    data:
      - (record data)
      - (record data)
      - (record data)
    error:  (error / null)
Run Code Online (Sandbox Code Playgroud)

问题变成:我在哪里放置副作用.

对于无副作用的动作,这变得非常容易.例如,在LOGOUT行动时,record减速器可以简单地将所有记录都抛出.

但是,某些行为确实有副作用.例如,如果我在提交分数之前没有登录,那么我成功登录,该SET_USER操作将用户保存到商店中.

但是因为我有一个要提交的分数,这个SET_USER动作也必须引发一个AJAX请求,同时设置record.levelN.statusloading.

问题是:当我以原子方式登录时,我如何表示应该发生副作用(得分提交)

在Elm架构中,更新程序在使用形式时也会发出副作用Action -> Model -> (Model, Effects Action),但在Redux中,它只是(State, Action) -> State.

Async Actions文档中,他们推荐的方法是将它们放在动作创建器中.这是否意味着提交分数的逻辑也必须放在动作创建者中以便成功登录?

function login (options) {
  return (dispatch) => {
    service.login(options).then(user => dispatch(setUser(user)))
  }
}

function setUser (user) {
  return (dispatch, getState) => {
    dispatch({ type: 'SET_USER', user })
    let scoreboards = getObservedScoreboards(getState())
    for (let scoreboard of scoreboards) {
      service.loadUserRanking(scoreboard.level)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我发现这有点奇怪,因为负责这种连锁反应的代码现在存在于两个地方:

  1. 在减速机中.当SET_USER动作被分派时,record减速机还必须设置的属于观察记分牌,以记录的状态loading.
  2. 在动作创建者中,执行获取/提交分数的实际副作用.

似乎我必须手动跟踪所有活跃的观察者.而在Bacon.js版本中,我做了类似这样的事情:

Bacon.once() // When first observing the scoreboard
.merge(resubmit?) // When resubmitting because of network error
.merge(user?.changes().filter(user => !!user).first()) // When user logs in (but only once)
.flatMapLatest(submitOrGetRanking(data))
Run Code Online (Sandbox Code Playgroud)

由于上面所有复杂的规则,实际的培根代码要长得多,这使得培根版本几乎不可读.

但培根自动跟踪所有活动订阅.这让我开始质疑它可能不值得切换,因为将其重写为Redux需要大量的手动处理.谁能建议一些指针?

Dan*_*mov 52

当你想要复杂的异步依赖时,只需使用Bacon,Rx,channels,sagas或其他异步抽象.您可以使用或不使用Redux.Redux示例:

observeSomething()
  .flatMap(someTransformation)
  .filter(someFilter)
  .map(createActionSomehow)
  .subscribe(store.dispatch);
Run Code Online (Sandbox Code Playgroud)

您可以按照自己喜欢的方式编写异步操作 - 唯一重要的部分是最终它们变成store.dispatch(action)调用.

Redux Thunk对于简单的应用程序已经足够了,但是随着您的异步需求变得更加复杂,您需要使用真正的异步组合抽象,而Redux并不关心您使用哪一个.


更新:一段时间过去了,出现了一些新的解决方案.我建议你查看Redux Saga,它已经成为Redux中异步控制流程的一个相当流行的解决方案.

  • 中间件用于简单的动作转换 - 不适用于复杂的构图.只要把它放在Redux之外,就像常规函数返回可观察量一样,无论如何你喜欢.您也可以使用https://github.com/acdlite/redux-rx之类的东西. (4认同)
  • 这个异步组合应该正常驻留的位置?我的意思是,在redux之外的某个地方,或者它更适合中间件? (3认同)

Seb*_*ber 17

编辑:现在有一个受这些想法启发的redux-saga项目

这里有一些不错的资源


Flux/Redux的灵感来自后端事件流处理(无论名称是什么:eventsourcing,CQRS,CEP,lambda架构......).

我们可以将Flux的ActionCreators/Actions与Commands/Events(通常在后端系统中使用的术语)进行比较.

在这些后端体系结构中,我们使用的模式通常称为Saga或Process Manager.基本上它是系统中接收事件,可以管理自己的状态,然后可以发出新命令的部分.为简单起见,它有点像实现IFTTT(If-This-Then-That).

您可以使用FRP和BaconJS实现此功能,但您也可以在Redux reducer之上实现此功能.

function submitScoreAfterLoginSaga(action, state = {}) {  
  switch (action.type) {

    case SCORE_RECORDED:
      // We record the score for later use if USER_LOGGED_IN is fired
      state = Object.assign({}, state, {score: action.score}

    case USER_LOGGED_IN: 
      if ( state.score ) {
        // Trigger ActionCreator here to submit that score
        dispatch(sendScore(state.score))
      } 
    break;

  }
  return state;
}
Run Code Online (Sandbox Code Playgroud)

为了说清楚:从reducer计算到驱动React渲染的状态应该绝对保持纯粹!但并非所有应用程序状态都有触发渲染的目的,这就是需要将应用程序的不同分离部分与复杂规则同步的情况.这个"传奇"的状态不应该触发React渲染!

我不认为Redux提供任何支持这种模式的东西,但你可以轻松地自己实现它.

我在我们的启动框架中完成了这个,这个模式运行正常.它允许我们处理IFTTT之类的事情:

  • 当用户登录处于活动状态且用户关闭Popup1时,则打开Popup2,并显示一些提示工具提示.

  • 当用户使用移动网站并打开Menu2时,请关闭Menu1

重要提示:如果您正在使用某些框架(如Redux)的撤消/重做/重播功能,重要的是在重放事件日志期间,所有这些传奇都是无线的,因为您不希望在重播期间触发新事件!