我一直在努力寻找解决这个问题的方法......
我正在开发一个带有在线记分牌的游戏。玩家可以随时登录和注销。完成一场比赛后,玩家会看到计分板,看到自己的排名,分数会自动提交。
记分牌显示玩家的排名和排行榜。
当用户完成游戏(提交分数)和用户只想查看他们的排名时,都会使用记分牌。
这是逻辑变得非常复杂的地方:
如果用户已登录,则将首先提交分数。保存新记录后,将加载记分牌。
否则,计分板将立即加载。玩家将可以选择登录或注册。之后会提交分数,然后再次刷新记分牌。
但是,如果没有要提交的分数(只是查看高分表)。在这种情况下,只需下载播放器的现有记录。但是由于这个动作不会影响记分牌,所以记分牌和球员的记录应该同时下载。
有无限数量的级别。每个级别都有不同的记分牌。当用户查看记分牌时,用户正在“观察”该记分牌。当它关闭时,用户停止观察它。
用户可以随时登录和注销。如果用户退出,用户的排名应该消失,如果用户作为另一个帐户登录,那么应该获取并显示该帐户的排名信息。
...但是仅对用户当前正在观察的记分牌进行此信息获取。
对于查看操作,结果应该缓存在内存中,这样如果用户重新订阅同一个计分板,就不会提取。但是,如果提交了分数,则不应使用缓存。
这些网络操作中的任何一个都可能失败,玩家必须能够重试它们。
这些操作应该是原子的。所有状态都应该一次性更新(没有中间状态)。
目前,我可以使用 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)
问题变成了:我把副作用放在哪里。
对于无副作用的操作,这变得非常容易。例如,在LOGOUT
操作时,record
reducer 可以简单地删除所有记录。
但是,某些操作确实有副作用。例如,如果我在提交分数之前没有登录,那么我登录成功,该SET_USER
操作将用户保存到商店中。
但是因为我有一个分数要提交,所以这个SET_USER
动作也必须导致一个 AJAX 请求被触发,同时,record.levelN.status
将loading
.
问题是:当我以原子方式登录时,如何表示副作用(分数提交)应该发生?
在 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)
}
}
}
我觉得这有点奇怪,因为负责这种连锁反应的代码现在存在于两个地方:
- 在减速机中。
SET_USER
调度操作时,reducerrecord
还必须将属于观察到的记分板的记录的状态设置为loading
。 - 在动作创建者中,它执行获取/提交分数的实际副作用。
似乎我还必须手动跟踪所有活跃的观察者。而在 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))
实际的培根代码要长得多,因为上面所有复杂的规则,使得培根版本几乎不可读。
但是培根会自动跟踪所有活跃的订阅。这让我开始质疑它可能不值得切换,因为将它重写到 Redux 需要大量的手动处理。任何人都可以提出一些建议吗?