Comunicação entre componentes irmãos em VueJs 2.0

113

Visão geral

No Vue.js 2.x, model.syncserá preterido .

Então, qual é a maneira adequada de se comunicar entre componentes irmãos no Vue.js 2.x ?


fundo

Pelo que entendi Vue 2.x, o método preferido para comunicação entre irmãos é usar uma loja ou um barramento de evento .

De acordo com Evan (criador do Vue):

Também vale a pena mencionar que "passar dados entre componentes" geralmente é uma má ideia, porque no final o fluxo de dados se torna impossível de rastrear e muito difícil de depurar.

Se um dado precisa ser compartilhado por vários componentes, prefira lojas globais ou Vuex .

[ Link para discussão ]

E:

.oncee .syncestão obsoletos. Os adereços agora estão sempre em uma direção para baixo. Para produzir efeitos colaterais no escopo pai, um componente precisa explicitamente emitum evento em vez de depender de vinculação implícita.

Portanto, Evan sugere o uso de $emit()e $on().


Preocupações

O que me preocupa é:

  • Cada um storee eventtem uma visibilidade global (corrija-me se estiver errado);
  • É muito desperdício criar uma nova loja para cada comunicação secundária;

O que eu quero é algum escopo events ou storesvisibilidade para componentes irmãos. (Ou talvez eu não tenha entendido a ideia acima.)


Questão

Então, qual é a maneira correta de se comunicar entre componentes irmãos?

Sergei Panfilov
fonte
2
$emitcombinado com v-modelpara emular .sync. Eu acho que você deveria seguir o caminho da Vuex
eltonkamami
3
Portanto, considerei a mesma preocupação. Minha solução é usar um emissor de evento com um canal de transmissão equivalente ao 'escopo' - ou seja, uma configuração de filho / pai e irmão usa o mesmo canal para se comunicar. No meu caso, eu uso a biblioteca de rádio radio.uxder.com porque são apenas algumas linhas de código e são à prova de balas, mas muitos escolheriam o nó EventEmitter.
Tremendus Apps

Respostas:

84

Com o Vue 2.0, estou usando o mecanismo eventHub conforme demonstrado na documentação .

  1. Defina o hub de eventos centralizado.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Agora, em seu componente, você pode emitir eventos com

    this.eventHub.$emit('update', data)
  3. E para ouvir você faz

    this.eventHub.$on('update', data => {
    // do your thing
    })

Atualização Veja a resposta de @alex , que descreve uma solução mais simples.

kakoni
fonte
3
Apenas um aviso: fique de olho nos Global Mixins e tente evitá-los sempre que possível, pois de acordo com este link vuejs.org/v2/guide/mixins.html#Global-Mixin eles podem afetar até mesmo componentes de terceiros.
Vini.g.fer
6
Uma solução muito mais simples é usar o que @Alex descreveu - this.$root.$emit()ethis.$root.$on()
Webnet de
5
Para referência futura, não atualize sua resposta com a resposta de outra pessoa (mesmo se você achar que é melhor e fizer referência a ela). Link para a resposta alternativa, ou até mesmo peça ao OP para aceitar a outra se você achar que deveria - mas copiar a resposta dele para a sua não é uma boa forma e desencoraja os usuários de dar crédito onde é devido, pois eles podem simplesmente votar positivamente apenas no seu responder apenas. Incentive-os a navegar para (e assim votar positivamente) na resposta que você está referenciando, não incluindo essa resposta na sua.
GrayedFox
4
Obrigado pelo valioso feedback @GrayedFox, atualizei minha resposta de acordo.
kakoni
2
Observe que esta solução não terá mais suporte no Vue 3. Consulte stackoverflow.com/a/60895076/752916
AlexMA
146

Você pode até mesmo torná-lo mais curto e usar a Vue instância raiz como Hub de eventos global:

Componente 1:

this.$root.$emit('eventing', data);

Componente 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
fonte
2
Isso funciona melhor do que definir um hub de eventos adicionais e anexá-lo a qualquer consumidor de eventos.
schad de
2
Sou um grande fã desta solução, pois realmente não gosto de eventos que tenham alcance. No entanto, não uso o VueJS todos os dias, então estou curioso para saber se há alguém por aí que vê problemas com essa abordagem.
Webnet
2
A solução mais simples de todas as respostas
Vikash Gupta
1
agradável, curto e fácil de implementar, fácil de entender também
nada
1
Se você deseja apenas comunicação direta entre irmãos, use $ parent em vez de $ root
Malkev
47

