Communication entre les composants frères dans VueJs 2.0

113

Aperçu

Dans Vue.js 2.x, model.syncsera obsolète .

Alors, quelle est la bonne façon de communiquer entre les composants frères dans Vue.js 2.x ?


Contexte

Si je comprends bien Vue 2.x, la méthode préférée pour la communication entre frères et sœurs est d'utiliser un magasin ou un bus d'événements .

Selon Evan (créateur de Vue):

Il convient également de mentionner que «passer des données entre les composants» est généralement une mauvaise idée, car à la fin le flux de données devient impossible à suivre et très difficile à déboguer.

Si une donnée doit être partagée par plusieurs composants, préférez les magasins globaux ou Vuex .

[ Lien vers la discussion ]

Et:

.onceet .syncsont obsolètes. Les accessoires sont désormais toujours à sens unique. Pour produire des effets secondaires dans la portée parent, un composant doit explicitement emitun événement au lieu de s'appuyer sur une liaison implicite.

Ainsi, Evan suggère d' utiliser $emit()et $on().


Préoccupations

Ce qui m'inquiète, c'est:

  • Chaque storeet eventa une visibilité globale (corrigez - moi si je me trompe);
  • Il est trop coûteux de créer un nouveau magasin pour chaque communication mineure;

Ce que je veux, c'est une certaine portée events ou storesvisibilité pour les composants frères. (Ou peut-être que je n'ai pas compris l'idée ci-dessus.)


Question

Alors, quelle est la bonne façon de communiquer entre les composants frères?

Sergueï Panfilov
la source
2
$emitcombiné avec v-modelpour émuler .sync. Je pense que vous devriez suivre la voie
Vuex
3
J'ai donc considéré la même préoccupation. Ma solution consiste à utiliser un émetteur d'événement avec un canal de diffusion équivalent à «scope» - c'est-à-dire qu'une configuration enfant / parent et frère utilise le même canal pour communiquer. Dans mon cas, j'utilise la bibliothèque radio radio.uxder.com car ce n'est que quelques lignes de code et son pare-balles, mais beaucoup choisiraient le nœud EventEmitter.
Tremendus Apps

Réponses:

84

Avec Vue 2.0, j'utilise le mécanisme eventHub comme démontré dans la documentation .

  1. Définissez un hub d'événements centralisé.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Maintenant, dans votre composant, vous pouvez émettre des événements avec

    this.eventHub.$emit('update', data)
  3. Et pour vous écouter

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

Mise à jour Veuillez consulter la réponse de @alex , qui décrit une solution plus simple.

