体验没有Reduce的Redux使用



我想分享一下我在企业应用程序中使用redux的经验。 在文章中谈到公司软件时,我将重点介绍以下功能:

  • 首先,这是功能的数量。 这些系统已经开发了很多年,将继续构建新模块,或者无限期地复杂化现有系统。
  • 其次,通常,如果我们考虑的不是演示屏幕,而是某人的工作场所,则可以在一页上安装大量附加组件。
  • 第三,业务逻辑的复杂性。 如果我们想要获得一个响应迅速且易于使用的应用程序,则逻辑的很大一部分将必须由客户端来完成。

前两点对生产率的限制施加了限制。 稍后再详细介绍。 现在,我建议讨论使用经典的redux(工作流程)遇到的问题,开发比TODO(列表)更复杂的东西。

经典还原


例如,请考虑以下应用程序:

图片

用户开押韵-对自己的才能进行评估。 引入经文的控制得到控制,并且每次更改都会对评估进行重新计算。 还有一个按钮,通过该按钮可以重置带有结果的文本,并向用户显示一条消息,提示他可以从头开始。 此线程中的源代码。

代码组织:

图片

有两个模块。 更准确地说,一个模块是poemScoring。 具有整个系统通用功能的应用程序的根源是app。 那里我们有关于用户的信息,向用户显示消息。 每个模块都有其自己的简化器,动作,控件等。随着应用程序的增长,新模块将成倍增加。

使用redux-immutable的级联化简器形成以下完全不可变的状态:

图片

运作方式:

1.控制调度动作创建者:

import at from '../constants/actionTypes'; export function poemTextChange(text) { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: text }); }; } 

动作类型的常量将移动到单独的文件中。 首先,我们很容易错别字。 其次,智能感知将向我们提供。

2.然后进入减速器。

 import logic from '../logic/poem'; export default function poemScoringReducer(state = Immutable.Map(), action) { switch (action.type) { case at.POEM_TYPE: return logic.onType(state, action.payload); default: return state; } } 

逻辑处理移至单独的案例功能 。 否则,reducer代码将很快变得不可读。

3.使用词法分析和人工智能处理点击的逻辑:

 export default { onType(state, text) { return state .set('poemText', text) .set('score', this.calcScore(text)); }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; } }; 

对于“新诗”按钮,我们有以下动作创建者:

 export function newPoem() { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: '' }); dispatch({ type: appAt.SHOW_MESSAGE, payload: 'You can begin a new poem now!' }); }; } 

首先,请执行相同的操作以重置我们的文字和得分。 然后,发送动作,该动作将被另一个reducer捕获,并向用户显示一条消息。
一切都美好。 让我们为自己创造问题:

问题:


我们已经发布了我们的申请。 但是,看到他们被要求写诗的用户,自然就开始发表自己的作品,这与诗语言的公司标准不符。 换句话说,我们需要适度淫秽的词语。

我们将做什么:

  • 在输入文本中,有必要将所有未经训练的单词替换为*删节*
  • 另外,如果用户打了脏话,则需要用一条消息警告他做错了。

好啊 除了计算分数之外,我们只需要分析文本即可替换坏词。 没问题 另外,为了通知用户,您需要列出我们删除的内容。 源代码在这里

我们重新构造了逻辑功能,以便除了新状态外,它还向用户返回消息所需的信息(被替换的单词):

 export default { onType(state, text) { const { reductedText, censoredWords } = this.redactText(text); const newState = state .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); return { newState, censoredWords }; }, calcScore(text) { const score = Math.floor(text.length / 10); return score > 5 ? 5 : score; }, redactText(text) { const result = { reductedText:text }; const censoredWords = []; obscenseWords.forEach((badWord) => { if (result.reductedText.indexOf(badWord) >= 0) { result.reductedText = result.reductedText.replace(badWord, '*censored*'); censoredWords.push(badWord); } }); if (censoredWords.length > 0) { result.censoredWords = censoredWords.join(' ,'); } return result; } }; 

