sigi icon indicating copy to clipboard operation
sigi copied to clipboard

如何创建一个模块的多个实例?

Open zybzzc opened this issue 4 years ago • 11 comments

以及能和vue devtools的vuex模块打通吗?

zybzzc avatar Sep 16 '20 02:09 zybzzc

如何创建一个模块的多个实例

Sigi 可以这样做但是不鼓励这样做。一个模块的多个实例在使用上会造成很多概念不一致的问题。 如果你需要一个模块复用在两个不同的业务逻辑,可以在 state 层面做好区分。并通过 selector API 将 state map 到不同的组件中。你可以把 Sigi 的 Module 看作 redux 的 store。

以及能和vue devtools的vuex模块打通吗

后续会做这件事情

Brooooooklyn avatar Sep 17 '20 07:09 Brooooooklyn

https://sigi.how/zh/recipes/state-selector

Brooooooklyn avatar Sep 17 '20 10:09 Brooooooklyn

Sigi 可以这样做但是不鼓励这样做。一个模块的多个实例在使用上会造成很多概念不一致的问题。

假设我要在页面上做文件拷贝, 有一个 FolderExplorer 组件和对应的 folder-explorer.module 模块, 此时页面上的两个 FolderExplorer 需要的是 FolderExplorerModule 的两个实例, 这种情况使用 Sigi 该如何处理呢?

另外, Sigi 和自行封装 Vuex 的动态模块, 除了框架无关外还有哪些优势呢(除了RxJS本身带来的优势之外)?

zybzzc avatar Sep 18 '20 03:09 zybzzc

假设我要在页面上做文件拷贝, 有一个 FolderExplorer 组件和对应的 folder-explorer.module 模块, 此时页面上的两个 FolderExplorer 需要的是 FolderExplorerModule 的两个实例, 这种情况使用 Sigi 该如何处理呢?

因为 Sigi 的 Module 上 EffectReducer 都是纯函数,所以这部分是可以共用的。你可以在 State 上对着两块业务进行划分:

state = {
  page1: FolderExploerDefaultState,
  page2: FolderExploerDefaultState,
}

然后在组件调用 dispatcher 的时候传入各自的 namespace

dispatcher.update({ page: 'page1', value: ..... })

然后在 Effect/Reducer 里面分别处理

@Effect()
update(payload$: Observable<{ page: 'page1' | 'page2', value ... }>) {
  return payload$.pipe(
    exhaustMap(({ page, value }) => {
      return this.fetch(...value).pipe(
        map((res) => this.getActions().updatePage({ page, res }))
      )
    })
  )
}

另外, Sigi 和自行封装 Vuex 的动态模块, 除了框架无关外还有哪些优势呢

除了 RxJS 以外,Sigi 在测试上有很大的优势,DI 可以让你在编写单元测试的时候 Mock 掉所有模块无关的逻辑,减少运行时间和测试复杂度。

Brooooooklyn avatar Sep 18 '20 03:09 Brooooooklyn

state = {
  page1: FolderExploerDefaultState,
  page2: FolderExploerDefaultState,
}

不太明白这样做的意思, 感觉更复杂了.

其实本来 FolderExplorer 自己维护内部状态就行了, 但是我想复用其中的状态和逻辑, 所以我想到的是使用 Sigi 实现 folder-explorer.module 模块, 但是它是单例的.

或许这种场景应该用纯粹的 react hooks 或其它组件框架相关的技术, Sigi 只该用在需要状态共享的地方?

zybzzc avatar Sep 18 '20 04:09 zybzzc

非单例模块在注入到其它模块的时候会有歧义。

不太明白这样做的意思, 感觉更复杂了.

其实就是 follow redux 的设计,redux 单一 store 也是通过在 store 里面加 namespace 来区分相同"形状"的不同模块

Brooooooklyn avatar Sep 18 '20 05:09 Brooooooklyn

// App
<div>
     <SigiMultiFolderExplorer />  
     <SigiFolderExplorer />
</div>

