我一直在努力寻找解决这个问题的方法......
我正在开发一款带有在线记分牌的游戏。玩家可以随时登录和注销。完成游戏后,玩家将看到记分牌,并查看自己的排名,分数将自动提交。
记分牌显示玩家的排名和排行榜。
记分牌既可以在用户完成比赛时使用(提交分数),也可以在用户只想查看他们的排名时使用。
这是逻辑变得非常复杂的地方:
如果用户已登录,则会先提交分数。保存新记录后,将加载记分板。
否则,将立即加载记分板。玩家将获得登录或注册的选项。之后,将提交分数,然后记分板将再次刷新。
但是,如果没有要提交的分数(只需查看高分表)。在这种情况下,只需下载播放器的现有记录。但由于此操作不会影响记分牌,因此记分牌和玩家记录应同时下载。
有无限数量的关卡。每个级别都有不同的记分牌。当用户查看记分板时,用户正在“观察”该记分板。当它关闭时,用户停止观察它。
用户可以随时登录和注销。如果用户注销,则用户的排名应该消失,如果用户以其他帐户登录,则应该获取并显示该帐户的排名信息。
...但是此信息只能用于当前用户正在观察的记分板。
对于查看操作,结果应缓存在内存中,以便如果用户重新订阅同一记分板,则不会进行提取。但是,如果提交的分数,则不应使用缓存。
任何这些网络操作都可能失败,播放器必须能够重试。
这些操作应该是原子的。所有州都应该一次更新(没有中间状态)。
目前,我能够使用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
减速器可以简单地关闭所有记录。
然而,某些行为确实有副作用。例如,如果我在提交分数之前没有登录,那么我成功登录,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
操作时,record
减速器还必须将属于观察记分板的记录的状态设置为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需要大量的手动处理。有谁能建议一些指针?
答案 0 :(得分:52)
当你想要复杂的异步依赖时,只需使用Bacon,Rx,channels,sagas或其他异步抽象。您可以使用或不使用Redux。 Redux示例:
observeSomething()
.flatMap(someTransformation)
.filter(someFilter)
.map(createActionSomehow)
.subscribe(store.dispatch);
您可以按照自己喜欢的方式撰写异步操作 - 唯一重要的部分是最终转化为store.dispatch(action)
次调用。
Redux Thunk对于简单的应用程序已经足够了,但是当您的异步需求变得更加复杂时,您需要使用真正的异步组合抽象,而Redux并不关心您使用哪一个。
更新:一段时间过去了,出现了一些新的解决方案。我建议你查看Redux Saga,这已经成为Redux中异步控制流的一个相当流行的解决方案。
答案 1 :(得分:17)
修改:现在有一个受这些想法启发的redux-saga项目
以下是一些不错的资源
Flux / Redux的灵感来自后端事件流处理(无论名称是什么:eventsourcing,CQRS,CEP,lambda架构......)。
我们可以将Flux的ActionCreators / Actions与命令/事件(通常在后端系统中使用的术语)进行比较。
在这些后端架构中,我们使用的模式通常称为Saga或流程管理器。基本上它是系统中接收事件,可以管理自己的状态,然后可以发出新命令的部分。为简单起见,这有点像实现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;
}
要说清楚:从reducer计算到驱动React渲染的状态应该绝对保持纯净!但并非所有应用程序状态都有触发渲染的目的,这就是需要将应用程序的不同分离部分与复杂规则同步的情况。这个“传奇”的状态不应该触发React渲染!
我不认为Redux提供任何支持这种模式的东西,但你可以轻松地自己实现它。
我已经在我们的startup framework中完成了这个,这个模式运行正常。它允许我们处理IFTTT之类的事情:
当用户入职处于活动状态且用户关闭Popup1时,请打开Popup2,并显示一些提示工具提示。
当用户使用移动网站并打开Menu2时,请关闭Menu1
重要:如果您正在使用某些框架(如Redux)的撤消/重做/重播功能,重要的是在重播事件日志期间,所有这些传奇都是无线的,因为您不会t希望在重播期间触发新事件!