Tipos de comunicação

Ao projetar um aplicativo Vue (ou, na verdade, qualquer aplicativo baseado em componente), existem diferentes tipos de comunicação que dependem de quais preocupações estamos lidando e eles têm seus próprios canais de comunicação.

Lógica de negócios: refere-se a tudo que é específico para seu aplicativo e seu objetivo.

Lógica de apresentação: qualquer coisa com a qual o usuário interage ou que resulte da interação do usuário.

Essas duas preocupações estão relacionadas a estes tipos de comunicação:

  • Estado do aplicativo
  • Pai-filho
  • Pai-filho
  • Irmãos

Cada tipo deve usar o canal de comunicação correto.


Canais de comunicação

Canal é um termo vago que usarei para me referir a implementações concretas para trocar dados em torno de um aplicativo Vue.

Adereços: lógica de apresentação pai-filho

O canal de comunicação mais simples no Vue para comunicação direta entre pais e filhos . Deve ser usado principalmente para passar dados relacionados à lógica de apresentação ou um conjunto restrito de dados para baixo na hierarquia.

Refs e métodos: Antipadrão de apresentação

Quando não faz sentido usar um prop para permitir que um filho manipule um evento de um pai, configurar um refno componente filho e chamar seus métodos é ótimo.

Não faça isso, é um antipadrão. Repense a arquitetura de seu componente e o fluxo de dados. Se você quiser chamar um método em um componente filho de um pai, provavelmente é hora de levantar o estado ou considerar as outras maneiras descritas aqui ou nas outras respostas.

Eventos: lógica de apresentação filho-pai

$emite $on. O canal de comunicação mais simples para comunicação direta entre pais e filhos. Novamente, deve ser usado para lógica de apresentação.

Ônibus de eventos

A maioria das respostas dá boas alternativas para o bus de eventos, que é um dos canais de comunicação disponíveis para componentes distantes, ou qualquer coisa na verdade.

Isso pode se tornar útil ao passar adereços por todo o lugar, de muito para baixo, até componentes filhos profundamente aninhados, com quase nenhum outro componente precisando deles no meio. Use moderadamente para dados cuidadosamente selecionados.

Tenha cuidado: a criação subsequente de componentes que estão se vinculando ao barramento de eventos serão vinculados mais de uma vez - levando a múltiplos manipuladores acionados e vazamentos. Pessoalmente, nunca senti a necessidade de um ônibus para eventos em todos os aplicativos de página única que projetei no passado.

O seguinte demonstra como um simples erro leva a um vazamento em que o Itemcomponente ainda dispara, mesmo se removido do DOM.

Lembre-se de remover os ouvintes do destroyedgancho do ciclo de vida.

Loja centralizada (lógica de negócios)

Vuex é o caminho a seguir com Vue para a gestão do estado . Ele oferece muito mais do que apenas eventos e está pronto para ser aplicado em grande escala.

E agora você pergunta :

[S] devo criar a loja da Vuex para cada comunicação secundária?

Realmente brilha quando:

  • lidando com sua lógica de negócios,
  • comunicar-se com um back-end (ou qualquer camada de persistência de dados, como armazenamento local)

Portanto, seus componentes podem realmente se concentrar nas coisas que devem ser, gerenciando interfaces de usuário.

Isso não significa que você não pode usá-lo para a lógica do componente, mas eu definiria o escopo dessa lógica para um módulo Vuex com namespace com apenas o estado de IU global necessário.

Para evitar lidar com uma grande bagunça em um estado global, o armazenamento deve ser separado em vários módulos com espaço de nomes.


Tipos de componentes

Para orquestrar todas essas comunicações e facilitar a reutilização, devemos pensar nos componentes como dois tipos diferentes.

  • Contêineres específicos de aplicativos
  • Componentes genéricos

Novamente, isso não significa que um componente genérico deva ser reutilizado ou que um contêiner específico de aplicativo não possa ser reutilizado, mas eles têm responsabilidades diferentes.

Contêineres específicos de aplicativos

