在 react-router onEnter 钩子中懒惰地添加新的 epics 是一种有效的做法吗?

Is it an efficient practice to add new epics lazily inside react-router onEnter hooks?

将 redux-observable 与 react-router 一起使用时,按照 here 文档 here 中 [= 的 onEnter 挂钩中的说明异步添加新的 epics 是否有意义68=] 路线变更期间?

./epics/index.js

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineEpics } from 'redux-observable';

import { epic1 } from './epic1'
import { epic2 } from './epic2'

export const epic$ = new BehaviorSubject(combineEpics(epic1, epic2));
export const rootEpic = (action$, store) =>
    epic$.mergeMap(epic =>
        epic(action$, store)
    );

export {
    rootEpic
};

...routes/SomePath/index.js

import { epic$ } from './../../../../epics/index'
import { epic3 } from './../../../../epics/epic3'

module.exports = {
    path: 'some-path',
    getComponent( location, cb ) {
        require.ensure( [], ( require ) => {
            cb( null, require( './components/SomePath' ) )
        } )
    },
    onEnter() {
        epic$.next(epic3)

    }
}

对 Rxjs 和 redux-observable 非常陌生。这似乎可行,但想知道: 1. 在这种情况下,每次我们导航到 /some-path 时,epic3 是否会再次添加到 rootEpic? 2. 如果我想将 console.log 其中 epics 添加到 rootEpic 中,我该怎么做?


编辑以回应@JayPhelps

我可以要求澄清几点吗?

  1. registerEpic fn 效果很好。我一直在使用 .distinct() 运算符来解决重复的史诗注册问题,如下所示:

    export const epic$ = new BehaviorSubject(combineEpics(epic1, epic2)).distinct()

这是处理惰性史诗注册的同样 valid/good 方式吗?如果不是,您能解释一下原因吗?

  1. 我正在使用 create-react-app,它具有 require.ensure 以及 ES6 导入(它们是不同的导入方式,对吗?),基本上,我 copy-pasted react-router's huge-apps example其中有 require.ensure 代码,但在其他任何地方,我都在文件顶部使用导入语句。

所以在顶部导入 epics 和 registerEpic fn 似乎可行,而将路径放在 require.ensure 第一个参数中似乎也可行。如果我使用 require.ensure AND import 语句,它会把事情搞得一团糟吗?如果我使用 require.ensure 来加载路由所需的一切,这是否意味着我删除了该路由组件的嵌套 (route-less) 组件内的所有导入语句?

import { epic3 } from './../../epics/epic3'
import { epic4 } from './../../epics/epic4'
import { registerEpic } from './../../epics/index'