// SigiFolderExplorer
import { useModule, useModuleState } from '@sigi/react'
import { FolderExplorerModule } from './folder-explorer.module'

export default ({ actived }) => {
 const [state, dispatcher] = useModule(FolderExplorerModule )
 return (
   <DummyFolderExplorer {...state} actived onChange={dispatcher.update} />
 )
}

// SigiMultiFolderExplorer
// SigiMultiFolderExplorer 中 item 自身的状态该由谁管理 ?
// 如果是 SigiMultiFolderExplorer 自身管理, 如何复用 SigiFolderExplorer  组件 ?
// 如果是 SigiFolderExplorer 管理, 如何让所有 SigiFolderExplorer 中的状态独立 ?
import { useModule, useModuleState } from '@sigi/react'
import SigiFolderExplorer from './SigiFolderExplorer '
import { MultiFolderExplorerState } from './multi-folder-explorer.module'

export default () => {
 const [state, dispatcher] = useModule(MultiFolderExplorerState)
 return (
   <div>
      copy files in {state.explorer1.currentPath} to {state.explorer2.currentPath}
     // <SigiFolderExplorer actived={state.actived === 'explorer1'} />
     // <SigiFolderExplorer actived={state.actived === 'explorer2'} />
     <DummyFolderExplorer {...state.explorer1} onChange={dispatcher.updateExplorer1} />
     <DummyFolderExplorer {...state.explorer2} onChange={dispatcher.updateExplorer2} />
   </div>
 )
}


//  folder-explorer.module
@Module("FolderExplorer")
export class FolderExplorerModule extends EffectModule<FolderExplorerState> {
 defaultState: FolderExplorerState= {
   currentPath: '',
   // ...
 };

 constructor(
   private readonly appService: AppService,
   private readonly appModule: AppModule,
   private readonly authModule: AuthModule,
   // ...
) {
   super();
 }
}

// multi-folder-explorer.module
@Module("MultiFolderExplorer")
export class MultiFolderExplorerModule extends EffectModule<MultiFolderExplorerState> {
 defaultState: MultiFolderExplorerState= {
   // 应该在这里管理 item 的状态 ?
   // 如果在这里管理, 则需要在这里重写一遍 folder-explorer.module 中 的 reducer/effect
   explorer1: FolderExploerDefaultState,
   explorer2: FolderExploerDefaultState,
   actived: 'explorer1',
   // ...
 };

 constructor(
   private readonly appService: AppService,
   private readonly appModule: AppModule,
   private readonly authModule: AuthModule,
   // ...
) {
   super();
 }
}

不知道这样能否表达清楚.

你上面提到的:

state = {
  page1: FolderExploerDefaultState,
  page2: FolderExploerDefaultState,
}

这种做法, 我没理解错的话就是要在 multi-folder-explorer.module 中重写一遍 folder-explorer.module 中 的 reducer/effect.

zybzzc avatar Sep 18 '20 07:09 zybzzc

大概这样封装,FolderExplorerModule 存所有 explorer 的状态,Effect/Reducer 通过组件传进来的 Explorer 来决定更新哪一个状态,所以 Effect/Reducer 是完全复用的。Redux 在这种场景下也是一样的用法,复用 reducer/effect 就得在参数上区分更新哪一个部分的状态,渲染的时候通过 selector 选择渲染需要的那一小部分数据。

// App
<div>
     <SigiMultiFolderExplorer />  
     <SigiFolderExplorer />
</div>

// SigiFolderExplorer
import { useModule, useModuleState } from '@sigi/react'
import { FolderExplorerModule } from './folder-explorer.module'

export default ({ actived }) => {
 const [state, dispatcher] = useModule(FolderExplorerModule, {
    selector: (state) => state[actived],
    dependencies: [actived]
 })
 const update = useCallback((actived) => {
   (updated: FolderUpdated) => dispatcher.update({ explorer: actived, updated })
 }, [actived, dispatcher])
 return (
   <DummyFolderExplorer {...state} actived onChange={update} />
 )
}

