Comunicación entre componentes hermanos en VueJs 2.0

112

Visión general

En Vue.js 2.x, model.syncquedará obsoleto .

Entonces, ¿cuál es una forma adecuada de comunicarse entre componentes hermanos en Vue.js 2.x ?


Antecedentes

Según tengo entendido Vue 2.x, el método preferido para la comunicación entre hermanos es usar una tienda o un bus de eventos .

Según Evan (creador de Vue):

También vale la pena mencionar que "pasar datos entre componentes" es generalmente una mala idea, porque al final el flujo de datos se vuelve imposible de rastrear y muy difícil de depurar.

Si un dato necesita ser compartido por múltiples componentes, prefiera tiendas globales o Vuex .

[ Enlace a la discusión ]

Y:

.oncey .syncestán en desuso. Los accesorios ahora son siempre unidireccionales. Para producir efectos secundarios en el ámbito principal, un componente necesita explícitamente emitun evento en lugar de depender de un enlace implícito.

Entonces, Evan sugiere usar $emit()y $on().


Preocupaciones

Lo que me preocupa es:

  • Cada uno storey eventtiene una visibilidad global (corríjanme si me equivoco);
  • Es un desperdicio crear una nueva tienda para cada comunicación menor;

Lo que quiero es algún alcance events o storesvisibilidad para los componentes de los hermanos. (O quizás no entendí la idea anterior).


Pregunta

Entonces, ¿cuál es la forma correcta de comunicarse entre componentes hermanos?

Sergei Panfilov
fuente
2
$emitcombinado con v-modelemular .sync. Creo que deberías seguir el camino de
Vuex
3
Por eso he considerado la misma preocupación. Mi solución es usar un emisor de eventos con un canal de transmisión que sea equivalente a 'alcance', es decir, una configuración para niños / padres y hermanos usan el mismo canal para comunicarse. En mi caso, uso la biblioteca de radio radio.uxder.com porque es solo unas pocas líneas de código y es a prueba de balas, pero muchos elegirían el nodo EventEmitter.
Tremendus Apps

Respuestas:

83

Con Vue 2.0, estoy usando el mecanismo eventHub como se muestra en la documentación .

  1. Definir centro de eventos centralizado.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
    
  2. Ahora en tu componente puedes emitir eventos con

    this.eventHub.$emit('update', data)
  3. Y escucharte lo haces

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

Actualización Consulte la respuesta de @alex , que describe una solución más sencilla.

kakoni
fuente
3
Solo un aviso: esté atento a los Global Mixins e intente evitarlos siempre que sea posible, ya que según este enlace vuejs.org/v2/guide/mixins.html#Global-Mixin pueden afectar incluso a componentes de terceros.
Vini.g.fer
6
Una solución mucho más simple es usar lo que @Alex describió - this.$root.$emit()ythis.$root.$on()
Webnet
5
Para referencia futura, no actualice su respuesta con la respuesta de otra persona (incluso si cree que es mejor y la hace referencia). Vínculo a la respuesta alternativa, o incluso pida al OP que acepte la otra si cree que debería hacerlo, pero copiar su respuesta en la suya es de mala forma y desalienta a los usuarios a dar crédito cuando es debido, ya que simplemente pueden votar a favor solo su responder solamente. Anímelos a navegar hasta (y por lo tanto votar a favor) la respuesta a la que hace referencia al no incluir esa respuesta en la suya.
GrayedFox
4
Gracias por los valiosos comentarios @GrayedFox, actualicé mi respuesta en consecuencia.
kakoni
2
Tenga en cuenta que esta solución ya no será compatible con Vue 3. Consulte stackoverflow.com/a/60895076/752916
AlexMA
145

Incluso puede acortarlo y usar la Vue instancia raíz como Event Hub global:

Componente 1:

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

Componente 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
fuente
2
Esto funciona mejor que definir un centro de eventos adicional y adjuntarlo a cualquier consumidor de eventos.
schad
2
Soy un gran admirador de esta solución, ya que realmente no me gusta que los eventos tengan alcance. Sin embargo, no uso VueJS todos los días, así que tengo curiosidad por saber si hay alguien que vea problemas con este enfoque.
Webnet
2
La solución más simple de todas las respuestas
Vikash Gupta
1
agradable, breve y fácil de implementar, fácil de entender también
nada
1
Si solo desea una comunicación exclusivamente directa entre hermanos, use $ parent en lugar de $ root
Malkev
47

