10 linhas de código para reduzir a dor do seu projeto Vue

... ou familiaridade com os plugins Vue JS como um exemplo de um barramento de eventos integrado


Algumas palavras sobre ...


Olá a todos! Farei uma reserva imediatamente. Eu realmente amo o VueJS, escrevo ativamente há mais de 2 anos e não acho que o desenvolvimento possa prejudicar pelo menos em um grau significativo :)
Por outro lado, estamos sempre tentando encontrar soluções universais que ajudem a gastar menos tempo em trabalhos mecânicos e mais no que é realmente interessante. Às vezes, a solução é particularmente bem-sucedida. Um deles eu quero compartilhar com você. As 10 linhas que serão discutidas (spoiler: no final, haverá um pouco mais) nasceram no processo de trabalho no projeto Cloud Blue - Connect, que é um aplicativo bastante grande com mais de 400 componentes. A solução que encontramos já está integrada em vários pontos do sistema e, por mais de meio ano, nunca exige correções, portanto pode ser considerada com segurança testada com sucesso quanto à estabilidade.

E o último. Antes de prosseguir diretamente para a solução, gostaria de me aprofundar um pouco mais na descrição dos três tipos de interação entre os componentes do Vue: os princípios do fluxo unidirecional, o padrão da loja e o barramento de eventos. Se essa explicação for desnecessária (ou chata) para você, vá diretamente para a seção com a solução - tudo é o mais breve e técnico possível.

Um pouco sobre como os componentes do Vue se comunicam


Talvez a primeira pergunta que surja para a pessoa que escreveu seu primeiro componente seja como ele receberá dados para o trabalho e como, por sua vez, ele transferirá os dados recebidos por ele "fora". O princípio de interação adotado no framework Vue JS é chamado ...

Fluxo de dados unidirecional


Em resumo, esse princípio soa como "propriedades - desativadas, eventos - ativadas". Ou seja, para receber dados de fora (“de cima”), registramos uma propriedade especial dentro do componente no qual a estrutura, se necessário, grava nossos dados recebidos “de fora”. Para transferir dados "para cima", dentro do componente no lugar certo, chamamos o método especial de estrutura $ emit, que passa nossos dados para o manipulador do componente pai. Ao mesmo tempo, no Vue JS, não podemos apenas "transmitir" o evento até uma profundidade ilimitada (como por exemplo no Angular 1.x). Ele "aparece" apenas um nível, para o pai imediato. O mesmo vale para eventos. Para transferi-los para o próximo nível, para cada um deles, você também precisa registrar uma interface especial - propriedades e eventos que transmitirão nossa "mensagem" ainda mais.

Isso pode ser descrito como um prédio de escritórios no qual os trabalhadores só podem passar dos andares para os vizinhos - um para cima e outro para baixo. Portanto, para transferir o “documento para assinatura” do quinto andar para o segundo, será necessária uma cadeia de três trabalhadores que o entregarão do quinto andar para o segundo e mais três que o devolverão ao quinto.

"Mas isso é inconveniente!" Obviamente, isso nem sempre é conveniente do ponto de vista do desenvolvimento, mas, olhando o código de cada componente, podemos ver o que e para quem ele passa. Não precisamos ter em mente toda a estrutura do aplicativo para entender se nosso componente está "a caminho" do evento ou não. Podemos ver isso no componente pai.

Embora as vantagens dessa abordagem sejam compreensíveis, ela também apresenta desvantagens óbvias, a saber, a alta coesão dos componentes. Simplificando, para colocarmos algum componente na estrutura, precisamos cobri-lo com as interfaces necessárias para gerenciar seu estado. Para reduzir essa conectividade, eles costumam usar "ferramentas de gerenciamento de estado". Talvez a ferramenta mais popular para o Vue seja ...

Vuex (lateral)


Continuando nossa analogia com um prédio de escritórios, o Vuex Stor é um serviço postal interno. Imagine que em cada andar do escritório haja uma janela para emissão e recebimento de encomendas. No quinto andar, transmitem o documento nº 11 para assinatura e, no segundo, perguntam periodicamente: “Existem documentos para assinatura?”, Assinam os documentos existentes e os devolvem. No quinto, eles também perguntam: "Existem signatários?" Ao mesmo tempo, os funcionários podem mudar para outros andares ou para outras salas - o princípio do trabalho não será alterado enquanto o correio estiver funcionando.

