Posso usar o Redux em um servidor?

O Redux é uma excelente ferramenta para gerenciar o estado de aplicativos front-end complexos. O autor do material, cuja tradução publicamos hoje, encontrará a resposta para a questão de saber se é possível tirar proveito dos recursos do Redux no ambiente do servidor.
imagem

Por que eu preciso de uma biblioteca Redux?


A página inicial da biblioteca Redux diz que é um "contêiner de estado previsível para aplicativos JavaScript". O Redux é geralmente referido como uma ferramenta para gerenciar o estado de um aplicativo e, embora essa biblioteca seja usada principalmente com o React, ela pode ser usada em qualquer projeto baseado em JavaScript.

Já mencionamos que o Redux é usado para controlar o estado de um aplicativo. Agora vamos falar sobre o que é uma "condição". Esse conceito é bastante difícil de definir, mas ainda tentamos descrevê-lo.

Considerando o "estado", se estamos falando de pessoas ou de objetos do mundo material, procuramos descrever, de fato, o estado deles no momento em que estamos falando deles, possivelmente considerando um ou mais parâmetros. Por exemplo, podemos dizer sobre o lago: "A água está muito quente" ou: "A água está congelada". Nestas declarações, descrevemos o estado do lago em termos de temperatura da água.

Quando alguém diz sobre si mesmo: "Estou encalhado", ele considera a quantidade de dinheiro que tem. É claro que em cada um desses exemplos estamos falando apenas de um aspecto do estado dos objetos. Mas, no exemplo sobre dinheiro, a declaração pode ser aquela que descreve vários parâmetros: "Estou encalhado, não como há muito tempo, mas estou feliz!". É muito importante notar aqui que o estado é algo impermanente. Isso significa que pode mudar. Portanto, quando aprendemos sobre o estado atual de um determinado objeto, entendemos que seu estado real pode mudar alguns segundos ou minutos depois de aprendermos sobre ele.

Quando lidamos com programas, alguns recursos estão associados ao conceito de "estado". Em primeiro lugar, o estado do aplicativo é representado por dados armazenados em algum lugar. Por exemplo, esses dados podem ser armazenados na memória (por exemplo, como um objeto JavaScript), mas podem ser armazenados em um arquivo, em um banco de dados e usando algum mecanismo de cache como o Redis. Em segundo lugar, o estado de um aplicativo geralmente está vinculado à sua instância específica. Portanto, quando falamos sobre o estado do aplicativo, queremos dizer uma instância específica desse aplicativo, um processo, um ambiente de trabalho organizado no aplicativo para um usuário específico. Um estado de aplicativo pode incluir, por exemplo, as seguintes informações:

  • O usuário está logado ou não? Em caso afirmativo, quanto tempo dura a sessão e quando expirará?
  • Quantos pontos o usuário marcou? Essa pergunta é relevante, por exemplo, para um determinado jogo.
  • Onde exatamente o usuário pausou o vídeo? Esta pergunta pode ser feita sobre o aplicativo player de vídeo.

Se falarmos sobre o estado dos aplicativos em um nível inferior, ele poderá incluir, por exemplo, as seguintes informações:

  • Quais variáveis ​​são definidas no ambiente atual em que o aplicativo está sendo executado (isso se refere às chamadas "variáveis ​​de ambiente").
  • Quais arquivos o programa está usando atualmente?

Observando o "instantâneo" (geralmente chamado de "instantâneos" - do instantâneo) do estado do aplicativo a qualquer momento, podemos aprender sobre as condições em que o aplicativo funcionou naquele momento e, se necessário, recriar essas condições: aplicativo para o estado em que estava no momento de receber o instantâneo.

O estado pode ser modificado durante a execução de determinadas ações pelo usuário. Por exemplo, se o usuário mover corretamente o personagem do jogo em um jogo simples, isso poderá aumentar o número de pontos. Em aplicações bastante complexas, a abordagem para modificar um estado pode se tornar mais complicada; as alterações de estado podem vir de diferentes fontes.