kakoni
la source
3
Attention: gardez un œil sur Global Mixins, et essayez de les éviter autant que possible, car selon ce lien vuejs.org/v2/guide/mixins.html#Global-Mixin, ils peuvent même affecter des composants tiers.
Vini.g.fer
6
Une solution beaucoup plus simple consiste à utiliser ce que @Alex a décrit - this.$root.$emit()etthis.$root.$on()
Webnet
5
Pour référence future, veuillez ne pas mettre à jour votre réponse avec la réponse de quelqu'un d'autre (même si vous pensez que c'est mieux et que vous la référencez). Faites un lien vers la réponse alternative, ou demandez même au PO d'accepter l'autre si vous pensez qu'il devrait - mais copier sa réponse dans la vôtre est une mauvaise forme et décourage les utilisateurs de donner du crédit là où il est dû, car ils peuvent simplement voter pour vous. réponse seulement. Encouragez-les à naviguer vers (et donc à voter pour) la réponse à laquelle vous faites référence en n'incluant pas cette réponse dans la vôtre.
GrayedFox
4
Merci pour les précieux commentaires @GrayedFox, a mis à jour ma réponse en conséquence.
kakoni le
2
Veuillez noter que cette solution ne sera plus prise en charge dans Vue 3. Voir stackoverflow.com/a/60895076/752916
AlexMA
146

Vous pouvez même le raccourcir et utiliser l' Vue instance racine comme hub d'événements global:

Composant 1:

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

Composant 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
la source
2
Cela fonctionne mieux que de définir un hub d'événements d'ajout et de l'attacher à n'importe quel consommateur d'événements.
schad le
2
Je suis un grand fan de cette solution car je n'aime vraiment pas que les événements aient une portée. Cependant, je ne suis pas avec VueJS tous les jours, donc je suis curieux de savoir s'il y a quelqu'un qui voit des problèmes avec cette approche.
Webnet
2
La solution la plus simple de toutes les réponses
Vikash Gupta
1
agréable, court et facile à mettre en œuvre, facile à comprendre aussi
nada
1
Si vous ne voulez que la communication directe avec les frères et sœurs, utilisez $ parent au lieu de $ root
Malkev
47

Types de communication

Lors de la conception d'une application Vue (ou en fait de toute application basée sur des composants), il existe différents types de communication qui dépendent des préoccupations auxquelles nous sommes confrontés et ils ont leurs propres canaux de communication.

Logique métier: fait référence à tout ce qui est spécifique à votre application et à son objectif.

Logique de présentation: tout ce avec quoi l'utilisateur interagit ou qui résulte de l'interaction de l'utilisateur.

Ces deux préoccupations sont liées à ces types de communication:

  • État de l'application
  • Parent-enfant
  • Enfant-parent
  • Fratrie

Chaque type doit utiliser le bon canal de communication.


Canaux de communication

Un canal est un terme vague que j'utiliserai pour faire référence à des implémentations concrètes pour échanger des données autour d'une application Vue.

Accessoires: logique de présentation parent-enfant

Le canal de communication le plus simple de Vue pour une communication directe parent-enfant . Il doit principalement être utilisé pour transmettre des données relatives à la logique de présentation ou à un ensemble restreint de données dans la hiérarchie.

Réfs et méthodes: Présentation anti-motif

Quand cela n'a pas de sens d'utiliser un accessoire pour laisser un enfant gérer un événement d'un parent, configurer un refsur le composant enfant et appeler ses méthodes est très bien.

Ne faites pas ça, c'est un anti-modèle. Repensez l'architecture de vos composants et le flux de données. Si vous souhaitez appeler une méthode sur un composant enfant d'un parent, il est probablement temps de lever l'état ou d'envisager les autres méthodes décrites ici ou dans les autres réponses.

Evénements: logique de présentation enfant-parent

$emitet $on. Le canal de communication le plus simple pour une communication directe enfant-parent. Encore une fois, devrait être utilisé pour la logique de présentation.

Bus événementiel

La plupart des réponses offrent de bonnes alternatives pour le bus d'événements, qui est l'un des canaux de communication disponibles pour les composants distants, ou quoi que ce soit en fait.

Cela peut devenir utile lorsque vous passez des accessoires partout, de loin vers le bas, à des composants enfants profondément imbriqués, presque aucun autre composant n'en ayant besoin entre les deux. Utilisez avec parcimonie pour des données soigneusement sélectionnées.

Attention: la création ultérieure de composants qui se lient au bus d'événements sera liée plus d'une fois, ce qui entraînera le déclenchement de plusieurs gestionnaires et des fuites. Personnellement, je n'ai jamais ressenti le besoin d'un bus événementiel dans toutes les applications à page unique que j'ai conçues dans le passé.

Ce qui suit montre comment une simple erreur conduit à une fuite où le Itemcomposant se déclenche toujours même s'il est supprimé du DOM.

N'oubliez pas de supprimer les auditeurs dans le destroyed hook de cycle de vie.

Magasin centralisé (logique métier)

Vuex est la voie à suivre avec Vue pour la gestion de l'état . Il offre bien plus que de simples événements et il est prêt pour une application à grande échelle.

Et maintenant vous demandez :

[P] Dois-je créer le magasin de vuex pour chaque communication mineure?

Ça brille vraiment quand:

  • gérer votre logique métier,
  • communiquer avec un backend (ou toute couche de persistance des données, comme le stockage local)

Ainsi, vos composants peuvent vraiment se concentrer sur ce qu'ils sont censés être, en gérant les interfaces utilisateur.

Cela ne signifie pas que vous ne pouvez pas l'utiliser pour la logique des composants, mais j'appliquerais cette logique à un module Vuex à espace de noms avec uniquement l'état d'interface utilisateur global nécessaire.

Pour éviter de gérer un gros désordre de tout dans un état global, le magasin doit être séparé en plusieurs modules d'espacement de noms.


Types de composants

Pour orchestrer toutes ces communications et pour faciliter la réutilisation, nous devons considérer les composants comme deux types différents.

  • Conteneurs spécifiques à l'application
  • Composants génériques

Encore une fois, cela ne signifie pas qu'un composant générique doit être réutilisé ou qu'un conteneur spécifique à une application ne peut pas être réutilisé, mais ils ont des responsabilités différentes.

Conteneurs spécifiques à l'application

Ce ne sont que de simples composants Vue qui enveloppent d'autres composants Vue (conteneurs génériques ou spécifiques à d'autres applications). C'est là que la communication du magasin Vuex doit avoir lieu et ce conteneur doit communiquer par d'autres moyens plus simples comme les accessoires et les écouteurs d'événements.

