我在redux-observable中有以下场景。我有一个组件,它检测要使用的后端,并应设置api-client使用的后端URL。客户端和URL都保存在全局状态对象中。
执行顺序应该是:1。检查后端2. on error替换状态3中保持的后端URL 3.触发3个操作以使用新的后端状态URL加载资源
到目前为止我所做的是在步骤1中从我的史诗中访问状态$对象并修改支持的URL。这似乎只有一半的工作。状态由在3中触发的操作更新。仍然看到旧状态并使用错误的后端。
如果依赖于执行顺序,在操作之间更新状态的标准方法是什么?
我的API-Epic看起来像这样:
export const authenticate = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
mergeMap(action =>
from(state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)).pipe(
map(bearer => apiActions.authenticatedSuccess(bearer))
)
)
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
concatMap(action => concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
)
我发现用户在GitHub和StackOverflow上讨论的一种常见方法是链接多个史诗,就像我相信你的例子试图展示的那样。第一个史诗在“完成”时发出动作。 reducer侦听此操作并更新商店的状态。第二部史诗(或许多额外的史诗,如果你想要并发操作)听取同样的动作并启动工作流程的下一个序列。次要史诗在减速器之后运行,因此看到更新的状态。 From the docs:
在减速器已经收到它们之后,Epics与正常的Redux调度通道一起运行......
我发现链接方法可以很好地解耦更大工作流程的各个阶段。您可能希望通过设计原因(例如关注点分离)进行解耦,重用较大工作流程的较小部分,或者制作较小的单元以便于测试。当您的史诗在较大工作流程的不同阶段之间调度操作时,这是一种简单的实现方法。
但是,请记住,state$
是一个可观察的。您可以使用它来获取任何时间点的当前值 - 包括在单个史诗中调度不同的动作之间。例如,考虑以下因素并假设我们的商店保留了一个简单的计数器:
export const workflow = (action$, state$) => action$.pipe(
ofType(constants.START),
withLatestFrom(state$),
mergeMap(([action, state]) => // "state" is the value when the START action was dispatched
concat(
of(actions.increment()),
state$.pipe(
first(),
map(state => // this new "state" is the _incremented_ value!
actions.decrement()),
),
defer(() => {
const state = state$.value // this new "state" is now the _decremented_ value!
return empty()
}),
),
),
)
有很多方法可以从观察中获得当前状态!
关于示例中的以下代码行:
state$.value.apiState.apiClient.authenticate(state$.value.apiState.bearer)
首先,使用状态传递API客户端不是常见/推荐的模式。你可能想看看injecting the API client as a dependency你的史诗(这使得单元测试更容易!)。其次,不清楚API客户端如何从状态获取当前后端URL。 API客户端是否可能正在使用状态的缓存版本?如果是,您可能想要重构您的authenticate
方法并传入当前后端URL。
这是一个处理错误并包含上述内容的示例:
/**
* Let's assume the state looks like the following:
* state: {
* apiState: {
* backend: "URL",
* bearer: "token"
* }
*/
// Note how the API client is injected as a dependency
export const authenticate = (action$, state$, { apiClient }) => action$.pipe(
ofType(actions.API_AUTHENTICATE),
withLatestFrom(state$),
mergeMap(([action, state]) =>
// Try to authenticate against the current backend URL
from(apiClient.authenticate(state.apiState.backend, state.apiState.bearer)).pipe(
// On success, dispatch an action to kick off the chained epic(s)
map(bearer => apiActions.authenticatedSuccess(bearer)),
// On failure, dispatch two actions:
// 1) an action that replaces the backend URL in the state
// 2) an action that restarts _this_ epic using the new/replaced backend URL
catchError(error$ => of(apiActions.authenticatedFailed(), apiActions.authenticate()),
),
),
)
export const authenticatedSuccess = (action$, state$) => action$.pipe(
ofType(actions.API_AUTHENTICATED_SUCCESS),
...
)
此外,请记住,当链接史诗时,像concat
这样的结构将不会等待链式史诗“完成”。例如:
concat(
of(resourceActions.doLoadAResource()),
of(resourceActions.doLoadOtherResource()),
of(resourceActions.doLoadSomethingElse()))
)
如果这些doLoadXXX
行动中的每一个都“开始”史诗,那么这三个行动都可能同时发生。每个动作将一个接一个地发送,每个史诗将“开始”一个接一个地运行,而不必等待前一个“完成”。这是因为史诗永远不会真正完成。它们是长寿的,永无止境的流。如果你想在doLoadAResource
之后运行doLoadOtherResource
,你需要明确地等待一些标识doLoadAResource
何时完成的信号。