Por exemplo, em um jogo multiplayer, quantos pontos um usuário depende não apenas de suas ações, mas também das ações daqueles que jogam com ele no mesmo time. E se um personagem controlado por computador atacar com sucesso um personagem do jogo que o usuário controla, o usuário poderá perder uma certa quantidade de pontos.

Imagine que estamos desenvolvendo um aplicativo front-end como o PWA Twitter . Este é um aplicativo de uma página em que existem várias guias, por exemplo - Página inicial, Pesquisa, Notificações e Mensagens. Cada uma dessas guias possui seu próprio espaço de trabalho, destinado a exibir determinadas informações e a sua modificação. Todos esses dados formam o estado do aplicativo. Assim, novos tweets, notificações e mensagens chegam no aplicativo a cada poucos segundos. O usuário pode trabalhar com o programa e com esses dados. Por exemplo, ele pode criar um tweet ou excluí-lo, ele pode retuitar um tweet, ele pode ler notificações, enviar mensagens para alguém e assim por diante. Tudo o que foi discutido modifica o estado do aplicativo.


Todas essas guias têm seu próprio conjunto de componentes da interface do usuário usados ​​para exibir e modificar dados. O estado do aplicativo pode ser afetado pelos dados que entram no aplicativo de fora e pelas ações do usuário

É claro que em tal aplicativo, as fontes de alterações de estado podem ser entidades diferentes, enquanto as alterações iniciadas por fontes diferentes podem ocorrer quase simultaneamente. Se gerenciarmos o estado manualmente, pode ser difícil monitorar o que está acontecendo. Essas dificuldades levam a contradições. Por exemplo, um tweet pode ser excluído, mas ainda será exibido no fluxo de tweets. Ou, digamos, o usuário possa ler a notificação ou a mensagem, mas ainda será exibida no programa como não visualizada.

O usuário pode gostar do tweet, um coração aparecerá na interface do programa, mas uma solicitação de rede que envia informações sobre o mesmo para o servidor não funcionará. Como resultado, o que o usuário vê será diferente do que está armazenado no servidor. É para evitar tais situações que o Redux pode ser necessário.

Como o Redux funciona?


Na biblioteca Redux, existem três conceitos principais que visam tornar o gerenciamento do estado do aplicativo simples e direto:

  1. Armazenamento (loja). Um repositório Redux é um objeto JavaScript que representa o estado de um aplicativo. Ela desempenha o papel de "a única fonte de dados confiáveis". Isso significa que o aplicativo inteiro deve confiar no armazenamento como a única entidade responsável por representar o estado.
  2. Acções O armazenamento de estado é somente leitura. Isso significa que não pode ser modificado acessando-o diretamente. A única maneira de modificar o conteúdo do repositório é usar ações. Qualquer componente que queira alterar o estado deve executar a ação apropriada.
  3. Redutores (redutores), também chamados de "conversores". Um redutor é uma função pura que descreve como um estado é modificado por meio de ações. O redutor assume o estado e a ação atuais, cuja execução foi solicitada por um determinado componente do aplicativo, após o qual retorna o estado transformado.

O uso desses três conceitos significa que o aplicativo não deve mais monitorar diretamente eventos que são fontes de alterações de estado (ações do usuário, respostas da API, ocorrência de eventos associados ao recebimento de determinados dados por meio do protocolo WebSocket etc.) e tomar decisões sobre como esses eventos afetarão a condição.

Ao usar o modelo Redux, esses eventos podem acionar ações que mudarão de estado. Os componentes que precisam usar dados armazenados no estado do aplicativo podem simplesmente se inscrever nas alterações de estado e receber informações de seu interesse. Ao usar todos esses mecanismos, o Redux está comprometido em fazer alterações previsíveis no estado do aplicativo.