Estes são apenas componentes Vue simples que envolvem outros componentes Vue (contêineres genéricos ou outros aplicativos específicos). É aqui que a comunicação da loja Vuex deve acontecer e este contêiner deve se comunicar por outros meios mais simples, como adereços e ouvintes de eventos.

Esses contêineres podem até mesmo não ter nenhum elemento DOM nativo e permitir que os componentes genéricos lidem com os modelos e as interações do usuário.

escopo de alguma forma eventsou storesvisibilidade para componentes irmãos

É aqui que o escopo acontece. A maioria dos componentes não sabe sobre a loja e este componente deve (principalmente) usar um módulo de loja com espaço de nomes com um conjunto limitado de getterse actionsaplicado com os auxiliares de ligação Vuex fornecidos .

Componentes genéricos

Eles devem receber seus dados de props, fazer alterações em seus próprios dados locais e emitir eventos simples. Na maioria das vezes, eles não deveriam saber que existe uma loja Vuex.

Eles também podem ser chamados de contêineres, pois sua única responsabilidade é o envio para outros componentes da IU.


Comunicação entre irmãos

Então, depois de tudo isso, como devemos nos comunicar entre dois componentes irmãos?

É mais fácil de entender com um exemplo: digamos que temos uma caixa de entrada e seus dados devem ser compartilhados pelo aplicativo (irmãos em locais diferentes na árvore) e persistidos com um back-end.

Começando com o pior cenário , nosso componente mesclaria apresentação e lógica de negócios .

// MyInput.vue
<template>
    <div class="my-input">
        <label>Data</label>
        <input type="text"
            :value="value" 
            :input="onChange($event.target.value)">
    </div>
</template>
<script>
    import axios from 'axios';

    export default {
        data() {
            return {
                value: "",
            };
        },
        mounted() {
            this.$root.$on('sync', data => {
                this.value = data.myServerValue;
            });
        },
        methods: {
            onChange(value) {
                this.value = value;
                axios.post('http://example.com/api/update', {
                        myServerValue: value
                    })
                    .then((response) => {
                        this.$root.$emit('update', response.data);
                    });
            }
        }
    }
</script>

Para separar essas duas questões, devemos envolver nosso componente em um contêiner específico do aplicativo e manter a lógica de apresentação em nosso componente de entrada genérico.

Nosso componente de entrada agora é reutilizável e não conhece o back-end nem os irmãos.

// MyInput.vue
// the template is the same as above
<script>
    export default {
        props: {
            initial: {
                type: String,
                default: ""
            }
        },
        data() {
            return {
                value: this.initial,
            };
        },
        methods: {
            onChange(value) {
                this.value = value;
                this.$emit('change', value);
            }
        }
    }
</script>

Nosso contêiner específico de aplicativo agora pode ser a ponte entre a lógica de negócios e a comunicação da apresentação.

// MyAppCard.vue
<template>
    <div class="container">
        <card-body>
            <my-input :initial="serverValue" @change="updateState"></my-input>
            <my-input :initial="otherValue" @change="updateState"></my-input>

        </card-body>
        <card-footer>
            <my-button :disabled="!serverValue || !otherValue"
                       @click="saveState"></my-button>
        </card-footer>
    </div>
</template>
<script>
    import { mapGetters, mapActions } from 'vuex';
    import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
    import { MyButton, MyInput } from './components';

    export default {
        components: {
            MyInput,
            MyButton,
        },
        computed: mapGetters(NS, [
            GETTERS.serverValue,
            GETTERS.otherValue,
        ]),
        methods: mapActions(NS, [
            ACTIONS.updateState,
            ACTIONS.updateState,
        ])
    }
</script>

Como as ações da loja Vuex lidam com a comunicação de backend, nosso contêiner aqui não precisa saber sobre axios e backend.

