sigi
sigi copied to clipboard
如何创建一个模块的多个实例?
以及能和vue devtools的vuex模块打通吗?
如何创建一个模块的多个实例
Sigi 可以这样做但是不鼓励这样做。一个模块的多个实例在使用上会造成很多概念不一致的问题。
如果你需要一个模块复用在两个不同的业务逻辑,可以在 state
层面做好区分。并通过 selector
API 将 state
map 到不同的组件中。你可以把 Sigi 的 Module 看作 redux 的 store。
以及能和vue devtools的vuex模块打通吗
后续会做这件事情
https://sigi.how/zh/recipes/state-selector
Sigi 可以这样做但是不鼓励这样做。一个模块的多个实例在使用上会造成很多概念不一致的问题。
假设我要在页面上做文件拷贝, 有一个 FolderExplorer
组件和对应的 folder-explorer.module
模块, 此时页面上的两个 FolderExplorer
需要的是 FolderExplorerModule
的两个实例, 这种情况使用 Sigi 该如何处理呢?
另外, Sigi 和自行封装 Vuex 的动态模块, 除了框架无关外还有哪些优势呢(除了RxJS本身带来的优势之外)?
假设我要在页面上做文件拷贝, 有一个 FolderExplorer 组件和对应的 folder-explorer.module 模块, 此时页面上的两个 FolderExplorer 需要的是 FolderExplorerModule 的两个实例, 这种情况使用 Sigi 该如何处理呢?
因为 Sigi 的 Module 上 Effect
和 Reducer
都是纯函数,所以这部分是可以共用的。你可以在 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 掉所有模块无关的逻辑,减少运行时间和测试复杂度。
state = { page1: FolderExploerDefaultState, page2: FolderExploerDefaultState, }
不太明白这样做的意思, 感觉更复杂了.
其实本来 FolderExplorer
自己维护内部状态就行了, 但是我想复用其中的状态和逻辑, 所以我想到的是使用 Sigi 实现 folder-explorer.module
模块, 但是它是单例的.
或许这种场景应该用纯粹的 react hooks 或其它组件框架相关的技术, Sigi 只该用在需要状态共享的地方?
非单例模块在注入到其它模块的时候会有歧义。
不太明白这样做的意思, 感觉更复杂了.
其实就是 follow redux 的设计,redux 单一 store 也是通过在 store 里面加 namespace 来区分相同"形状"的不同模块
// 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.
大概这样封装,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();
}
}
我知道这样可以解决, 但是这样的话 FolderExplorerModule 的 defaultState: { [index: string]: FolderExplorerState }
实际上已经包含多个 FolderExplorer 的状态了, 因为要考虑在 SigiFolderExplorer 和 SigiMultiFolderExplorer 中复用, 但 defaultState: FolderExplorerState
才更符合直觉. 或者直接全部状态丢到一 MultiFolderExplorerState 中管理.
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 也不是复用而是拷贝了一份,这会让程序更加难以理解。
以及能和vue devtools的vuex模块打通吗?
请问大致方案会是什么呢, 我现在用的是注册动态 vuex 模块. 订阅 state$
来维护 vuex 模块里的 state, 然后订阅 action$
来触发一个假的 commit mutation 或 dispatch action. 我不确定这样手动维护的 vuex 状态在 vue devtools 中与 sigi 模块的实际状态会不会有出入(暂时看来是没问题的), 而且还侵入到了 @sigi/core, 因为要标记 vuex 的 mutations 和 actions