发出动作,然后使用 redux-observable 和 rxjs 请求并发出另一个动作
Emit action, then request and emit another with redux-observable and rxjs
所以,我有一个接收 SUBMIT_LOGIN 动作的史诗,然后它应该触发 generateDeviceId 函数,returns 一个带有 id 作为负载的动作。在 reducer 处理并更新商店之后,它应该请求登录,然后将其解析为商店,最后将用户重定向到我们的仪表板
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.mapTo(generateDeviceId(uuidv1()))
.flatMap(({payload}) => login(payload.email, payload.password)
.flatMap(({response}) => [resolveLogin(response.content), loadAbout()])
);
ps: login
函数是 ajax
来自 rx-dom
的 returns 流:
const AjaxRequest = (method, url, data) => {
const state = store.getState();
const {token, deviceId} = state.user;
return ajax({
method,
timeout: 10000,
body: data,
responseType: 'json',
url: url,
headers: {
token,
'device-id': deviceId,
'Content-Type': 'application/json'
}
});
};
const login = (email, password) => AjaxRequest('post', 'sign_in', {email, password});
ps2: uuidv1
函数只生成一个随机密钥(它是一个库)
我认为(实际上我确定)我做错了,但两天后我真的不知道如何进行。 :/
更新
在 Sergey 的第一次更新后,我将我的史诗更改为那个,但不幸的是由于某些原因 rx-dom's
ajax 不像 Sergey 的 login$
observable 那样工作。我们目前正在处理这个问题。
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({payload}) =>
Observable.of(generateDeviceId(uuid()))
.concat(login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
更新 2
在 Sergey 的第二次更新后,我再次更改了我的代码并最终得到了一个解决方案,我使用 two epics
和 .concatMap
运算符来 synchronously
调度操作,它works as expected
.
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.concatMap(({payload}) => [
generateDeviceId(uuid()),
requestLogin(payload.email, payload.password)
]);
const requestLoginEpic = (action$) =>
action$
.ofType(REQUEST_LOGIN)
.mergeMap(({payload}) => login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
如果我没看错,你希望你的史诗产生以下一系列动作来响应每个 SUBMIT_LOGIN
:
GENERATE_DEVICE_ID -- RESOLVE_LOGIN -- LOAD_ABOUT
另外,我猜GENERATE_DEVICE_ID
需要在收到SUBMIT_LOGIN
后立即发出,
而 RESOLVE_LOGIN
和 LOAD_ABOUT
应该仅在 login()
返回的流发出后发出。
如果我的猜测是正确的,那么你只需要启动嵌套的 observable(每个 SUBMIT_LOGIN
创建一个)
使用 GENERATE_DEVICE_ID
操作和 startWith
运算符正是这样做的:
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
login(payload.email, payload.password)
.mergeMap(({ response }) => Rx.Observable.of(resolveLogin(response.content), loadAbout()))
.startWith(generateDeviceId(uuidv1()))
);
更新: 一种可能的替代方法是使用 concat
运算符:obs1.concat(obs2)
仅在 [=28= 时订阅 obs2
] 已完成。
另请注意,如果 login()
需要在 GENERATE_DEVICE_ID
被调度后调用,您可能希望将其包装在 "cold" observable 中:
const login$ = payload =>
Rx.Observable.create(observer => {
return login(payload.email, payload.password).subscribe(observer);
});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
Rx.Observable.of(generateDeviceId(uuidv1()))
.concat(login$(payload).map(({ response }) => resolveLogin(response.content)))
.concat(Rx.Observable.of(loadAbout()))
);
这样 GENERATE_DEVICE_ID
在调用 login()
之前发出,即序列为
GENERATE_DEVICE_ID -- login() -- RESOLVE_LOGIN -- LOAD_ABOUT
更新 2: login()
无法按预期工作的原因是因为它依赖于外部状态 (const state = getCurrentState()
),这在调用 login()
以及订阅 login()
返回的可观察对象的时间点。 AjaxRequest
捕获调用 login()
时的状态,这发生在 GENERATE_DEVICE_ID
被调度到商店之前。那时还没有执行网络请求,但是 ajax
observable 已经基于错误的状态进行了配置。
为了看看会发生什么,让我们稍微简化一下并以这种方式重写史诗:
const createInnerObservable = submitLoginAction => {
return Observable.of(generateDeviceId()).concat(login());
}
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN).mergeMap(createInnerObservable);
当SUBMIT_LOGIN
动作到达时,mergeMap()
首先调用createInnerObservable()
函数。该函数需要创建一个新的可观察对象,为此它必须调用 generateDeviceId()
和 login()
函数。当 login()
被调用时,状态仍然是旧的,因为此时内部 observable 尚未创建,因此 GENERATE_DEVICE_ID
没有机会被调度。因此 login()
returns 一个 ajax
可观察对象配置了一个旧数据,它成为了结果内部可观察对象的一部分。一旦 createInnerObservable()
returns,mergeMap()
订阅返回的内部可观察对象并开始发出值。 GENERATE_DEVICE_ID
排在第一位,被分派到商店并更改状态。之后,ajax
observable(现在是内部 observable 的一部分)被订阅并执行网络请求。但是新状态对此没有影响,因为 ajax
observable 已经用旧数据初始化。
将 login
包装到 Observable.create
中会推迟调用,直到订阅了 Observable.create
返回的可观察对象,此时状态已经是 up-to-date。
另一种方法是引入一个额外的史诗,它会对 GENERATE_DEVICE_ID
操作(或不同的操作,以适合您的域的操作为准)做出反应并发送登录请求,例如:
const submitLogin = payload => ({ type: "SUBMIT_LOGIN", payload });
// SUBMIT_LOGIN_REQUESTED is what used to be called SUBMIT_LOGIN
const submitLoginRequestedEpic = action$ =>
action$.ofType(SUBMIT_LOGIN_REQUESTED)
.mergeMap(({ payload }) => Rx.Observable.of(
generateDeviceId(uuidv1()),
submitLogin(payload))
);
const submitLoginEpic = (action$, store) =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) => {
// explicitly pass all the data required to login
const { token, deviceId } = store.getState().user;
return login(payload.email, payload.password, token, deviceId)
.map(({ response }) => resolveLogin(response.content))
.concat(loadAbout());
});
学习资源
由于 redux-observable
基于 RxJS,因此首先熟悉 Rx 是有意义的。
我强烈推荐观看 André Staltz 的 "You will learn RxJS" 演讲。它应该让您直观地了解什么是 observables 以及它们在幕后如何工作。
André 还撰写了关于 egghead 的这些非凡课程:
- Creating Observables from scratch
- Operators in Depth
- Use Higher Order Observables in RxJS Effectively
Jay Phelps 也在 redux-observable
上给出了 a brilliant talk,绝对值得一看。
所以,我有一个接收 SUBMIT_LOGIN 动作的史诗,然后它应该触发 generateDeviceId 函数,returns 一个带有 id 作为负载的动作。在 reducer 处理并更新商店之后,它应该请求登录,然后将其解析为商店,最后将用户重定向到我们的仪表板
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.mapTo(generateDeviceId(uuidv1()))
.flatMap(({payload}) => login(payload.email, payload.password)
.flatMap(({response}) => [resolveLogin(response.content), loadAbout()])
);
ps: login
函数是 ajax
来自 rx-dom
的 returns 流:
const AjaxRequest = (method, url, data) => {
const state = store.getState();
const {token, deviceId} = state.user;
return ajax({
method,
timeout: 10000,
body: data,
responseType: 'json',
url: url,
headers: {
token,
'device-id': deviceId,
'Content-Type': 'application/json'
}
});
};
const login = (email, password) => AjaxRequest('post', 'sign_in', {email, password});
ps2: uuidv1
函数只生成一个随机密钥(它是一个库)
我认为(实际上我确定)我做错了,但两天后我真的不知道如何进行。 :/
更新
在 Sergey 的第一次更新后,我将我的史诗更改为那个,但不幸的是由于某些原因 rx-dom's
ajax 不像 Sergey 的 login$
observable 那样工作。我们目前正在处理这个问题。
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({payload}) =>
Observable.of(generateDeviceId(uuid()))
.concat(login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
更新 2
在 Sergey 的第二次更新后,我再次更改了我的代码并最终得到了一个解决方案,我使用 two epics
和 .concatMap
运算符来 synchronously
调度操作,它works as expected
.
const generateDeviceId = (deviceId) => (({type: GENERATE_DEVICE_ID, payload: deviceId}));
const resolveLogin = (response) => ({type: RESOLVE_LOGIN, payload: response});
const submitLogin = (email, password) => ({type: SUBMIT_LOGIN, payload: {email, password}});
const requestLogin = (email, password) => ({type: REQUEST_LOGIN, payload: {email, password}});
const loadAbout = () => ({type: LOAD_ABOUT});
const submitLoginEpic = (action$) =>
action$
.ofType(SUBMIT_LOGIN)
.concatMap(({payload}) => [
generateDeviceId(uuid()),
requestLogin(payload.email, payload.password)
]);
const requestLoginEpic = (action$) =>
action$
.ofType(REQUEST_LOGIN)
.mergeMap(({payload}) => login(payload.email, payload.password)
.concatMap(({response}) => [resolveLogin(response.content), loadAbout()])
如果我没看错,你希望你的史诗产生以下一系列动作来响应每个 SUBMIT_LOGIN
:
GENERATE_DEVICE_ID -- RESOLVE_LOGIN -- LOAD_ABOUT
另外,我猜GENERATE_DEVICE_ID
需要在收到SUBMIT_LOGIN
后立即发出,
而 RESOLVE_LOGIN
和 LOAD_ABOUT
应该仅在 login()
返回的流发出后发出。
如果我的猜测是正确的,那么你只需要启动嵌套的 observable(每个 SUBMIT_LOGIN
创建一个)
使用 GENERATE_DEVICE_ID
操作和 startWith
运算符正是这样做的:
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
login(payload.email, payload.password)
.mergeMap(({ response }) => Rx.Observable.of(resolveLogin(response.content), loadAbout()))
.startWith(generateDeviceId(uuidv1()))
);
更新: 一种可能的替代方法是使用 concat
运算符:obs1.concat(obs2)
仅在 [=28= 时订阅 obs2
] 已完成。
另请注意,如果 login()
需要在 GENERATE_DEVICE_ID
被调度后调用,您可能希望将其包装在 "cold" observable 中:
const login$ = payload =>
Rx.Observable.create(observer => {
return login(payload.email, payload.password).subscribe(observer);
});
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) =>
Rx.Observable.of(generateDeviceId(uuidv1()))
.concat(login$(payload).map(({ response }) => resolveLogin(response.content)))
.concat(Rx.Observable.of(loadAbout()))
);
这样 GENERATE_DEVICE_ID
在调用 login()
之前发出,即序列为
GENERATE_DEVICE_ID -- login() -- RESOLVE_LOGIN -- LOAD_ABOUT
更新 2: login()
无法按预期工作的原因是因为它依赖于外部状态 (const state = getCurrentState()
),这在调用 login()
以及订阅 login()
返回的可观察对象的时间点。 AjaxRequest
捕获调用 login()
时的状态,这发生在 GENERATE_DEVICE_ID
被调度到商店之前。那时还没有执行网络请求,但是 ajax
observable 已经基于错误的状态进行了配置。
为了看看会发生什么,让我们稍微简化一下并以这种方式重写史诗:
const createInnerObservable = submitLoginAction => {
return Observable.of(generateDeviceId()).concat(login());
}
const submitLoginEpic = action$ =>
action$.ofType(SUBMIT_LOGIN).mergeMap(createInnerObservable);
当SUBMIT_LOGIN
动作到达时,mergeMap()
首先调用createInnerObservable()
函数。该函数需要创建一个新的可观察对象,为此它必须调用 generateDeviceId()
和 login()
函数。当 login()
被调用时,状态仍然是旧的,因为此时内部 observable 尚未创建,因此 GENERATE_DEVICE_ID
没有机会被调度。因此 login()
returns 一个 ajax
可观察对象配置了一个旧数据,它成为了结果内部可观察对象的一部分。一旦 createInnerObservable()
returns,mergeMap()
订阅返回的内部可观察对象并开始发出值。 GENERATE_DEVICE_ID
排在第一位,被分派到商店并更改状态。之后,ajax
observable(现在是内部 observable 的一部分)被订阅并执行网络请求。但是新状态对此没有影响,因为 ajax
observable 已经用旧数据初始化。
将 login
包装到 Observable.create
中会推迟调用,直到订阅了 Observable.create
返回的可观察对象,此时状态已经是 up-to-date。
另一种方法是引入一个额外的史诗,它会对 GENERATE_DEVICE_ID
操作(或不同的操作,以适合您的域的操作为准)做出反应并发送登录请求,例如:
const submitLogin = payload => ({ type: "SUBMIT_LOGIN", payload });
// SUBMIT_LOGIN_REQUESTED is what used to be called SUBMIT_LOGIN
const submitLoginRequestedEpic = action$ =>
action$.ofType(SUBMIT_LOGIN_REQUESTED)
.mergeMap(({ payload }) => Rx.Observable.of(
generateDeviceId(uuidv1()),
submitLogin(payload))
);
const submitLoginEpic = (action$, store) =>
action$.ofType(SUBMIT_LOGIN)
.mergeMap(({ payload }) => {
// explicitly pass all the data required to login
const { token, deviceId } = store.getState().user;
return login(payload.email, payload.password, token, deviceId)
.map(({ response }) => resolveLogin(response.content))
.concat(loadAbout());
});
学习资源
由于 redux-observable
基于 RxJS,因此首先熟悉 Rx 是有意义的。
我强烈推荐观看 André Staltz 的 "You will learn RxJS" 演讲。它应该让您直观地了解什么是 observables 以及它们在幕后如何工作。
André 还撰写了关于 egghead 的这些非凡课程:
- Creating Observables from scratch
- Operators in Depth
- Use Higher Order Observables in RxJS Effectively
Jay Phelps 也在 redux-observable
上给出了 a brilliant talk,绝对值得一看。