Ces conteneurs peuvent même ne contenir aucun élément DOM natif et laisser les composants génériques gérer la création de modèles et les interactions des utilisateurs.

portée en quelque sorte eventsou storesvisibilité pour les composants frères et sœurs

C'est là que se déroule le cadrage. La plupart des composants ne connaissent pas le magasin et ce composant devrait (principalement) utiliser un module de magasin avec un espace de noms limité getterset actionsappliqué avec le assistants de liaison Vuex .

Composants génériques

Ceux-ci devraient recevoir leurs données des accessoires, apporter des modifications à leurs propres données locales et émettre des événements simples. La plupart du temps, ils ne devraient pas savoir du tout qu'un magasin Vuex existe.

Ils pourraient également être appelés conteneurs, car leur seule responsabilité pourrait être de les envoyer à d'autres composants de l'interface utilisateur.


Communication fraternelle

Alors, après tout cela, comment devrions-nous communiquer entre deux composants frères?

C'est plus facile à comprendre avec un exemple: disons que nous avons une zone de saisie et que ses données doivent être partagées dans l'application (frères et sœurs à différents endroits de l'arborescence) et persistantes avec un backend.

En commençant par le pire des cas , notre composant mélangerait présentation et logique métier .

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

Pour séparer ces deux préoccupations, nous devons envelopper notre composant dans un conteneur spécifique à l'application et conserver la logique de présentation dans notre composant d'entrée générique.

Notre composant d'entrée est désormais réutilisable et ne connaît ni le backend ni les frères et sœurs.

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

Notre conteneur spécifique à l'application peut désormais servir de pont entre la logique métier et la communication de présentation.

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

Étant donné que les actions du magasin Vuex concernent la communication du backend, notre conteneur n'a pas besoin de connaître axios et le backend.

Émile Bergeron
la source
3
D'accord avec le commentaire sur le fait que les méthodes sont " le même couplage que l'utilisation des accessoires "
ghybs
J'aime cette réponse. Mais pourriez-vous s'il vous plaît élaborer sur Event Bus et "Attention:" note? Peut-être que vous pouvez donner un exemple, je ne comprends pas comment les composants peuvent être liés deux fois.
vandroid le
Comment communiquez-vous entre le composant parent et le composant grand-enfant, par exemple la validation de formulaire. Où le composant parent est une page, l'enfant est un formulaire et grand-enfant est un élément de formulaire d'entrée?
Lord Zed
1
@vandroid J'ai créé un exemple simple qui montre une fuite lorsque les écouteurs ne sont pas supprimés correctement, comme tous les exemples de ce fil.
Emile Bergeron
@LordZed Cela dépend vraiment, mais d'après ma compréhension de votre situation, cela ressemble à un problème de conception. Vue doit être utilisé principalement pour la logique de présentation. La validation du formulaire doit être effectuée ailleurs, comme dans l'interface API JS vanilla, qu'une action Vuex appellerait avec les données du formulaire.
Emile Bergeron
10

D'accord, nous pouvons communiquer entre frères et sœurs via les parents en utilisant des v-onévénements.

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

Supposons que nous voulons mettre à jour le Detailscomposant lorsque nous cliquons sur un élément dans List.


dans Parent:

Modèle:

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

Ici:

  • v-on:select-itemc'est un événement, qui sera appelé en Listcomposant (voir ci-dessous);
  • setSelectedItemc'est une Parentméthode de mise à jour selectedModel;

JS:

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

Dans List :

Modèle:

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

Ici:

  • this.$emit('select-item', item)enverra l'article via select-itemdirectement dans le parent. Et le parent l'enverra à la Detailsvue
Sergueï Panfilov
la source
5

Ce que je fais habituellement si je veux "pirater" les modèles normaux de communication dans Vue, spécialement maintenant qui .syncest obsolète, est de créer un simple EventEmitter qui gère la communication entre les composants. De l'un de mes derniers projets:

import {EventEmitter} from 'events'

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

Avec cet Transmitterobjet, vous pouvez alors faire, dans n'importe quel composant:

import Transmitter from './Transmitter'

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

Et pour créer un composant "récepteur":

import Transmitter from './Transmitter'

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

Encore une fois, c'est pour des utilisations vraiment spécifiques. Ne basez pas toute votre application sur ce modèle, utilisez Vuexplutôt quelque chose comme .

Hector Lorenzo
la source
1
J'utilise déjà vuex, mais encore une fois, dois-je créer le magasin de vuex pour chaque communication mineure?
Sergei Panfilov
C'est difficile pour moi de dire avec cette quantité d'informations, mais je dirais que si vous utilisez déjà vuexoui, allez-y. Utilise le.
Hector Lorenzo
1
En fait, je ne serais pas d'accord sur le fait que nous devons utiliser vuex pour chaque communication mineure ...
Victor
Non, bien sûr que non, tout dépend du contexte. En fait, ma réponse s'éloigne de vuex. D'un autre côté, j'ai constaté que plus vous utilisez vuex et le concept d'objet d'état central, moins je compte sur la communication entre objets. Mais oui, d'accord, tout dépend.
Hector Lorenzo
3

La manière de gérer la communication entre frères et sœurs dépend de la situation. Mais d'abord, je tiens à souligner que l'approche globale du bus d'événements disparaît dans Vue 3 . Voir cette RFC . C'est pourquoi j'ai décidé d'écrire une nouvelle réponse.

Motif ancestral commun le plus bas (ou «LCA»)

Pour les cas simples, je recommande fortement d'utiliser le modèle de l'ancêtre commun le plus bas (également connu sous le nom de «données vers le bas, événements vers le haut»). Ce modèle est facile à lire, implémenter, tester et déboguer.

En substance, cela signifie que si deux composants doivent communiquer, placez leur état partagé dans le composant le plus proche que les deux partagent en tant qu'ancêtre. Passez les données du composant parent au composant enfant via les accessoires et transmettez les informations de l'enfant au parent en émettant un événement (voir l'exemple au bas de cette réponse).

Pour un exemple artificiel, dans une application de messagerie, si le composant "À" devait interagir avec le composant "corps du message", l'état de cette interaction pourrait vivre dans leur parent (peut-être un composant appelé email-form). Vous pourriez avoir un accessoire dans le email-formappelé addresseeafin que le corps du message puisse automatiquement ajouterDear {{addressee.name}} à l'e-mail en fonction de l'adresse e-mail du destinataire.

L'ACV devient onéreuse si la communication doit parcourir de longues distances avec de nombreux composants intermédiaires. Je renvoie souvent mes collègues à cet excellent article de blog . (Ignorez le fait que ses exemples utilisent Ember; ses idées sont applicables dans de nombreux frameworks d'interface utilisateur.)

Modèle de conteneur de données (par exemple, Vuex)

Pour les cas complexes ou les situations où la communication parent-enfant impliquerait trop d'intermédiaires, utilisez Vuex ou une technologie de conteneur de données équivalente. Le cas échéant, utilisez modules d'espacement de noms .

Par exemple, il peut être raisonnable de créer un espace de noms distinct pour une collection complexe de composants avec de nombreuses interconnexions, comme un composant de calendrier complet.

Modèle de publication / abonnement (bus d'événements)

Si le modèle de bus d'événements (ou «publier / souscrire») est plus approprié pour vos besoins, l'équipe de base de Vue recommande maintenant d'utiliser une bibliothèque tierce telle que mitt . (Voir la RFC référencée au paragraphe 1.)

Randonnées et code bonus

Voici un exemple de base de la solution de l'ancêtre commun le plus bas pour la communication entre frères et sœurs, illustrée via le jeu whack-a-mole .

Une approche naïve pourrait être de penser: «la taupe 1 devrait indiquer à la taupe 2 d'apparaître après avoir été frappée». Mais Vue déconseille ce type d'approche, car il veut que nous pensions en termes de structures arborescentes .

C'est probablement une très bonne chose. Une application non triviale où les nœuds communiquent directement entre eux à travers les arbres DOM serait très difficile à déboguer sans une sorte de système de comptabilité (comme le fournit Vuex). En plus de cela, les composants qui utilisent «les données vers le bas, les événements vers le haut» ont tendance à présenter un faible couplage et une réutilisation élevée, deux caractéristiques hautement souhaitables qui aident les grandes applications à évoluer.

Dans cet exemple, lorsqu'une taupe est frappée, elle émet un événement. Le composant du gestionnaire de jeu décide du nouvel état de l'application et, par conséquent, la taupe sœur sait quoi faire implicitement après le nouveau rendu de Vue. C'est un exemple assez trivial de «plus petit ancêtre commun».

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
la source