Tipos de comunicación

Al diseñar una aplicación Vue (o de hecho, cualquier aplicación basada en componentes), existen diferentes tipos de comunicación que dependen de las preocupaciones que estemos tratando y tienen sus propios canales de comunicación.

Lógica empresarial: se refiere a todo lo específico de su aplicación y su objetivo.

Lógica de presentación: cualquier cosa con la que interactúe el usuario o que resulte de la interacción del usuario.

Estas dos preocupaciones están relacionadas con estos tipos de comunicación:

  • Estado de la aplicación
  • Padre-hijo
  • Hijo-padre
  • Hermanos

Cada tipo debe utilizar el canal de comunicación correcto.


Canales de comunicación

Un canal es un término vago que usaré para referirme a implementaciones concretas para intercambiar datos en torno a una aplicación Vue.

Accesorios: lógica de presentación de padres e hijos

El canal de comunicación más simple de Vue para la comunicación directa entre padres e hijos . Debería utilizarse principalmente para pasar datos relacionados con la lógica de presentación o un conjunto restringido de datos hacia abajo en la jerarquía.

Refs y métodos: Presentación anti-patrón

Cuando no tiene sentido usar un accesorio para permitir que un niño maneje un evento de un padre, configurar un refcomponente secundario y llamar a sus métodos está bien.

No hagas eso, es un anti-patrón. Reconsidere la arquitectura de sus componentes y el flujo de datos. Si desea llamar a un método en un componente secundario de un padre, probablemente sea hora de elevar el estado o considerar las otras formas descritas aquí o en las otras respuestas.

Eventos: lógica de presentación hijo-padre

$emity $on. El canal de comunicación más simple para la comunicación directa entre padres e hijos. Nuevamente, debería usarse para la lógica de presentación.

Bus de eventos

La mayoría de las respuestas ofrecen buenas alternativas para el bus de eventos, que es uno de los canales de comunicación disponibles para componentes distantes, o cualquier otra cosa.

Esto puede resultar útil cuando se pasan accesorios por todas partes, desde muy arriba hacia abajo hasta componentes secundarios profundamente anidados, sin que casi ningún otro componente los necesite en el medio. Úselo con moderación para datos cuidadosamente seleccionados.

Tenga cuidado: la creación posterior de componentes que se unen al bus de eventos se vinculará más de una vez, lo que provocará que se activen varios controladores y se produzcan fugas. Personalmente, nunca sentí la necesidad de un bus de eventos en todas las aplicaciones de una sola página que diseñé en el pasado.

A continuación se demuestra cómo un simple error conduce a una fuga en la que el Itemcomponente aún se activa incluso si se elimina del DOM.

Recuerde eliminar los oyentes en el destroyedenlace del ciclo de vida.

Tienda centralizada (lógica empresarial)

Vuex es el camino a seguir con Vue para la gestión estatal . Ofrece mucho más que eventos y está listo para su aplicación a gran escala.

Y ahora preguntas :

[S] ¿debo crear la tienda de vuex para cada comunicación menor?

Realmente brilla cuando:

  • lidiar con la lógica de su negocio,
  • comunicarse con un backend (o cualquier capa de persistencia de datos, como el almacenamiento local)

Por lo tanto, sus componentes realmente pueden enfocarse en las cosas que deben ser, administrar interfaces de usuario.

No significa que no pueda usarlo para la lógica de componentes, pero limitaría esa lógica a un módulo Vuex con espacio de nombres con solo el estado de IU global necesario.

Para evitar lidiar con un gran lío de todo en un estado global, la tienda debe estar separada en varios módulos con espacios de nombres.


Tipos de componentes

Para orquestar todas estas comunicaciones y facilitar la reutilización, debemos pensar en los componentes como dos tipos diferentes.

  • Contenedores específicos de la aplicación
  • Componentes genéricos

Nuevamente, no significa que un componente genérico deba reutilizarse o que el contenedor específico de una aplicación no pueda reutilizarse, pero tienen diferentes responsabilidades.

Contenedores específicos de la aplicación

Estos son solo componentes de Vue simples que envuelven otros componentes de Vue (contenedores genéricos u otros contenedores específicos de la aplicación). Aquí es donde debería ocurrir la comunicación de la tienda Vuex y este contenedor debería comunicarse a través de otros medios más simples como accesorios y oyentes de eventos.

