发出动作,然后使用 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_LOGINLOAD_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 的这些非凡课程:

Jay Phelps 也在 redux-observable 上给出了 a brilliant talk,绝对值得一看。