Aproximadamente por esse princípio, o padrão chamado Store também funciona. Usando a interface Vuex, um armazém de dados global é registrado e configurado, e os componentes se inscrevem nele. E não importa em que nível de estrutura a apelação ocorreu, a loja sempre fornecerá as informações corretas.

Parece que com isso todos os problemas já foram resolvidos. Mas em algum momento do nosso edifício metafórico, um funcionário quer ligar para outro para almoçar ... ou relatar algum tipo de erro. E aqui começa o estranho. A mensagem em si não requer transmissão como tal. Mas, para usar o e-mail, você precisa transferir alguma coisa. Em seguida, nossos funcionários criam um código. Uma bola verde - vá almoçar, dois cubos vermelhos - ocorreu um erro de aplicação E-981273, três moedas amarelas - verifique seu e-mail e assim por diante.

É fácil adivinhar que, com a ajuda dessa metáfora incômoda, descrevo situações em que precisamos garantir a resposta do nosso componente a um evento que ocorreu em outro componente, que por si só não está conectado de nenhuma maneira ao fluxo de dados. A gravação de um novo item está concluída - você precisa refazer a coleção. Ocorreu um erro 403 não autorizado - você precisa iniciar um logout do usuário e assim por diante. A prática usual (e longe da melhor) nesse caso é criar sinalizadores dentro da loja ou interpretar indiretamente os dados armazenados e suas alterações. Isso leva rapidamente à poluição da própria loja e à lógica dos componentes ao seu redor.

Nesse estágio, começamos a pensar em como transmitir eventos diretamente, ignorando toda a cadeia de componentes. E, um pouco do google ou vasculhando a documentação, encontramos um padrão ...

Barramento de evento


Do ponto de vista técnico, o barramento de eventos é um objeto que permite o uso de um método especial para iniciar um "evento" e se inscrever usando outro. Em outras palavras, ao se inscrever no evento eventA, esse objeto armazena a função de manipulador de passagem dentro de sua estrutura, que será chamada quando o método de inicialização com a chave eventA for chamado em algum lugar do aplicativo. Para assinar ou executar, basta acessá-lo por importação ou por referência, e pronto.

Metaforicamente, em nosso “prédio”, um ônibus é um bate-papo comum no messenger. Componentes assinam um "bate-papo geral" para o qual outros componentes enviam mensagens. Assim que uma "mensagem" aparecer no "bate-papo", ao qual o componente se inscreveu, o manipulador será iniciado.

Existem muitas maneiras diferentes de criar um barramento de eventos. Você pode escrever por conta própria ou pode usar soluções prontas - o mesmo RxJS, que fornece enorme funcionalidade para trabalhar com fluxos inteiros de eventos. Mas, na maioria das vezes, ao trabalhar com o VueJS, eles usam, curiosamente, o próprio VueJS. A instância do Vue criada por meio do construtor (new Vue ()) fornece uma interface de evento bonita e concisa, descrita na documentação oficial.

Aqui chegamos perto da próxima pergunta ...

O que nós queremos?


E queremos construir um barramento de eventos em nosso aplicativo. Mas temos dois requisitos adicionais:

  1. Deve ser facilmente acessível em todos os componentes. As importações separadas para cada uma das dezenas de componentes nos parecem redundantes.
  2. Deve ser modular. Não queremos manter todos os nomes de eventos em mente para evitar a situação em que o evento "criado por item" aciona manipuladores de todo o aplicativo. Portanto, queremos poder separar facilmente um pequeno fragmento da árvore de componentes em um módulo separado e transmitir seus eventos dentro e não fora.

Para implementar essa funcionalidade impressionante, usamos a poderosa interface de plug-in fornecida pelo VueJS. Você pode se familiarizar com isso com mais detalhes aqui na página com documentação oficial.

Vamos registrar nosso plugin primeiro. Para fazer isso, logo antes do ponto de inicialização do aplicativo Vue (antes de chamar Vue. $ Mount ()), colocamos o seguinte bloco:

Vue.use({   
  install(vue) { }, 
});