Estos contenedores podrían incluso no tener ningún elemento DOM nativo y dejar que los componentes genéricos se ocupen de las plantillas y las interacciones del usuario.

alcance de alguna manera eventso storesvisibilidad para componentes hermanos

Aquí es donde ocurre el alcance. La mayoría de los componentes no conocen la tienda y este componente debería (en su mayoría) usar un módulo de tienda con espacio de nombres con un conjunto limitado de gettersy actionsaplicado con los ayudantes de enlace Vuex proporcionados .

Componentes genéricos

Estos deben recibir sus datos de accesorios, realizar cambios en sus propios datos locales y emitir eventos simples. La mayoría de las veces, no deberían saber que existe una tienda Vuex.

También podrían denominarse contenedores, ya que su única responsabilidad podría ser enviar a otros componentes de la interfaz de usuario.


Comunicación entre hermanos

Entonces, después de todo esto, ¿cómo deberíamos comunicarnos entre dos componentes hermanos?

Es más fácil de entender con un ejemplo: digamos que tenemos un cuadro de entrada y sus datos deben compartirse en la aplicación (hermanos en diferentes lugares del árbol) y persistir con un backend.

Comenzando con el peor de los casos , nuestro componente mezclaría presentación y lógica comercial .

// 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 estas dos preocupaciones, debemos envolver nuestro componente en un contenedor específico de la aplicación y mantener la lógica de presentación en nuestro componente de entrada genérico.

Nuestro componente de entrada ahora es reutilizable y no conoce el backend ni los hermanos.

// 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>

Nuestro contenedor específico de la aplicación ahora puede ser el puente entre la lógica empresarial y la comunicación de presentación.

// 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>

Dado que las acciones de la tienda Vuex se ocupan de la comunicación de backend, nuestro contenedor aquí no necesita conocer axios y el backend.

Emile Bergeron
fuente
3
Estoy de acuerdo con el comentario acerca de que los métodos son " el mismo acoplamiento que el de usar accesorios "
ghybs
Me gusta esta respuesta. Pero, ¿podría dar más detalles sobre Event Bus y la nota "Ten cuidado"? Tal vez pueda dar algún ejemplo, no entiendo cómo los componentes podrían unirse dos veces.
vandroid
¿Cómo se comunica entre el componente principal y el componente secundario, por ejemplo, la validación de formularios? ¿Donde el componente principal es una página, el secundario es un formulario y el nieto es un elemento de formulario de entrada?
Lord Zed
1
@vandroid Creé un ejemplo simple que muestra una fuga cuando los oyentes no se eliminan correctamente, como todos los ejemplos de este hilo.
Emile Bergeron
@LordZed Realmente depende, pero por lo que entiendo de su situación, parece un problema de diseño. Vue debe usarse principalmente para la lógica de presentación. La validación del formulario debe realizarse en otro lugar, como en la interfaz vanilla JS API, que una acción de Vuex llamaría con los datos del formulario.
Emile Bergeron
10

Bien, podemos comunicarnos entre hermanos a través de los padres mediante v-oneventos.

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

Supongamos que queremos actualizar el Detailscomponente cuando hacemos clic en algún elemento List.


en Parent:

Modelo:

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

Aquí:

  • v-on:select-itemes un evento, que se llamará en el Listcomponente (ver más abajo);
  • setSelectedItemes el Parentmétodo de a para actualizar selectedModel;

JS:

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

En 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"
  },
}
//...

Aquí:

  • this.$emit('select-item', item)enviará el artículo select-itemdirectamente a los padres. Y el padre lo enviará a la Detailsvista.
Sergei Panfilov
fuente
5

Lo que suelo hacer si quiero "piratear" los patrones normales de comunicación en Vue, especialmente ahora que .syncestá obsoleto, es crear un EventEmitter simple que maneje la comunicación entre componentes. De uno de mis últimos proyectos:

import {EventEmitter} from 'events'

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

Con este Transmitterobjeto puede hacer, en cualquier componente:

import Transmitter from './Transmitter'

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

Y para crear un componente de "recepción":

import Transmitter from './Transmitter'

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

Nuevamente, esto es para usos realmente específicos. No base toda su aplicación en este patrón, use algo como Vuex.