Aqui está um exemplo esquemático que demonstra como você pode organizar um sistema simples de gerenciamento de estado usando o Redux em nosso aplicativo fictício:

import { createStore } from 'redux'; //  const tweets = (state = {tweets: []}, action) => {  switch (action.type) {    //     ,     .    case 'SHOW_NEW_TWEETS':      state.numberOfNewTweets = action.count;      return state.tweets.concat([action.tweets]);    default:      return state;  } }; //  ,     . SHOW_NEW_TWEETS const newTweetsAction = (tweets) => {  return {      type: 'SHOW_NEW_TWEETS',      tweets: tweets,      count: tweets.length  }; }; const store = createStore(tweets); twitterApi.fetchTweets()  .then(response => {    //  ,        ,    //    Redux.    store.dispatch(newTweetsAction(response.data));  }); //  ,    SHOW_NEW_TWEETS     //         . const postTweet = (text) => {  twitterApi.postTweet(text)  .then(response => {    store.dispatch(newTweetsAction([response.data]));  }); }; // ,  ,   WebSocket,   . //         . SHOW_NEW_TWEETS socket.on('newTweets', (tweets) => { store.dispatch(newTweetsAction(tweets)); }; //     ,  React,       , // ,         . //         , //    . store.subscribe(() => {  const { tweets } = store.getSTate();  render(tweets); }); 

Tomando esse código como base, podemos equipar nosso sistema de gerenciamento de estados de aplicativos com ações adicionais e enviá-los de diferentes locais do aplicativo sem correr o risco de ficar confuso.

Aqui está o material com o qual você pode aprender mais sobre os três princípios fundamentais do Redux.

Agora vamos falar sobre o uso do Redux em um ambiente de servidor.

Migrando princípios de Redux para o ambiente do servidor


Exploramos os recursos do Redux usados ​​no desenvolvimento de aplicativos clientes. Mas, como o Redux é uma biblioteca JavaScript, teoricamente também pode ser usado em um ambiente de servidor. Vamos refletir sobre como os princípios acima podem ser aplicados no servidor.

Lembra-se de como conversamos sobre como é o estado do aplicativo cliente? Note-se que existem algumas diferenças conceituais entre aplicativos cliente e servidor. Portanto, os aplicativos clientes tendem a manter o estado entre vários eventos, por exemplo, entre a execução de solicitações ao servidor. Esses aplicativos são chamados de aplicativos com estado.

Se eles não se esforçarem para armazenar o estado, então, por exemplo, ao trabalhar com um determinado serviço da Web que exija login e senha, o usuário deverá executar este procedimento sempre que acessar uma nova página da interface da Web correspondente.

Os aplicativos de back-end, por outro lado, se esforçam para não armazenar o estado (eles também são chamados de aplicativos sem estado). Aqui, falando em "aplicativos de back-end", queremos dizer principalmente projetos baseados em certas APIs separadas dos aplicativos de front-end. Isso significa que as informações sobre o estado do sistema devem ser fornecidas a aplicativos similares sempre que acessadas. Por exemplo, a API não monitora se o usuário está logado ou não. Ele determina seu status analisando o token de autenticação em seus pedidos para esta API.

Isso nos leva a uma razão importante pela qual o Redux dificilmente seria usado em servidores na forma em que descrevemos seus recursos acima.

O fato é que o Redux foi projetado para armazenar o estado temporário do aplicativo. Mas o estado do aplicativo armazenado no servidor geralmente deve existir por tempo suficiente. Se você usasse o repositório Redux no aplicativo Node.js. do lado do servidor, o estado desse aplicativo seria limpo toda vez que o processo do node parar. E se estamos falando de um servidor PHP que implementa um esquema de gerenciamento de estado semelhante, o estado será limpo quando cada nova solicitação chegar ao servidor.