module.exports = {
    path: 'my-path',
    getComponent( location, done ) {
        require.ensure( [], ( require ) => {
           registerEpic(addYoutubeEpic)
            registerEpic(linkYourSiteEpic)
            done( null, require( './components/Edit' ) )
        } )
    }
}
  1. 关于在 'getComponent' fn 中注册 epics 以进行异步加载 - 我认为在这里进行同步加载实际上是可取的,这样路由的组件就不会呈现直到史诗被注册。如果用户试图在路线上做一些事情,比如获取一些详细信息或提交表格,这需要注册史诗,而史诗尚未注册,会发生什么情况?

  2. 除了 react-router module.export 声明之外还有其他地方适合延迟注册 epics 吗?由于我项目中的许多路由不需要登录,因此登录并不总是会触发路由更改。但是,有一堆 epics 应该在用户登录后注册。目前,我通过在我的 authReducer 中设置一个令牌变量,以一种非常愚蠢且可能是错误的方式来做这件事,但它所做的一切是调用注册新 epics:

    的函数
    './../reducers/authReducer.js'
    
    import { greatEpic } from './../epics/fetchFollowListEpic'
    import { usefulEpic } from './../epics/fetchMyCollectionsEpic'
    // and a few more 
    import { registerEpic } from './../epics/index'
    
    const addEpics = () => {
        registerEpic(greatEpic)
         registerEpic(usefulEpic)
         ...etc
    }
    
    export default function reducer( state = {
        loggingIn: false,   
        loggedIn: false,
        error: '',
    }, action ) {
        switch ( action.type ) {
            case "LOGIN_SEQUENCE_DONE": {
                return {
                    ...state,
                    aid: setAuthToken(action.payload),
                    loginFailed: false,
                    addEpics: addEpics(),
                }
    
            }
    

我认为 loginEpic 是注册 epics 的更好地方,而不是 reducer。 (我发现 your login example on github 所以它看起来可能很眼熟)。那是正确的,还是我完全没有根据?我知道 .concat() 是同步的,我知道 redux 是同步的——我不确定 registerEpics() 是否同步,但是使用 reducer 注册 epics SEEMS 可以正常工作——这是否意味着registerEpics 是同步的吗?或者它只是意味着事情发生得太快以至于无关紧要?或者它是否只是由于我对导入语句以及 webpack 如何处理我必须研究更多的代码拆分的一些基本误解而起作用?

./../epics/loginEpic

export const loginEpic = action$ =>
    action$.ofType("LOGIN")
        .mergeMap(action =>

            Observable.fromPromise(axios.post('webapi/login', action.payload))
                .flatMap(payload =>
                    // Concat multiple observables so they fire sequentially
                    Observable.concat(
                        // after LOGIN_FULILLED
                        Observable.of({ type: "LOGIN_FULFILLED", payload: payload.data.aid }),                   
                        // ****This is where I think I should be registering the epics right after logging in. "ANOTHER_ACTION" below depends upon one of these epics****     
                        Observable.of({type: "ANOTHER_ACTION", payload: 'webapi/following'}),
                        Observable.of({type: "LOGIN_SEQUENCE_DONE"}),
                    )
                )
                .startWith({ type: "LOGIN_PENDING" })
                .takeUntil(action$.ofType("LOGIN_CANCELLED"))
                .catch(error => Observable.of({
                    type: "LOGIN_REJECTED",
                    payload: error,
                    error: true
                }))
        );

In this case, would epic3 get added again to the rootEpic every time we navigate to /some-path?

如果您正在使用 react-router 并进行代码拆分,您实际上需要在 getComponent 挂钩中添加必要的 epics,不是 onEnter 挂钩。这是因为 getComponent 钩子允许异步加载该路由所需的资源,不仅是组件,还有任何 epics、reducer 等。如果你使用 require.ensure(),这会告诉 webpack将这些项目拆分成一个单独的包,这样它们就不会包含在原始条目中——所以不要将这些文件导入其他任何地方,否则你会否定它!

大致来说,这是一个可能看起来像的例子:

routes/SomePath/index.js

import { registerEpic } from 'where/ever/this/is/epics/index.js';

export default {
  path: 'some-path',

  getComponent(location, done) {
    require.ensure(['./components/SomePath', 'path/to/epics/epic3'], require => {
      // this is where you register epics, reducers, etc
      // that are part of this separate bundle
      const epic = require('path/to/epics/epic3').epic3;
      registerEpic(epic);

      done(null, require('./components/SomePath'));
    });
  }
};

where/ever/this/is/epics/index.js

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineEpics } from 'redux-observable';

import { epic1 } from './epic1';
import { epic2 } from './epic2';

const epicRegistry = [epic1, epic2];
const epic$ = new BehaviorSubject(combineEpics(...epicRegistry));

export const registerEpic = (epic) => {
  // don't add an epic that is already registered/running
  if (epicRegistry.indexOf(epic) === -1) {
    epicRegistry.push(epic);
    epic$.next(epic);
  }
};

// your root epic needs to use `mergeMap` on the epic$ so any
// new epics are composed with the others
export const rootEpic = (action$, store) =>
  epic$.mergeMap(epic =>
    epic(action$, store)
  );

我创建了一个注册功能,确保您不会添加已经添加的史诗; react-router 将调用 getComponent 每次 你进入那条路线,所以这很重要,这样你就不会有两个 运行 同一个史诗的实例.

但是请注意,当涉及到 require('path/to/epics/epic3').epic3 之类的事情时,我的代码做了一些假设,这些假设可能是开箱即用的。您的 epic3 实际上是作为命名的 属性 导出的吗?我想是的,但我注意到您正在这样做 done(null, require('./components/SomePath')) 如果您使用 export default 和 ES2015/ES6 模块导出该组件,我的直觉告诉我可能无法正常工作。如果这是真的,它实际上是在 require('./components/SomePath').default 找到的——注意 .default!所以虽然我的代码是一般的想法,但你应该特别注意需要的路径、导出的属性等。你说它似乎有效,所以也许你的设置不同(或者我错了嘿嘿)


  1. If I wanted to console.log which epics have been added into the rootEpic, how would I do that?

最简单的方法就是登录 registerEpic 实用程序:

export const registerEpic = (epic) => {
  // don't add an epic that is already registered/running
  if (epicRegistry.indexOf(epic) === -1) {
    epicRegistry.push(epic);
    console.log(`new epic added: ${epic.name}`);
    epic$.next(epic);
  }
};

但您也可以在 epic$ 主题上使用 RxJS .do() 运算符更具体地做到这一点:

export const rootEpic = (action$, store) =>
  epic$
    .do(epic => console.log(`new epic added: ${epic.name || '<anonymous>'}`))
    .mergeMap(epic =>
      epic(action$, store)
    );

然而,这将记录 <anonymous> 第一次,因为第一个通过的是 combineEpics(...epicRegistry) 的结果,它将多个 epics 组合成一个匿名史诗。