Emile Bergeron
fonte
3
Concordar com o comentário sobre os métodos serem " o mesmo acoplamento do uso de adereços "
ghybs
Eu gosto dessa resposta. Mas você poderia elaborar sobre o Event Bus e a nota "Tenha cuidado:"? Talvez você possa dar algum exemplo, não entendo como os componentes podem ser vinculados duas vezes.
vandroid
Como você se comunica entre o componente pai e o componente neto, por exemplo, validação de formulário. Onde o componente pai é uma página, o filho é o formulário e o neto é o elemento do formulário de entrada?
Lord Zed
1
@vandroid Eu criei um exemplo simples que mostra um vazamento quando os ouvintes não são removidos corretamente, como todos os exemplos neste tópico.
Emile Bergeron,
@LordZed Realmente depende, mas pelo que entendi da sua situação, parece um problema de design. O Vue deve ser usado principalmente para lógica de apresentação. A validação do formulário deve ser feita em outro lugar, como na interface vanilla JS API, que uma ação Vuex chamaria com os dados do formulário.
Emile Bergeron,
10

Ok, podemos nos comunicar entre irmãos via pais usando v-oneventos.

Parent
 |-List of items //sibling 1 - "List"
 |-Details of selected item //sibling 2 - "Details"

Vamos supor que desejamos atualizar o Detailscomponente ao clicar em algum elemento List.


no Parent :

Modelo:

<list v-model="listModel"
      v-on:select-item="setSelectedItem" 
></list> 
<details v-model="selectedModel"></details>

Aqui:

  • v-on:select-itemé um evento, que será chamado em Listcomponente (veja abaixo);
  • setSelectedItemé um Parentmétodo de atualização selectedModel;

JS:

//...
data () {
  return {
    listModel: ['a', 'b']
    selectedModel: null
  }
},
methods: {
  setSelectedItem (item) {
    this.selectedModel = item //here we change the Detail's model
  },
}
//...

No List :

Modelo:

<ul>
  <li v-for="i in list" 
      :value="i"
      @click="select(i, $event)">
        <span v-text="i"></span>
  </li>
</ul>

JS:

//...
data () {
  return {
    selected: null
  }
},
props: {
  list: {
    type: Array,
    required: true
  }
},
methods: {
  select (item) {
    this.selected = item
    this.$emit('select-item', item) // here we call the event we waiting for in "Parent"
  },
}
//...

Aqui:

  • this.$emit('select-item', item)enviará o item via select-itemdiretamente no pai. E os pais irão enviar para a Detailsvista
Sergei Panfilov
fonte
5

O que normalmente faço se quero "hackear" os padrões normais de comunicação no Vue, especialmente agora que .syncestá obsoleto, é criar um EventEmitter simples que lida com a comunicação entre os componentes. De um dos meus projetos mais recentes:

import {EventEmitter} from 'events'

var Transmitter = Object.assign({}, EventEmitter.prototype, { /* ... */ })

Com este Transmitterobjeto você pode fazer, em qualquer componente:

import Transmitter from './Transmitter'

var ComponentOne = Vue.extend({
  methods: {
    transmit: Transmitter.emit('update')
  }
})

E para criar um componente de "recebimento":

import Transmitter from './Transmitter'

var ComponentTwo = Vue.extend({
  ready: function () {
    Transmitter.on('update', this.doThingOnUpdate)
  }
})

Novamente, isso é para usos realmente específicos. Não baseie todo o seu aplicativo neste padrão, use algo como Vuex.

Hector Lorenzo
fonte
1
Já estou usando vuex, mas, novamente, devo criar a loja da vuex para cada comunicação menor?
Sergei Panfilov
É difícil para mim dizer com essa quantidade de informações, mas diria que se você já estiver usando vuex sim, vá em frente. Use-o.
Hector Lorenzo
1
Na verdade, eu discordo que precisamos usar vuex para cada comunicação menor ...
Victor
Não, claro que não, tudo depende do contexto. Na verdade, minha resposta se afasta de vuex. Por outro lado, descobri que quanto mais você usa vuex e o conceito de um objeto de estado central, menos confio na comunicação entre objetos. Mas sim, concordo, tudo depende.
Hector Lorenzo
3

Como lidar com a comunicação entre irmãos depende da situação. Mas, primeiro, quero enfatizar que a abordagem de ônibus de eventos globais está indo embora no Vue 3 . Veja este RFC . Por isso decidi escrever uma nova resposta.

Padrão Ancestral Comum Mais Baixo (ou “LCA”)

Para casos simples, eu recomendo fortemente o uso do padrão de Ancestral Comum Mais Baixo (também conhecido como “dados para baixo, eventos para cima”). Esse padrão é fácil de ler, implementar, testar e depurar.