A situação é ainda mais complicada se considerarmos os aplicativos de servidor em termos de escalabilidade. Se você tivesse que escalar o aplicativo horizontalmente, aumentando o número de servidores, haveria muitos processos Node.js. em execução ao mesmo tempo, e cada um deles teria sua própria opção de estado. Isso significa que, com o recebimento simultâneo de duas solicitações idênticas ao back-end, diferentes respostas poderiam ter sido fornecidas.

Como aplicar os princípios de gerenciamento de estado discutidos por nós no servidor? Vamos dar uma outra olhada nos conceitos de Redux e ver como eles geralmente são usados ​​em um ambiente de servidor:

  1. Repositório. No back-end, "a única fonte de dados confiáveis" é geralmente um banco de dados. Às vezes, para facilitar o acesso aos dados que geralmente são necessários ou por algum outro motivo, uma cópia de parte do banco de dados pode ser feita - na forma de um cache ou na forma de um arquivo. Normalmente, essas cópias são somente leitura. Os mecanismos que os controlam são inscritos nas alterações no repositório principal e, quando essas alterações ocorrem, atualizam o conteúdo das cópias.
  2. Ações e redutores. Eles são os únicos mecanismos usados ​​para mudar de estado. Na maioria dos aplicativos de back-end, o código é escrito em um estilo imperativo, o que não é particularmente propício ao uso de conceitos e redutores de ação.

Considere dois padrões de design que, por sua natureza, são semelhantes ao que a biblioteca Redux pretende fazer. Estes são CQRS e Event Sourcing. De fato, eles apareceram antes do Redux, sua implementação pode ser extremamente difícil, então falaremos sobre eles muito brevemente.

CQRS e fornecimento de eventos


CQRS (Segregação de responsabilidade de consulta de comando) é um padrão de design, na implementação em que o aplicativo lê dados do armazenamento usando apenas consultas e gravando apenas usando comandos.

Ao usar o CQRS, a única maneira de alterar o estado de um aplicativo é enviar um comando. Os comandos são semelhantes às ações do Redux. Por exemplo, no Redux, você pode escrever um código que corresponda a esse esquema:

 const action = { type: 'CREATE_NEW_USER', payload: ... }; store.dispatch(action); //      const createUser = (state = {}, action) => { // }; 

Ao usar o CQRS, algo assim seria assim:

 //      class Command { handle() { } } class CreateUserCommand extends Command { constructor(user) {   super();   this.user = user; } handle() {   //        } } const createUser = new CreateUserCommand(user); //   (   handle()) dispatch(createUser); //      CommandHandler commandHandler.handle(createUser); 

As consultas são mecanismos de leitura de dados no modelo CQRS. Eles são equivalentes à construção store.getState() . Em uma implementação simples do CQRS, as consultas interagirão diretamente com o banco de dados, recuperando registros dele.

O modelo de fornecimento de eventos foi projetado para registrar todas as alterações no estado do aplicativo como uma sequência de eventos. Este modelo é mais adequado para aplicativos que precisam saber não apenas sobre seu estado atual, mas também sobre o histórico de suas alterações, sobre como o aplicativo atingiu seu estado atual. Como exemplos, você pode citar o histórico de operações com contas bancárias, rastreamento de encomendas, trabalho com pedidos em lojas on-line, organização de transporte de carga e logística.

