@ exodus/ redux - 依赖性 - 注入
依赖性注射框架在redux上方.
为什么
大型redux应用有上百个动作,还原器,特别是选择器. 动作甚至不假装支持依赖性注射,减员者不应该有任何依赖性,尽管选择者的设计中考虑到依赖性注射(PcreateSelector(...dependencies, resultFunction)),它们的依赖性通常是: require“d”/ import'ed,这失去了依赖性注射的许多好处.
在雷杜克地外 我们有 @exodus/dependency-injection一个国际奥委会的容器 可以让我们以宣示的方式定义依赖性 然后解析依赖图并即时分出所有节点. 这使得在测试过程中容易地注入假相,不同的平台适配器在不同平台上,并且根据需要不同地执行相同的接口. 但你不是来宣传的...
要适应redux,我们需要首先支持以能够依赖性注射的方式声明redux组件(动作,还原器,选择器). 输入"Redux模块定义".
Redux 模块定义
以下是由 JK Rowling 所写的 redux 模块定义的例子 :
{
id: 'harryPotter',
type: 'redux-module',
// the `initialState` is used to auto-generate a base set of selectors
initialState: {
wand: null,
knownSpells: [],
},
// how events get to the redux module is covered further down
eventReducers: {
harryGoesToOlivanders: (state, payload) => ({ ...state, wand: payload }),
harryLearnsSpell: (state, payload) => ({ ...state, knownSpells: [...state.knownSpells, payload] }),
},
selectorDefinitions: [
// vanilla selector
{
id: 'knowsBasicSpells',
resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
// the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
// based on `initialState`
dependencies: [
{ selector: 'knownSpells' },
// NOTE: this selector is external to the module
{ module: 'hogwarts' selector: 'basicSpells' },
],
},
// selector factory
{
id: 'knowsSpell',
selectorFactory: (knownSpellsSelector) => (spell) => createSelector(
knownSpellsSelector,
knownSpells =>knownSpells.includes(spell)
),
// the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
// based on `initialState`
dependencies: [{ selector: 'knownSpells' }],
},
],
// ACTIONS ARE (SORT OF) SUPPORTED, BUT NOT RECOMMENDED!
// all your business logic belongs in `features` and `modules`, NOT in redux actions.
// support for these may be removed in the future
actionsDefinition: ({ hogwarts, harryPotter }) => {
return {
helpRonWithHomework: (spell) => (dispatch, getState) => {
if (harry.selectors.knowsSpell(getState())(spell)) {
dispatch(hogwarts.actions.submitHomework({ author: 'Ron' }))
}
}
}
},
eventToActionMappers: {
harryForgetsSpell: (spell) => ({ type: 'HARRY_POTTER_FORGETS_SPELL', payload: spell }),
},
dependencies: [
'hogwarts',
]
}
类型脚本帮助 API
对于 TypeScript 代码, 偏好帮助者 API 而非原始 satisfies 定义:
import {
defineReduxModule,
defineSelectorFactory,
defineSelectorResultFunction,
} from '@exodus/redux-dependency-injection'
import { createSelector } from 'reselect'
const selectorDefinitions = [
defineSelectorResultFunction({
id: 'loaded',
resultFunction: (data: { loaded: boolean }) => data.loaded,
dependencies: [{ selector: 'data' }],
}),
defineSelectorFactory({
id: 'createById',
selectorFactory: (dataSelector: (state: unknown) => { id: string }[]) => (id: string) =>
createSelector(dataSelector, (items) => items.find((item) => item.id === id)),
dependencies: [{ selector: 'data' }],
}),
] as const
export default defineReduxModule({
id: 'example',
initialState: { data: [] as { id: string }[] },
eventReducers: {},
selectorDefinitions,
} as const)
潜入更深一点.
选择器
在此之前
// harry-potter/selectors/knows-basic-spells.js
import { createSelector } from 'reselect'
import basicSpellsSelector from '~/ui/state/hogwarts/selectors/basic-spells'
import harryPotterKnownSpells from './known-spells'
const knowsBasicSpellsSelector = createSelector(
basicSpellsSelector, // e.g. state => state.hogwarts.basicSpells
harryPotterKnownSpells, // e.g. state => state.harryPottern.knownSpells
(basicSpells, knownSpells) => basicSpells.every((spell) => knownSpells.includes(spell))
)
export default knowsBasicSpellsSelector
之后
- 我们不出口选择器本身,而是 定义 因此,它可以被国际奥委会的容器瞬间化为懒散。 这也能让我们注射假药
wand财务报告和审定财务报表knownSpells测试中的依赖性. - 我们可以依赖外部选择器,例如
basicSpells从hogwarts模块,不导入它们. - 我们不再进入全球
state。所有数据通过其他选择器访问。 这个knownSpells在依赖性中声明的选择器由此库从initialState。 。 。 .
{
...,
selectorDefinitions: [
// vanilla selector
{
id: 'knowsBasicSpells',
resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
// the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
// based on `initialState`
dependencies: [
{ selector: 'knownSpells' },
// NOTE: this selector is external to the module
{ module: 'hogwarts' selector: 'basicSpells' },
],
},
// selector factory
{
id: 'knowsSpell',
selectorFactory: (knownSpells) => (spell) => knownSpells.includes(spell),
// the 'knownSpells' dependency selector is auto-generated by @exodus/redux-dependency-injection
// based on `initialState`
dependencies: [{ selector: 'knownSpells' }],
}
]
}
访问状态
如前所述,每件财产 initialState 单独的选择器是自动生成的.
您可以使用这些选择器, 在定义中将它们作为依赖性 .
在下面的例子中,将产生两个州选手, wand 财务报告和审定财务报表 knownSpells, (中文(简体) ),
各返回其状态中的当前值.
const initialState = {
wand: null,
knownSpells: [],
}
export default initialState
如果要访问redux模块的整个状态片段,您可以使用
特殊选择器 MY_STATE中定义的字符串常数 redux-dependency-injection。 。 。 .
import { MY_STATE } from '@exodus/redux-dependency-injection'
const someSelectorDefinition = {
// ...
dependencies: [
// ...
{ selector: MY_STATE },
],
}
选择定义属性
编号
所需经费
在其他选择者中将其用作依赖时, 确定这个选择者 。 我们也使用 命名公约 对于属于redux模块的选定者.
结果函数
不使用时需要
selectorFactory
这个 resultFunc 定义 重新选择。 。 。 .
此函数接受 产出 中定义的选定者 dependencies。 。 。 .
const definition = {
id: 'knowsBasicSpells',
resultFunction: (knownSpells, basicSpells) => basicSpells.every((spell) => knownSpells.includes(spell)),
dependencies: [
{ selector: 'knownSpells' },
{ module: 'hogwarts' selector: 'basicSpells' },
],
}
属于上述定义的客户端使用示例 harryPotter redux 模块 :
import { selectors } from '#/flux'
selectors.harryPotter.knowsBasicSpells(state)('finite')
选择器事实
不使用时需要
resultFunction
此函数接受选择器, 列在 dependencies,而不是输出,返回一个选择器工厂函数.
它是一种比香草选择器更能表现的替代品,它能返回一个函数. 看我们的 选择者最佳做法 更多细节.
import lodash from 'lodash'
import { createSelector } from 'reselect'
const { memoize } = lodash
const definition = {
id: 'createKnowsCharm',
selectorFactory: (knownCharmsSelector) =>
// meomize on charm since it is frequently used
memoize((charm) =>
createSelector(knownCharmsSelector, (knownCharms) => knownCharms.includes(charm))
),
dependencies: [{ selector: 'knownCharms' }],
}
属于上述定义的客户端使用示例 harryPotter redux 模块 :
import { selectors } from '#/flux'
selectors.harryPotter.createKnowsCharm('expelliarmus')(state)
依赖关系
选项
允许您通过提供标识对象列表来注入和使用其他选择对象。 该条目将传递给 resultFunction 或者说 selectorFactory 基于他们的立场.
属于模块本身的选择器只需提供选择器的ID即可被引用.
{
//...
dependencies: [
// first arg of resultFunction/selectorFactory
{ selector: 'charmSpells' },
// second arg of resultFunction/selectorFactory
{ selector: 'curseSpells' },
],
}
当引用来自不同模块的选取者时,除了选取者id外,还需要模块id.
{
//...
dependencies: [
// another redux module `hogwarts` that has a `basicSpells` selector
{ module: 'hogwarts', selector: 'basicSpells' }
],
}
行动
在此之前
import hogwartsActions from '../hogwarts'
import harryKnowsSpell from '#/harry-potter/selectors/knows-spell'
export const helpRonWithHomework = (spell) => (dispatch, getState) => {
if (harryKnowsSpell(getState())(spell)) {
dispatch(hogwartsActions.submitHomework({ author: 'Ron' }))
}
}
之后
- 我们定义行动创造者,而不是即时攻击他们.
- 所有依赖都是注入而不是导入的,这使得动作可以测试并避免静态依赖其他模块的动作/选择器.
{
...,
actionsDefinition: ({ hogwarts, harryPotter }) => {
return {
helpRonWithHomework: (spell) => (dispatch, getState) => {
if (harry.selectors.knowsSpell(getState())(spell)) {
dispatch(hogwarts.actions.submitHomework({ author: 'Ron' }))
}
}
}
},
// in case you need to map events to actions for some reason, e.g.
// because some other consumer expects this action. These are mutually exclusive with `eventReducers`
eventToActionMappers: {
harryForgetsSpell: (spell) => ({ type: 'HARRY_POTTER_FORGETS_SPELL', payload: spell }),
},
dependencies: ['hogwarts'],
}
缩小器
在此之前
function harryPotterReducer(state, { type, payload }) {
switch (payload.type) {
case 'HARRY_POTTER_GOES_TO_OLIVANDERS':
return { ...state, wand: payload.wand }
case 'HARRY_POTTER_LEARNS_SPELL':
return { ...state, knownSpells: [...state.knownSpells, payload.spell] }
default:
return state
}
}
之后
缩小器被宣布为从事件名称到转换函数的映射. 这样可以跳过事件名称映射到reduux动作类型的人工步骤. 这是(有点)有意见的,因为Exodus钱包使用事件来泡出从SDK到UI的日期.
{
...,
eventReducers: {
harryGoesToOlivanders: (state, payload) => ({ ...state, wand: payload }),
harryLearnsSpell: (state, payload) => ({ ...state, knownSpells: [...state.knownSpells, payload] }),
}
}
在某些情况下,可能有必要通过经常性行动而不是源自SDK的事件来吸收数据。 例如,这可能是只与UI相关的数据。 为了支持这一点,一个redux模块也可以定义 actionReducers数字 :
{
...,
actionReducers: {
USE_DIFFERENT_WAND: (state, payload) => ({ ...state, wand: payload }),
POTTER_CASTS_SPELL: (state, payload) => ({ ...state, spell: 'lumos' ?? payload }),
}
}
使用量
参见完整示例 @exodus/wallet-账户 下划线 /redux 文件夹。 这里有一个小例子:
// flux/index.js
import { setupRedux } from '@exodus/redux-dependency-injection'
import basicSpellsSelector from '~/ui/state/hogwarts/selectors/basic-spells'
const dependencies = [
// external selector, e.g. from a client repo
{
id: 'hogwarts.selectors.basicSpells',
factory: () => basicSpellsSelector,
},
harryPotterReduxModule,
]
const {
reducers: mergedReducers,
initialState: mergedInitialState,
selectors,
handleEvent,
ioc,
} = setupRedux({
// legacy reducers
reducers,
initialState,
dependencies,
logger: console,
})
const store = createStore(mergedReducers, mergedInitialState, enhancers)
const mergedActions = setupActions({ ioc, actions: legacyActionCreators })
const actions = bindAllActionCreators(store, mergedActions)
export { store, actions, selectors }
// to handle an event:
handleEvent('harryLearnsSpell', 'lumos')
// somewhere else in the app
import { selectors } from '#/flux'
selectors.harryPotter.knowsSpell('lumos')
解决问题
选择者是 undefined
这可能是由要求周期引起的。 如果测试不报告这一点,则使用客户端的操纵台,例如移动式的操纵台来方便地找到造成循环的原因.
删除要求的周期应再次提供选择器.
未排放或处理事件
确保事件发射器存在,例如模块生命周期插件中的观察者.
还确保redux模块定义中发射器和事件减少器之间的事件名称相同.