Hector lorenzo
fuente
1
Ya lo estoy usando vuex, pero nuevamente, ¿debo crear la tienda de vuex para cada comunicación menor?
Sergei Panfilov
Es difícil para mí decirlo con esta cantidad de información, pero diría que si ya está usando vuexsí, hágalo. Úselo.
Héctor Lorenzo
1
En realidad, no estaría de acuerdo en que necesitemos usar vuex para cada comunicación menor ...
Victor
No, claro que no, todo depende del contexto. En realidad, mi respuesta se aleja de vuex. Por otro lado, descubrí que cuanto más usa vuex y el concepto de objeto de estado central, menos confío en la comunicación entre objetos. Pero sí, de acuerdo, todo depende.
Héctor Lorenzo
3

Cómo manejar la comunicación entre hermanos depende de la situación. Pero primero quiero enfatizar que el enfoque de bus de eventos global está desapareciendo en Vue 3 . Vea este RFC . De ahí por qué decidí escribir una nueva respuesta.

Patrón de antepasado común más bajo (o "LCA")

Para casos simples, recomiendo encarecidamente utilizar el patrón de ancestro común más bajo (también conocido como "datos caídos, eventos arriba"). Este patrón es fácil de leer, implementar, probar y depurar.

En esencia, esto significa que si dos componentes necesitan comunicarse, coloque su estado compartido en el componente más cercano que ambos comparten como antepasados. Pase datos del componente principal al componente secundario a través de accesorios y pase información de un elemento secundario a otro mediante la emisión de un evento (vea un ejemplo de esto en la parte inferior de esta respuesta).

Para un ejemplo artificial, en una aplicación de correo electrónico, si el componente "Para" necesitaba interactuar con el componente "cuerpo del mensaje", el estado de esa interacción podría vivir en su padre (tal vez un componente llamado email-form). Es posible que tenga un accesorio en el email-formllamado addresseepara que el cuerpo del mensaje pueda anteponerse automáticamente Dear {{addressee.name}}al correo electrónico según la dirección de correo electrónico del destinatario.

El LCA se vuelve oneroso si la comunicación tiene que viajar largas distancias con muchos componentes intermediarios. A menudo recomiendo a mis colegas esta excelente publicación de blog . (Ignore el hecho de que sus ejemplos usan Ember; sus ideas son aplicables en muchos marcos de interfaz de usuario).

Patrón de contenedor de datos (p. Ej., Vuex)

Para casos complejos o situaciones en las que la comunicación entre padres e hijos involucraría a demasiados intermediarios, utilice Vuex o una tecnología de contenedor de datos equivalente. Cuando sea apropiado, use módulos con espacio de nombres .

Por ejemplo, podría ser razonable crear un espacio de nombres separado para una colección compleja de componentes con muchas interconexiones, como un componente de calendario con todas las funciones.

Patrón de publicación / suscripción (bus de eventos)

Si el patrón de bus de eventos (o "publicar / suscribirse") es más apropiado para sus necesidades, el equipo central de Vue ahora recomienda usar una biblioteca de terceros como mitt . (Consulte el RFC mencionado en el párrafo 1.)

Divagaciones y código extra

A continuación, se muestra un ejemplo básico de la solución Lowest Common Ancestor para la comunicación entre hermanos, ilustrada mediante el juego whack-a-mole .

Un enfoque ingenuo podría ser pensar, "el topo 1 debería decirle al topo 2 que aparezca después de ser golpeado". Pero Vue desalienta este tipo de enfoque, ya que quiere que pensemos en términos de estructuras de árboles .

Probablemente esto sea algo muy bueno. Una aplicación no trivial donde los nodos se comunican directamente entre sí a través de árboles DOM sería muy difícil de depurar sin algún tipo de sistema de contabilidad (como lo proporciona Vuex). Además de eso, los componentes que usan "datos inactivos, eventos arriba" tienden a exhibir un bajo acoplamiento y una alta capacidad de reutilización, ambos rasgos muy deseables que ayudan a escalar las aplicaciones grandes.

En este ejemplo, cuando un lunar es golpeado, emite un evento. El componente del administrador del juego decide cuál es el nuevo estado de la aplicación y, por lo tanto, el hermano topo sabe qué hacer implícitamente después de que Vue vuelva a renderizar. Es un ejemplo algo trivial del "antepasado común más bajo".

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
fuente