De fato, os plugins do Vue são uma maneira de estender a funcionalidade da estrutura em todo o nível do aplicativo. A interface do plug-in fornece várias maneiras de se integrar ao componente, mas hoje apresentaremos a interface mixin. Este método aceita um objeto que estende o descritor de cada componente antes de iniciar o ciclo de vida no aplicativo.(O código do componente que escrevemos provavelmente não é o componente em si, mas uma descrição de seu comportamento e encapsulamento de uma certa parte da lógica que a estrutura usa em vários estágios do seu ciclo de vida. A inicialização do plug-in está fora do ciclo de vida do componente, precedendo-o, portanto, nós dizemos "descritor", não um componente, para enfatizar que exatamente o código que está escrito em nosso arquivo, e não alguma entidade que é um produto do trabalho da estrutura, será transferido para a seção mixin do plug-in) .

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

É esse objeto vazio que conterá as extensões para nossos componentes. Mas, para começar, outra parada. No nosso caso, queremos criar uma interface para acessar o barramento no nível de cada componente. Vamos adicionar o campo '$ broadcast' ao nosso descritor, ele armazenará um link para o nosso barramento. Para fazer isso, use Vue.prototype:

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

Agora precisamos criar o próprio barramento, mas primeiro, vamos relembrar o requisito da modularidade e assumir que no descritor de componentes declararemos um novo módulo com o campo "$ module" com algum valor de texto (precisaremos um pouco mais tarde). Se o campo $ module for especificado no próprio componente, criaremos um novo barramento para ele; caso contrário, passaremos o link para o pai através do campo $ parent. Observe que os campos do descritor estarão disponíveis para nós através do campo $ options.

Colocaremos a criação de nosso ônibus o mais cedo possível - no gancho beforeCreate.

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

Por fim, vamos preencher os ramos lógicos. Se o descritor contiver uma nova declaração de módulo, crie uma nova instância de barramento, caso contrário, pegue o link em $ parent.

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

Descartamos o anúncio do plugin, consideramos ... 1, 2, 3, 4 ... 10 linhas, como prometi!

Podemos fazer melhor?


Claro que podemos. Este código é facilmente extensível. Por exemplo, no nosso caso, além de $ broadcast, decidimos adicionar a interface $ rootBroadcast, que dá acesso a um único barramento para todo o aplicativo. Os eventos que o usuário executa no barramento $ broadcast são duplicados no barramento $ rootBroadcast, para que você possa se inscrever em todos os eventos de um módulo específico (nesse caso, o nome do evento será passado para o manipulador como o primeiro argumento) ou para todos os eventos de aplicativos em geral (depois o nome do módulo será passado para o manipulador com o primeiro argumento, o nome do evento com o segundo e os dados transmitidos com o evento serão transmitidos com os seguintes argumentos). Esse design nos permitirá estabelecer interação entre os módulos, além de travar um único manipulador nos eventos de diferentes módulos.

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

Vamos ver como podemos conseguir isso:

Primeiro, crie um único barramento, que será organizado através de $ rootBroadcast, e o próprio campo com um link:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Agora precisamos da associação do módulo em cada componente, então vamos expandir a definição de modularidade como esta:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Em seguida, precisamos fazer com que o evento no barramento local modular reflita da maneira que precisamos para a raiz. Para fazer isso, primeiro precisamos criar uma interface proxy simples e colocar o próprio barramento na propriedade condicionalmente privada de $ bus:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

E, finalmente, adicione métodos de proxy ao objeto - porque agora o campo $ broadcast não fornece acesso direto ao barramento:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

Bem, como toque final, lembremos que obtemos acesso ao barramento fechando, o que significa que os manipuladores adicionados uma vez não serão limpos com o componente, mas permanecerão durante todo o tempo trabalhando com o aplicativo. Isso pode causar efeitos colaterais desagradáveis, então vamos adicionar uma função de limpeza de ouvinte ao nosso barramento no final do ciclo de vida do componente:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

Portanto, essa opção fornece uma funcionalidade mais interessante, embora menos concisa. Com ele, você pode implementar um sistema completo de comunicação alternativa entre componentes. Além disso, ele está completamente sob nosso controle e não traz dependências externas ao nosso projeto.

Espero que, depois de ler, você tenha adquirido ou atualizado seus conhecimentos sobre os plug-ins do Vue e, talvez, da próxima vez que precisar adicionar alguma funcionalidade genérica ao seu aplicativo, possa implementá-lo com mais eficiência - sem adicionar dependências externas.

All Articles