
Gostaria de compartilhar minha experiência no uso do redux em um aplicativo corporativo. Falando sobre software corporativo como parte do artigo, concentro-me nos seguintes recursos:
- Em primeiro lugar, este é o volume de funcionalidade. Estes são sistemas desenvolvidos por muitos anos, continuando a construir novos módulos ou complicando o que já existe por tempo indeterminado.
- Em segundo lugar, frequentemente, se considerarmos não uma tela de apresentação, mas o local de trabalho de alguém, um grande número de componentes conectados pode ser montado em uma página.
- Terceiro, a complexidade da lógica de negócios. Se queremos obter um aplicativo responsivo e agradável de usar, uma parte significativa da lógica terá que ser feita pelo cliente.
Os dois primeiros pontos impõem restrições à margem de produtividade. Mais sobre isso mais tarde. E agora, proponho discutir os problemas encontrados ao usar o redux - fluxo de trabalho clássico, desenvolvendo algo mais complicado que a lista TODO.
Redux clássico
Por exemplo, considere o seguinte aplicativo:

O usuário dirige uma rima - obtém uma avaliação de seu talento. O controle com a introdução do versículo é controlado e o recálculo da avaliação ocorre para cada mudança. Há também um botão no qual o texto com o resultado é redefinido e uma mensagem é exibida para o usuário que ele pode iniciar desde o início. Código fonte neste
segmento .
Organização do código:

Existem dois módulos. Mais precisamente, um módulo é diretamente poemaScoring. E a raiz do aplicativo com funções comuns para todo o sistema é app. Lá temos informações sobre o usuário, exibindo mensagens para o usuário. Cada módulo possui seus próprios redutores, ações, controles, etc. À medida que o aplicativo cresce, novos módulos se multiplicam.
Uma cascata de redutores, usando imutável por redux, forma o seguinte estado totalmente imutável:

Como funciona:
1. Controle de envio de ação-criador:
import at from '../constants/actionTypes'; export function poemTextChange(text) { return function (dispatch, getstate) { dispatch({ type: at.POEM_TYPE, payload: text }); }; }
As constantes dos tipos de ação são movidas para um arquivo separado. Em primeiro lugar, estamos tão seguros de erros de digitação. Em segundo lugar, o intellisense estará disponível para nós.
2. Depois, chega ao redutor.
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; } }
O processamento lógico é movido para uma
função de caso separada. Caso contrário, o código do redutor rapidamente se tornará ilegível.
3. A lógica do processamento de cliques usando análise lexical e inteligência artificial:
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; } };
No caso do botão "Novo poema", temos o seguinte criador de ação:
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!' }); }; }
Primeiro, despache a mesma ação que redefine nosso texto e pontuação. Em seguida, envie a ação, que será capturada por outro redutor e exibirá uma mensagem para o usuário.
Tudo é lindo. Vamos criar problemas para nós mesmos:
Os problemas:
Nós postamos nosso aplicativo. Mas nossos usuários, ao serem solicitados a escrever poesia, naturalmente começaram a publicar seus trabalhos, o que é incompatível com os padrões corporativos da linguagem poética. Em outras palavras, precisamos moderar palavras obscenas.
O que faremos:
- no texto de entrada, é necessário substituir todas as palavras não cultivadas por * censurado *
- além disso, se o usuário tiver dirigido uma palavra suja, você precisará avisá-lo com uma mensagem de que está fazendo algo errado.
Bom Só precisamos analisar o texto, além de calcular a pontuação, para substituir os palavrões. Não é um problema. E também, para informar o usuário, você precisa de uma lista do que excluímos. O código fonte está
aqui .
Nós refazemos a função da lógica para que, além do novo estado, retorne as informações necessárias para a mensagem ao usuário (palavras substituídas):
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; } };
Vamos aplicá-lo agora. Mas como No redutor, não faz mais sentido chamá-lo, pois colocaremos o texto e a avaliação no estado, mas o que devemos fazer com a mensagem? Para enviar uma mensagem, em qualquer caso, teremos que despachar a ação correspondente. Então, estamos finalizando o criador da ação.
export function poemTextChange(text) { return function (dispatch, getState) { const globalState = getState(); const scoringStateOld = globalState.get('poemScoring');
Também é necessário modificar o redutor, porque ele não chama mais a função lógica:
switch (action.type) { case at.POEM_TYPE: return action.payload; default: return state;
O que aconteceu:
E agora, a questão é. Por que precisamos de um redutor que, na maioria das vezes, simplesmente retorne a carga útil em vez de um novo estado? Quando outras ações aparecerem que processam a lógica da ação, será necessário registrar um novo tipo de ação? Ou talvez crie um SET_STATE comum? Provavelmente não, porque então, o inspetor estará uma bagunça. Então, vamos produzir o mesmo tipo de caso?
A essência do problema é a seguinte. Se o processamento da lógica envolve trabalhar com um pedaço de estado pelo qual vários redutores são responsáveis, você deve escrever todos os tipos de perversões. Por exemplo, os resultados intermediários de funções de caso, que devem ser espalhadas por diferentes redutores usando várias ações.
Uma situação semelhante, se a função de caso precisar de mais informações do que o que está no seu redutor, você deverá fazer sua chamada para a ação, onde há acesso ao estado global, seguido pelo envio do novo estado como carga útil. Um redutor terá que ser dividido em qualquer caso, se houver muita lógica no módulo. E isso cria um grande inconveniente.
Vamos olhar para a situação de um lado. Em nossa ação, obtemos um pedaço de estado do global. Isso é necessário para
modificá- lo (
globalState.get ('poemScoring'); ). Acontece que já sabemos em ação com que parte do estado o trabalho está acontecendo. Temos um novo pedaço de estado. Nós sabemos onde colocá-lo. Mas, em vez de colocá-lo em um global, o executamos com algum tipo de texto constante em toda a cascata de redutores, para que ele passe por cada caixa de comutação e a substitua apenas uma vez. Eu da realização disso, rugas. Entendo que isso é feito para facilitar o desenvolvimento e reduzir a conectividade. Mas, no nosso caso, não tem mais um papel.
Agora, listarei todos os pontos que não gosto na implementação atual, se precisar ser dimensionado em profundidade e profundidade por um tempo ilimitado :
- Inconveniente significativo ao trabalhar com um estado fora do redutor.
- O problema da separação de código. Toda vez que despachamos uma ação, ela passa por cada redutor, passa por cada caso. É conveniente não incomodar quando você tem um aplicativo pequeno. Mas, se você tem um monstro construído por vários anos com dezenas de redutores e centenas de casos, então começo a pensar na viabilidade de tal abordagem. Talvez, mesmo com milhares de casos, isso não tenha um impacto significativo no desempenho. Mas, entendendo que, ao imprimir texto, cada impressora causará uma passagem por centenas de casos, não posso deixar como está. Qualquer um, o menor atraso, multiplicado pelo infinito, tende ao infinito. Em outras palavras, se você não pensar nessas coisas, mais cedo ou mais tarde, surgirão problemas.
Quais são as opções?
a. Aplicativos isolados com seus próprios provedores . Em cada módulo (subaplicativo), você terá que duplicar as partes gerais do estado (conta, mensagens, etc.).
b. Use redutores assíncronos conectáveis. Isso não é recomendado pelo próprio Dan.
c. Use filtros de ação em redutores. Ou seja, cada despacho deve ser acompanhado de informações sobre para qual módulo está sendo enviado. E nos redutores de raiz dos módulos, escreva as condições apropriadas. Eu tentei. Não havia esse número de erros involuntários antes ou depois. Existe uma confusão constante sobre onde a ação vai. - Cada vez que uma ação é despachada, não há apenas uma execução para cada redutor, mas também a coleta do estado reverso. Não importa se o estado mudou no redutor - ele será substituído nos combineReducers.
- Cada despacho força o mapStateToProps a ser processado para cada componente conectado que está montado na página. Se dividirmos redutores, temos que dividir as expedições. É fundamental que tenhamos um botão que substitua o texto e exiba a mensagem com diferentes despachos? Provavelmente não. Mas tenho experiência em otimização, ao reduzir o número de expedições de 15 para 3, permitindo aumentar significativamente a capacidade de resposta do sistema, com a mesma quantidade de lógica de negócios processada. Eu sei que existem bibliotecas que podem combinar vários despachos em um lote, mas isso é uma luta com a investigação usando muletas.
- Ao esmagar despachos, às vezes é muito difícil ver o que está acontecendo. Não há um lugar, tudo está espalhado em arquivos diferentes. É necessário procurar onde o processamento é implementado, procurando constantes em todos os códigos-fonte.
- No código acima, componentes e ações acessam o estado global diretamente:
const userName = globalState.getIn(['app', 'account', 'name']); … const text = state.getIn(['poemScoring', 'poemText']);
Isso não é bom por vários motivos:
a. Idealmente, os módulos devem ser isolados. Eles não precisam saber em que estado vivem.
b. Mencionar os mesmos caminhos em lugares diferentes é muitas vezes repleto não apenas de erros / erros de digitação, mas também torna a refatoração extremamente difícil no caso de alterar a configuração do estado global ou alterar a maneira como ele é armazenado.
- Cada vez mais, ao escrever uma nova ação, tive a impressão de estar escrevendo código por causa do código. Suponha que desejemos adicionar uma caixa de seleção à página e refletir seu estado booleano na história. Se queremos uma organização uniforme de ação / redutores, precisamos:
- Registrar constante do tipo de ação
- Escreva uma cratera de ação
- No controle, importe-o e registre-o em mapDispatchToProps
- Registre-se em PropTypes
- Crie um handleCheckBoxClick no controle e especifique-o na caixa de seleção
- Adicione um interruptor no redutor com uma chamada de função de caso
- Escreva uma função de caso na lógica
Por uma questão de boxe! - O estado que é gerado com combineReducers é estático. Não importa se você já entrou no módulo B ou não, esta peça estará na história. Vazio, mas será. Não é conveniente usar o inspetor quando houver muitos nós vazios não utilizados no steet.
Como tentamos resolver alguns dos problemas descritos acima
Então, obtivemos redutores estúpidos e, na ação-crateras / lógica, escrevemos trechos de código para trabalhar com estruturas imutáveis profundamente embutidas. Para me livrar disso, uso o mecanismo de seletores hierárquicos, que permitem não apenas o acesso ao pedaço de estado desejado, mas também o substitui (conveniente setIn). Publiquei isso no pacote
immutable-selectors .
Vejamos nosso exemplo de como funciona (
repositório ):
No módulo poemScoring, descrevemos o objeto seletores. Descrevemos os campos do estado ao qual queremos ter acesso direto de leitura / gravação. Qualquer aninhamento e parâmetros para acessar os elementos de coleções são permitidos. Não é necessário descrever todos os campos possíveis em nosso artigo.
import extendSelectors from 'immutable-selectors'; const selectors = { poemText:{}, score:{} }; extendSelectors(selectors, [ 'poemScoring' ]); export default selectors;
Além disso, o método extendSelectors transforma cada campo em nosso objeto em uma função seletora. O segundo parâmetro indica o caminho para a parte do estado que o seletor controla. Não criamos um novo objeto, mas alteramos o atual. Isso nos dá um bônus na forma de inteligência de trabalho:

Qual é o nosso objeto - um seletor após sua expansão:

A função
selectors.poemText (state) simplesmente executa
state.getIn (['poemScoring', 'poemText']) .
Função
root (state) - obtém 'poemScoring'.
Cada seletor possui sua própria função de
substituição (globalState, newPart) , que através de setIn retorna um novo estado global com a parte correspondente substituída.
Além disso, é adicionado um objeto
plano ao qual todas as chaves seletoras são duplicadas. Ou seja, se usarmos um estado profundo da forma
selectors = { dive:{ in:{ to:{ the:{ deep:{} } } } }}
Você pode se aprofundar como
seletores.de mergulho.para.de.deep
(estado) ou como
seletores.de.paço (estado) .
Vá em frente. Precisamos atualizar a aquisição de dados nos controles:
Poema:
function mapStateToProps(state, ownprops) { return { text:selectors.poemText(state) || '' }; }
Pontuação:
function mapStateToProps(state, ownprops) { const score = selectors.score(state); return { score }; }
Em seguida, altere o redutor de raiz:
import initialState from './initialState'; function setStateReducer(state = initialState, action) { if (action.setState) { return action.setState; } else { return state;
Se desejar, podemos combinar usando combineReducers.
Cratera de ação, por exemplo, poemTextChange:
export function poemTextChange(text) { return function (dispatch, getState) { dispatch({ type: 'Poem typing', setState: logic.onType(getState(), text), payload: text }); }; }
Não podemos mais usar constantes do tipo ação, porque agora o tipo é usado apenas para visualização no inspetor. No projeto, escrevemos descrições em texto completo da ação em russo. Você também pode se livrar da carga útil, mas tento salvá-la para que no inspetor, se necessário, entenda com quais parâmetros a ação foi chamada.
E, de fato, a própria lógica:
onType(gState, text) { const { reductedText, censoredWords } = this.redactText(text); const poemState = selectors.root(gState) || Immutable.Map();
Ao mesmo tempo,
message.showMessage é importado da lógica do módulo vizinho, que descreve seus seletores:
showMessage(gState, text) { return selectors.message.text.replace(gState, text); }.
O que acontece:

Observe que tivemos um despacho, os dados em dois módulos foram alterados.
Tudo isso nos permitiu eliminar redutores e constantes do tipo ação, além de solucionar ou contornar a maioria dos gargalos descritos acima.
De que outra forma isso pode ser aplicado?
Essa abordagem é conveniente de usar quando é necessário garantir que seus controles ou módulos forneçam trabalho com diferentes partes do estado. Digamos que um poema não seja suficiente para nós. Queremos que o usuário seja capaz de compor poemas em duas abas diferentes em diferentes disciplinas (infantil, romântica). Nesse caso, não podemos importar os seletores na lógica / controles, mas especificá-los como um parâmetro no controle externo:
<Poem selectors = {selectors.hildPoem}/> <Poem selectors = {selectors.romanticPoem}/>
E, além disso, passe esse parâmetro para crateras de ação. Isso é suficiente para tornar uma combinação complexa de componentes e lógica completamente fechada, facilitando a reutilização.
Limitações ao usar seletores imutáveis:Não funcionará para usar a chave no estado "nome", porque para a função pai, haverá uma tentativa de substituir a propriedade reservada.
Qual é o resultado
Como resultado, uma abordagem bastante flexível foi obtida, os relacionamentos implícitos de código por constantes de texto foram eliminados, a sobrecarga foi reduzida, mantendo a conveniência do desenvolvimento. Há também um inspetor de redux em pleno funcionamento, com a possibilidade de viajar no tempo. Não desejo voltar aos redutores padrão.
Em geral, isso é tudo. Obrigado pelo seu tempo. Talvez alguém esteja interessado em experimentar!