Em essência, isso significa que se dois componentes precisam se comunicar, coloque seu estado compartilhado no componente mais próximo que ambos compartilham como ancestrais. Passe dados do componente pai para o componente filho por meio de adereços e passe informações do filho para o pai emitindo um evento (veja um exemplo disso no final desta resposta).

Para um exemplo artificial, em um aplicativo de e-mail, se o componente "Para" precisasse interagir com o componente "corpo da mensagem", o estado dessa interação poderia residir em seu pai (talvez um componente chamado email-form). Você pode ter um prop no email-formchamado addresseepara que o corpo da mensagem possa preceder automaticamente Dear {{addressee.name}}o e-mail com base no endereço de e-mail do destinatário.

O LCA torna-se oneroso se a comunicação tiver que viajar longas distâncias com muitos componentes intermediários. Costumo referir colegas para este excelente post de blog . (Ignore o fato de que seus exemplos usam Ember; suas ideias são aplicáveis ​​em muitos frameworks de IU.)

Padrão de contêiner de dados (por exemplo, Vuex)

Para casos ou situações complexas em que a comunicação entre pais e filhos envolveria muitos intermediários, use o Vuex ou uma tecnologia de contêiner de dados equivalente. Quando apropriado, use módulos com namespace .

Por exemplo, pode ser razoável criar um namespace separado para uma coleção complexa de componentes com muitas interconexões, como um componente de calendário completo.

Padrão Publicar / Assinar (Event Bus)

Se o padrão de barramento de evento (ou “publicar / assinar”) for mais apropriado para suas necessidades, a equipe principal do Vue agora recomenda o uso de uma biblioteca de terceiros, como o mitt . (Consulte o RFC mencionado no parágrafo 1.)

Divagações bônus e código

Aqui está um exemplo básico da solução de menor ancestral comum para a comunicação de irmão para irmão, ilustrado por meio do jogo whack-a-mole .

Uma abordagem ingênua pode ser pensar, “a toupeira 1 deve dizer à toupeira 2 para aparecer depois de ser destruída”. Mas Vue desencoraja esse tipo de abordagem, uma vez que quer que pensemos em termos de estruturas de árvore .

Isso provavelmente é uma coisa muito boa. Um aplicativo não trivial em que os nós se comunicam diretamente entre si através das árvores DOM seria muito difícil de depurar sem algum tipo de sistema de contabilidade (como o Vuex fornece). Além disso, os componentes que usam "down data, events up" tendem a exibir baixo acoplamento e alta reutilização - ambas características altamente desejáveis ​​que ajudam a escalar aplicativos grandes.

Neste exemplo, quando uma toupeira é atingida, ela emite um evento. O componente gerenciador de jogo decide qual é o novo estado do aplicativo e, portanto, o irmão mole sabe o que fazer implicitamente depois que o Vue é renderizado novamente. É um exemplo trivial de “ancestral comum mais baixo”.

Vue.component('whack-a-mole', {
  data() {
    return {
      stateOfMoles: [true, false, false],
      points: 0
    }
  },
  template: `<div>WHACK - A - MOLE!<br/>
    <a-mole :has-mole="stateOfMoles[0]" v-on:moleMashed="moleClicked(0)"/>
    <a-mole :has-mole="stateOfMoles[1]"  v-on:moleMashed="moleClicked(1)"/>
    <a-mole :has-mole="stateOfMoles[2]" v-on:moleMashed="moleClicked(2)"/>
    <p>Score: {{points}}</p>
</div>`,
  methods: {
    moleClicked(n) {
      if(this.stateOfMoles[n]) {
         this.points++;
         this.stateOfMoles[n] = false;
         this.stateOfMoles[Math.floor(Math.random() * 3)] = true;
      }   
    }
  }
})

Vue.component('a-mole', {
  props: ['hasMole'],
  template: `<button @click="$emit('moleMashed')">
      <span class="mole-button" v-if="hasMole">🐿</span><span class="mole-button" v-if="!hasMole">🕳</span>
    </button>`
})

var app = new Vue({
  el: '#app',
  data() {
    return { name: 'Vue' }
  }
})
.mole-button {
  font-size: 2em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <whack-a-mole />
</div>

AlexMA
fonte