import { useModule, useModuleState } from '@sigi/react'
import SigiFolderExplorer from './SigiFolderExplorer '
import { MultiFolderExplorerState } from './multi-folder-explorer.module'
import { FolderExplorerModule } from './folder-explorer.module'

export default () => {
 const [state, dispatcher] = useModule(MultiFolderExplorerState)
 const [folderState, folderDispatcher] = useModule(FolderExplorerModule)
  
 const updateExplorer = useCallback((explorer: string) => {
    return (changed: FolderChanged) => folderDispatcher.updateExplorer({ explorer, changed })
 }, [folderDispatcher])

 return (
   <div>
      copy files in {state.explorer1.currentPath} to {state.explorer2.currentPath}
     <DummyFolderExplorer {...folderState.explorer1} onChange={updateExplorer(explorer1)} />
     <DummyFolderExplorer {...folderState.explorer2} onChange={updateExplorer(explorer2)} />
   </div>
 )
}


//  folder-explorer.module
@Module("FolderExplorer")
export class FolderExplorerModule extends EffectModule<FolderExplorerState> {
 defaultState: { [index: string]: FolderExplorerState }= {};

 constructor(
   private readonly appService: AppService,
   private readonly appModule: AppModule,
   private readonly authModule: AuthModule,
   // ...
) {
   super();
 }
}

// multi-folder-explorer.module
@Module("MultiFolderExplorer")
export class MultiFolderExplorerModule extends EffectModule<MultiFolderExplorerState> {
 defaultState: MultiFolderExplorerState= {
   actived: 'explorer1',
   // ...
 };

 constructor(
   private readonly appService: AppService,
   private readonly appModule: AppModule,
   private readonly authModule: AuthModule,
   // ...
) {
   super();
 }
}

Brooooooklyn avatar Sep 18 '20 07:09 Brooooooklyn

我知道这样可以解决, 但是这样的话 FolderExplorerModule 的 defaultState: { [index: string]: FolderExplorerState } 实际上已经包含多个 FolderExplorer 的状态了, 因为要考虑在 SigiFolderExplorer 和 SigiMultiFolderExplorer 中复用, 但 defaultState: FolderExplorerState 才更符合直觉. 或者直接全部状态丢到一 MultiFolderExplorerState 中管理.

zybzzc avatar Sep 18 '20 08:09 zybzzc

defaultState: FolderExplorerState 才更符合直觉

是的, 这个是 Sigi 在设计的时候做的取舍。我们用 Class 形式只是因为 DI,但是 Class 会带来额外的组合问题,比如你这个场景其实是 state 树上的多个叶子与同一组 reducer/effect 组合的问题。以前传统的 redux 做法中,reducer/effect 全是纯函数,所以比较好组合。但是 Class 里面就不太容易做这个事情。

但本质上代码量并没有增多,因为无论是 redux 还是 vuex,这种形状 { [index]: State } 的状态树都是存在的,只要出现这种状态树想要复用相同的 reducer/effect 就需要额外的传参数。 只不过说 Class 在这里会让使用者出现一种幻觉,那就是 Class 要是能多实例就可以在这种场景下自动隔离这些差异。但本质上多实例会让 Sigi 无法构建 state 树 (多实例与 root 的关系是什么?用什么区分?),而且对 reducer/effect 也不是复用而是拷贝了一份,这会让程序更加难以理解。

Brooooooklyn avatar Sep 18 '20 08:09 Brooooooklyn

以及能和vue devtools的vuex模块打通吗?

请问大致方案会是什么呢, 我现在用的是注册动态 vuex 模块. 订阅 state$ 来维护 vuex 模块里的 state, 然后订阅 action$ 来触发一个假的 commit mutation 或 dispatch action. 我不确定这样手动维护的 vuex 状态在 vue devtools 中与 sigi 模块的实际状态会不会有出入(暂时看来是没问题的), 而且还侵入到了 @sigi/core, 因为要标记 vuex 的 mutations 和 actions

zybzzc avatar Sep 28 '20 10:09 zybzzc