Aqui está um exemplo de implementação do modelo de Event Sourcing:

 //    Event Sourcing function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {   BankAccount.where({ id: fromAccount.id })     .decrement({ amount });   BankAccount.where({ id: toAccount.id })     .increment({ amount }); } function makeOnlinePayment(account, amount) {   BankAccount.where({ id: account.id })     .decrement({ amount }); } //    Event Sourcing function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {   dispatchEvent(new TransferFrom(fromAccount, amount, toAccount));   dispatchEvent(new TransferTo(toAccount, amount, fromAccount)); } function makeOnlinePayment(account, amount) {   dispatchEvent(new OnlinePaymentFrom(account, amount)); } class TransferFrom extends Event {   constructor(account, amount, toAccount) {     this.account = account;     this.amount = amount;     this.toAccount = toAccount;   }     handle() {     //    OutwardTransfer        OutwardTransfer.create({ from: this.account, to: this.toAccount, amount: this.amount, date: Date.now() });         //          BankAccount.where({ id: this.account.id })       .decrement({ amount: this.amount });   } } class TransferTo extends Event {   constructor(account, amount, fromAccount) {     this.account = account;     this.amount = amount;     this.fromAccount = fromAccount;   }     handle() {     //    InwardTransfer        InwardTransfer.create({ from: this.fromAccount, to: this.account, amount: this.amount, date: Date.now() });         //          BankAccount.where({ id: this.account.id })       .increment({ amount: this.amount });   } } class OnlinePaymentFrom extends Event {   constructor(account, amount) {     this.account = account;     this.amount = amount;   }     handle() {     //    OnlinePayment        OnlinePayment.create({ from: this.account, amount: this.amount, date: Date.now() });         //          BankAccount.where({ id: this.account.id })       .decrement({ amount: this.amount });   } } 

O que acontece aqui também se assemelha ao trabalho com ações Redux.

No entanto, o mecanismo de registro de eventos também organiza o armazenamento de informações a longo prazo sobre cada alteração de estado, e não apenas o armazenamento do próprio estado. Isso nos permite reproduzir essas alterações no momento em que precisamos, restaurando assim o conteúdo do estado do aplicativo nesse momento. Por exemplo, se precisamos entender quanto dinheiro havia na conta bancária para uma determinada data, precisamos reproduzir apenas os eventos que ocorreram com a conta bancária até chegarmos à data certa. Os eventos nesse caso são representados por recebimentos de fundos na conta e debitados deles, debitando a comissão do banco e outras operações similares. Se ocorrerem erros (ou seja, quando ocorrerem eventos contendo dados incorretos), podemos invalidar o estado atual do aplicativo, corrigir os dados correspondentes e retornar ao estado atual do aplicativo, agora formado sem erros.

Os modelos CQRS e Event Sourcing são frequentemente usados ​​juntos. E, curiosamente, o Redux, de fato, é parcialmente baseado nesses modelos. Os comandos podem ser escritos para que, quando chamados, enviem eventos. Em seguida, os eventos interagem com o repositório (banco de dados) e atualizam o estado. Em aplicativos em tempo real, os objetos de consulta também podem ouvir eventos e receber informações de status atualizadas do repositório.

O uso de qualquer um desses modelos em um aplicativo simples pode complicá-lo desnecessariamente. No entanto, no caso de aplicativos criados para solucionar problemas complexos de negócios, o CQRS e o Event Sourcing são abstrações poderosas que ajudam a modelar melhor a área de assunto desses aplicativos e melhorar o gerenciamento de estado.

Observe que os padrões CQRS e Event Sourcing podem ser implementados de maneiras diferentes, enquanto algumas de suas implementações são mais complexas que outras. Consideramos apenas exemplos muito simples de sua implementação. Se você estiver criando aplicativos de servidor no Node.js, dê uma olhada no wolkenkit . Essa estrutura, entre as encontradas nesta área, fornece ao desenvolvedor uma das interfaces mais simples para implementar os modelos CQRS e Event Sourcing.

Sumário


O Redux é uma ótima ferramenta para gerenciar o estado de um aplicativo, a fim de tornar previsíveis as alterações de estado. Neste artigo, falamos sobre os principais conceitos desta biblioteca e descobrimos que, embora o Redux em um ambiente de servidor provavelmente não seja uma boa ideia, você pode aplicar princípios semelhantes em um servidor usando os modelos CQRS e Event Sourcing .

Caros leitores! Como você organiza o gerenciamento de estado dos aplicativos cliente e servidor?

Source: https://habr.com/ru/post/pt437804/


All Articles