现在应用它。 但是如何? 在化简器中,再也没有必要再调用它了,因为我们将文本和评估置于状态中,但是我们应该如何处理该消息? 为了发送消息,无论如何,我们将必须调度相应的操作。 因此,我们正在确定行动创造者。

 export function poemTextChange(text) { return function (dispatch, getState) { const globalState = getState(); const scoringStateOld = globalState.get('poemScoring'); //        const { newState, censoredWords } = logic.onType(scoringStateOld, text); dispatch({ //        type: at.POEM_TYPE, payload: newState }); if (censoredWords) { //    ,    const userName = globalState.getIn(['app', 'account', 'name']); const message = `${userName}, avoid of using word ${censoredWords}, please!`; dispatch({ type: appAt.SHOW_MESSAGE, payload: message }); } }; } 

还需要修改reducer,因为它不再调用逻辑函数:

  switch (action.type) { case at.POEM_TYPE: return action.payload; default: return state; 

发生了什么:

图片

现在,问题是。 为什么我们需要一个减速器,在大多数情况下,该减速器将仅返回有效载荷而不是新状态? 当出现其他处理该操作逻辑的操作时,是否有必要注册新的操作类型? 或者创建一个通用的SET_STATE? 可能不是,因为那样的话,检查员会一团糟。 那么我们会产生相同类型的案例吗?

问题的实质如下。 如果逻辑处理涉及处理一个状态,而这个状态由几个化简器负责,那么您就必须编写各种变态。 例如,案例函数的中间结果,然后必须使用几个动作将它们分散在不同的化简器上。

类似的情况是,如果case函数需要比化简器中更多的信息,则您必须对其进行调用,该操作可以访问全局状态,然后将新状态作为有效负载发送。 如果模块中有很多逻辑,则在任何情况下都必须拆分化径器。 这带来了极大的不便。

让我们从一侧来看情况。 在我们的行动中,我们从全球获得了一片状态。 这是使它发生变异的必要条件( globalState.get('poemScoring'); )。 事实证明,我们已经在行动中知道工作正在进行什么状态。 我们有一个新的状态。 我们知道放在哪里。 但是,与其在全局化的级联器中使用某种文本常量来运行它,不如将其置于全局的状态中,以便它通过每个切换用例并替换一次。 我从意识到这一点起皱纹。 我知道这样做是为了简化开发并减少连接。 但就我们而言,它不再起作用。

现在,我将列出当前实施中我不喜欢的所有要点,如果必须在无限长的时间内进行广度和深度扩展

  1. 在异径管外的状态下工作时会带来极大的不便。
  2. 代码分离的问题。 每次我们分发动作时,它都会通过每个化简器,并通过每种情况。 当您的应用程序很小时,不打扰是很方便的。 但是,如果您有一个使用数十种减速器和数百个案例构建的怪兽,那么我开始考虑这种方法的可行性。 也许,即使有成千上万的案例,这也不会对性能产生重大影响。 但是,要理解在打印文本时,每次印刷都会导致数百种情况的发生,所以我不能原样保留它。 任何最小的滞后乘以无穷大都趋于无穷大。 换句话说,如果您不考虑此类问题,迟早会出现问题。

    有哪些选择?

    一个 与自己的提供程序隔离的应用程序 。 在每个模块(子应用程序)中,您将必须复制状态的一般部分(帐户,消息等)。

    b。 使用可插拔异步减速器 。 Dan本人不建议这样做。

    c。 在减速器中使用动作过滤器 。 也就是说,每个调度都应随附有关将其发送到哪个模块的信息。 并在模块的根减速器中编写适当的条件。 我试过了 在此之前或之后,没有如此多的非自愿错误。 人们对行动的去向一直感到困惑。
  3. 每次调度动作时,不仅每个减速器都有运行,而且还包含反向状态。 减速器中的状态是否改变都没有关系-它将在CombineReducers中替换。
  4. 每次调度都会强制为页面上安装的每个附加组件处理mapStateToProps。 如果拆分减速器,则必须拆分调度。 我们有一个按钮可以覆盖文本并以不同的调度方式显示消息是否至关重要? 可能不是。 但是我有优化经验,在将处理数量从15减少到3时,可以在相同数量的已处理业务逻辑的情况下显着提高系统的响应速度。 我知道有一些库可以将多个调度合并为一个批处理,但这与使用拐杖进行调查很不容易。
  5. 压碎调度时,有时很难看到正在发生的事情。 没有一个地方,一切都分散在不同的文件中。 有必要通过搜索所有源代码中的常量来搜索在哪里执行处理。
  6. 在上面的代码中,组件和操作直接访问全局状态:

     const userName = globalState.getIn(['app', 'account', 'name']); … const text = state.getIn(['poemScoring', 'poemText']); 

    由于以下几个原因,这不是很好:

    一个 理想情况下,模块应隔离。 他们不需要知道他们住在哪个州。

    b。 在不同位置提及相同路径通常不仅会充满错误/错别字,而且在更改全局状态的配置或更改其存储方式的情况下,重构也变得极为困难。
  7. 在编写新动作时,我越来越有一种印象,就是为了代码而编写代码。 假设我们要在页面上添加一个复选框,并在故事中反映其布尔状态。 如果我们想要统一的行动/减速器组织,那么我们必须:

    -注册动作类型常量
    -写一个动作陨石坑
    -在控件中,将其导入并在mapDispatchToProps中注册
    -在PropTypes中注册
    -在控件中创建一个handleCheckBoxClick并在复选框中指定它
    -通过case函数调用在化简器中添加一个开关
    -在逻辑中编写案例函数

    为了一次拳击检查!
  8. 用combinedReducers生成的状态是静态的。 无论您是否已经进入模块B,这都将成为故事。 空的,但是会的。 当steet中有许多未使用的空节点时,使用检查器是不方便的。

我们如何解决上述一些问题


因此,我们有了愚蠢的化简器,在动作弹坑/逻辑中,我们编写了用于处理深层嵌入的不可变结构的代码脚注。 为了消除这一点,我使用了层次选择器的机制,该机制不仅允许访问所需的状态,还可以替换状态(方便的setIn)。 我将其发布在不可变选择器包中。

让我们看一下示例,它是如何工作的repository ):
在poemScoring模块中,我们描述了选择器对象。 我们从希望直接读取/写入访问的状态描述了这些字段。 允许使用任何嵌套和参数访问集合的元素。 不必在本文中描述所有可能的字段。

 import extendSelectors from 'immutable-selectors'; const selectors = { poemText:{}, score:{} }; extendSelectors(selectors, [ 'poemScoring' ]); export default selectors; 

此外,extendSelectors方法将对象中的每个字段转换为选择器函数。 第二个参数指示选择器控制的状态部分的路径。 我们不创建新对象,而是更改当前对象。 这以工作情报的形式给了我们额外的好处:

图片

我们的对象是什么-展开后的选择器:

图片

selectors.poemText(状态)函数仅执行state.getIn(['poemScoring','poemText'])

函数根(状态) -获取“ poemScoring”。

每个选择器都有其自己的替换功能(globalState,newPart) ,该功能通过setIn返回一个新的全局状态,其中相应的零件已被替换。

此外, 还将添加一个平面对象,所有唯一选择器键都将复制到该平面对象上。 也就是说,如果我们使用表单的深层状态

 selectors = { dive:{ in:{ to:{ the:{ deep:{} } } } }} 

您可以作为选择器深入学习(状态),也可以作为选择器展现深度(状态)

来吧 我们需要更新控件中的数据采集:

诗:
 function mapStateToProps(state, ownprops) { return { text:selectors.poemText(state) || '' }; } 


分数:
 function mapStateToProps(state, ownprops) { const score = selectors.score(state); return { score }; } 

接下来,更改根减速器:

 import initialState from './initialState'; function setStateReducer(state = initialState, action) { if (action.setState) { return action.setState; } else { return state; // return combinedReducers(state, action); // } } export default setStateReducer; 

如果需要,我们可以结合使用CombineReducers。

动作弹坑,例如poemTextChange:

 export function poemTextChange(text) { return function (dispatch, getState) { dispatch({ type: 'Poem typing', setState: logic.onType(getState(), text), payload: text }); }; } 

我们不能再使用动作类型常量,因为类型现在仅用于检查器中的可视化。 我们在该项目中用俄语编写了该动作的全文描述。 您也可以摆脱有效负载,但是我会尝试将其保存,以便在检查器中,如有必要,我可以了解使用什么参数调用了该动作。

而且,实际上,逻辑本身:

  onType(gState, text) { const { reductedText, censoredWords } = this.redactText(text); const poemState = selectors.root(gState) || Immutable.Map(); //     const newPoemState = poemState //  .set('poemText', reductedText) .set('score', this.calcScore(reductedText)); let newGState = selectors.root.replace(gState, newPoemState); //    if (censoredWords) { //  ,    const userName = appSelectors.flat.userName(gState); const messageText = `${userName}, avoid of using word ${censoredWords}, please!`; newGState = message.showMessage(newGState, messageText); } return newGState; }, 

同时, message.showMessage是从相邻模块的逻辑中导入的,该模块描述了其选择器:

  showMessage(gState, text) { return selectors.message.text.replace(gState, text); }. 

结果是:

图片

请注意,我们进行了一次调度,两个模块中的数据已更改。
所有这些使我们摆脱了reducers和action-type常量,并解决了或克服了上面概述的大多数瓶颈。

还有什么可以应用的?


当必须确保您的控件或模块提供具有不同状态的工作时,使用此方法很方便。 假设一首诗对我们来说还不够。 我们希望用户能够在不同学科(儿童,浪漫)的两个不同选项卡上创作诗歌。 在这种情况下,我们不能在逻辑/控件中导入选择器,而是在外部控件中将它们指定为参数:

  <Poem selectors = {selectors.hildPoem}/> <Poem selectors = {selectors.romanticPoem}/> 

并且,进一步将此参数传递给动作坑。 这足以使组件和逻辑的复杂组合完全封闭,从而易于重用。

使用不可变选择器的限制:

在“名称”状态下使用键将不起作用,因为对于父函数,将尝试覆盖保留的属性。

结果如何


结果,获得了一种相当灵活的方法,消除了文本常量所隐含的代码关系,减少了开销,同时保持了开发的便利性。 还有一个功能齐全的Redux检查器,可能会花费时间。 我不想回到标准减速机。

一般来说,仅此而已。 谢谢您的时间。 也许有人会对尝试一下感兴趣!

Source: https://habr.com/ru/post/zh-CN424613/


All Articles