142 votes

Communication entre composants frères et sœurs dans Vue.js 2.0

Vue d'ensemble

Dans Vue.js 2.x, model.sync sera déprécié .

Alors, quelle est la bonne façon de communiquer entre des composants frères et sœurs dans le cadre d'un système de gestion de la qualité ? Vue.js 2.x ?


Contexte

D'après ce que j'ai compris de Vue.js 2.x, la méthode préférée pour la communication entre frères et sœurs est la suivante est d'utiliser un magasin ou un bus d'événements .

Selon Evan (créateur de Vue.js) :

Il est également utile de mentionner que "passer des données entre les composants" est généralement une mauvaise idée, car au final le flux de données devient introuvable et très difficile à déboguer.

Si un élément de données doit être partagé par plusieurs composants, il faut préférer magasins mondiaux ou Vuex .

[ Lien vers la discussion ]

Et :

.once et .sync sont dépréciés. Les accessoires sont maintenant toujours à sens unique vers le bas. Pour produire des effets secondaires dans la portée parent, un composant doit explicitement emit un événement au lieu de s'appuyer sur une liaison implicite.

Donc, Evan suggère en utilisant $emit() et $on() .


Préoccupations

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

  • Chaque store et event a 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 que certains portée events ou stores la visibilité des composants frères et sœurs. (Ou peut-être n'ai-je pas compris l'idée ci-dessus).


Question

Alors, quelle est la bonne façon de communiquer entre des composants frères et sœurs ?

2 votes

$emit combiné avec v-model pour émuler .sync . je pense que vous devriez suivre la voie Vuex.

194voto

Alex Points 7907

Vous pouvez même le rendre plus court et utiliser le Racine Vue en tant que centre d'événements global :

Composante 1 :

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

Composante 2 :

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}

3 votes

Cela fonctionne mieux que de définir un centre d'événements supplémentaire et de le rattacher à n'importe quel consommateur d'événements.

3 votes

Je suis un grand fan de cette solution car je n'aime vraiment pas que les événements aient une portée. Cependant, je n'utilise pas VueJS tous les jours et je suis curieux de savoir si quelqu'un d'autre voit des problèmes avec cette approche.

3 votes

La solution la plus simple de toutes les réponses

95voto

kakoni Points 1316

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

  1. Définir un centre 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 écouter vous faites

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

Mise à jour

Veuillez consulter la réponse d'alex qui décrit une solution plus simple.

3 votes

Juste un petit conseil : gardez un œil sur les Global Mixins, et essayez de les éviter autant que possible, comme l'indique ce lien vuejs.org/v2/guide/mixins.html#Global-Mixin ils peuvent même affecter des composants tiers.

7 votes

Une solution beaucoup plus simple consiste à utiliser ce que @Alex a décrit - this.$root.$emit() et this.$root.$on()

6 votes

Pour l'avenir, veuillez ne pas mettre à jour votre réponse avec la réponse de quelqu'un d'autre (même si vous pensez qu'elle est meilleure et que vous y faites référence). Créez un lien vers l'autre réponse, ou demandez même au PO d'accepter l'autre réponse si vous pensez qu'il devrait le faire - mais copier sa réponse dans la vôtre n'est pas correct et décourage les utilisateurs de donner du crédit là où il est dû, car ils peuvent simplement voter en hausse uniquement pour votre réponse. Encouragez-les à naviguer vers (et donc à upvoter) la réponse à laquelle vous faites référence en n'incluant pas cette réponse dans la vôtre.

52voto

emileb Points 614

Portée de l'État

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 données qui dépendent des préoccupations auxquelles nous avons affaire et chacune a ses propres canaux de communication préférés.

  • État global : peut inclure l'utilisateur connecté, le thème actuel, etc.

  • État local : attributs de formulaire, état des boutons désactivés, etc.

Notez qu'une partie de l'état global peut se retrouver dans l'état local à un moment donné, et qu'il peut être transmis aux composants enfants comme n'importe quel autre état local, soit intégralement, soit dilué en fonction du cas d'utilisation.


Canaux de communication

Un canal est un terme général que j'utiliserai pour désigner les implémentations concrètes permettant d'échanger des données dans une application Vue.

Chaque mise en œuvre concerne un canal de communication spécifique, qui comprend :

  • État global
  • Parent-enfant
  • Enfant-parent
  • Frères et sœurs

Des préoccupations différentes sont liées à des canaux de communication différents.

Props : Direct Parent-Enfant

Le canal de communication le plus simple de Vue pour la liaison de données à sens unique.

Événements : Direct Enfant-Parent

$emit et $on . Le canal de communication le plus simple pour une communication directe entre l'enfant et le parent.

Fournir/Injecter : État local global ou distant

Ajoutée dans Vue 2.2+, et très similaire à l'API contextuelle de React, elle pourrait être utilisée comme un remplacement viable d'un bus d'événements.

À n'importe quel endroit de l'arbre des composants, un composant pourrait fournir certaines données, auxquelles n'importe quel enfant pourrait accéder par l'intermédiaire de l'ordinateur. inject la propriété du composant.

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
  }
})

Cela pourrait être utilisé pour fournir un état global à la racine de l'application, ou un état localisé dans un sous-ensemble de l'arbre.

Magasin centralisé (état global)

Vuex est un modèle de gestion des états + bibliothèque pour les applications Vue.js. Il sert de magasin centralisé pour tous les composants d'une application. l'application, avec des règles garantissant que l'état ne peut être modifié que de de manière prévisible.

Et maintenant vous demandez :

[Dois-je créer un magasin Vuex pour chaque communication mineure ?

Il brille vraiment lorsqu'il s'agit de l'état global, ce qui inclut, mais n'est pas limité à :

  • les données reçues d'un backend,
  • l'état global de l'interface utilisateur comme un thème,
  • toute couche de persistance des données, par exemple la sauvegarde vers un backend ou l'interface avec un stockage local,
  • des messages ou des notifications de toast,
  • etc.

Ainsi, vos composants peuvent vraiment se concentrer sur ce qu'ils sont censés être, à savoir gérer les interfaces utilisateur, tandis que le magasin global peut gérer/utiliser la logique commerciale générale et offrir une API claire par le biais de getters et actions .

Cela ne veut pas dire que vous ne pouvez pas l'utiliser pour la logique du composant, mais personnellement, j'étendrais cette logique à un espace de nom Module Vuex avec seulement l'état global nécessaire de l'interface utilisateur.

Pour éviter d'avoir à gérer un grand désordre de tout ce qui se trouve dans un état global, voir la fonction Structure de l'application recommandations.

Réf. et les méthodes : Cas limites

Malgré l'existence des props et des événements, il est parfois nécessaire d'accéder directement à un composant enfant en JavaScript. besoin d'accéder directement à un composant enfant en JavaScript.

Il s'agit uniquement d'un trappe d'évacuation pour la manipulation directe des enfants - vous devez éviter d'accéder $refs à partir de modèles ou de propriétés calculées.

Si vous utilisez souvent des références et des méthodes enfant, il est sans doute temps d'adopter les mesures suivantes relever l'État ou envisagez les autres moyens décrits ici ou dans les autres réponses.

$parent : Cas limites

Similaire à $root le $parent peut être utilisée pour accéder à l'instance parent à partir d'un enfant. Il peut être tentant d'y recourir comme une alternative paresseuse au passage de données avec une prop.

Dans la plupart des cas, atteindre le parent rend votre application plus difficile à déboguer et à comprendre, en particulier si vous modifiez les données dans le parent. difficile à déboguer et à comprendre, en particulier si vous modifiez des données dans le parent. En regardant ce composant plus tard, il sera très difficile de comprendre d'où vient cette mutation. difficile de comprendre d'où vient cette mutation.

Vous pourriez en fait naviguer dans l'ensemble de l'arborescence en utilisant $parent , $ref ou $root Mais ce serait comme si tout était global et deviendrait probablement un spaghetti impossible à maintenir.

Bus d'événements : État local global/distant

Voir La réponse de @AlexMA pour obtenir des informations actualisées sur le modèle de bus d'événements.

Dans le passé, le modèle consistait à passer des accessoires partout, du haut en bas jusqu'aux composants enfants profondément imbriqués, sans qu'aucun autre composant n'en ait besoin entre les deux. À utiliser avec parcimonie pour des données soigneusement sélectionnées.

Faites 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 d'événements dans toutes les applications à page unique que j'ai conçues dans le passé.

L'exemple suivant démontre comment une simple erreur conduit à une fuite où le Item se déclenche toujours, même s'il est retiré du DOM.

// A component that binds to a custom 'update' event.
var Item = {
  template: `<li>{{text}}</li>`,
  props: {
    text: Number
  },
  mounted() {
    this.$root.$on('update', () => {
      console.log(this.text, 'is still alive');
    });
  },
};

// Component that emits events
var List = new Vue({
  el: '#app',
  components: {
    Item
  },
  data: {
    items: [1, 2, 3, 4]
  },
  updated() {
    this.$root.$emit('update');
  },
  methods: {
    onRemove() {
      console.log('slice');
      this.items = this.items.slice(0, -1);
    }
  }
});

<script src="https://unpkg.com/vue@2.5.17/dist/vue.min.js"></script>

<div id="app">
  <button type="button" @click="onRemove">Remove</button>
  <ul>
    <item v-for="item in items" :key="item" :text="item"></item>
  </ul>
</div>

N'oubliez pas de supprimer les écouteurs dans le destroyed le crochet du cycle de vie.


Types de composants

_Avis de non-responsabilité : les éléments suivants "Composants "conteneurs" ou "présentationnels n'est qu'un moyen parmi d'autres de structurer un projet et il existe désormais de multiples alternatives, comme la nouvelle Composition API qui pourrait effectivement remplacer les "conteneurs d'applications spécifiques" que je décris ci-dessous._

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

  • Conteneurs spécifiques aux applications
  • Composants génériques/présentationnels

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 aux applications

Note : voir le nouveau Composition API comme alternative à ces conteneurs.

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

Ces conteneurs pourraient même ne comporter aucun élément DOM natif et laisser les composants génériques s'occuper de la mise en page et des interactions avec l'utilisateur.

portée en quelque sorte events ou stores visibilité des composants frères et sœurs

C'est là que se fait 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 nom et un ensemble limité de fonctions getters et actions appliqué avec les Aides à la liaison Vuex .

Composants génériques/présentationnels

Ils doivent recevoir leurs données des props, effectuer des changements sur leurs propres données locales et émettre des événements simples. La plupart du temps, ils ne devraient même pas savoir qu'un magasin Vuex existe.

On pourrait aussi les appeler des conteneurs, car leur seule responsabilité pourrait être de distribuer des informations à d'autres composants de l'interface utilisateur.


Communication entre frères et sœurs

Donc, après tout cela, comment communiquer entre deux composants frères et sœurs ?

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

Problèmes de mélange

En commençant par le scénario le plus défavorable notre composant mélangerait présentation et entreprise logique.

// 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
                    });
            }
        }
    }
</script>

Bien qu'elle puisse sembler parfaite pour une application simple, elle présente de nombreux inconvénients :

  • Utilise explicitement l'instance globale axios
  • API codée en dur dans l'interface utilisateur
  • Étroitement couplé au composant racine (modèle de bus d'événements)
  • Plus difficile de faire des tests unitaires

Séparation des préoccupations

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.

Avec le modèle suivant, on peut :

  • Facilement tester chaque préoccupation avec des tests unitaires
  • Modifier l'API sans aucun impact sur les composants
  • Configurer les communications HTTP comme vous le souhaitez (axios, fetch, ajout de middlewares, tests, etc.).
  • Réutiliser le entrée composant n'importe où (couplage réduit)
  • Réagir aux changements d'état à partir de n'importe quel endroit de l'application grâce aux liaisons avec le magasin global.
  • etc.

Notre composant d'entrée est maintenant réutilisable et ne connaît pas 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 maintenant servir de pont entre la logique métier et la communication de la 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.saveState,
        ])
    }
</script>

Puisque le magasin Vuex actions s'occupe de la communication avec le backend, notre conteneur n'a pas besoin de connaître axios et le backend.

3 votes

Je suis d'accord avec le commentaire sur les méthodes qui sont " le même couplage que celui de l'utilisation des props "

0 votes

J'aime cette réponse. Mais pourriez-vous élaborer sur l'Event Bus et la note "Attention :"? Vous pouvez peut-être donner un exemple, je ne comprends pas comment les composants peuvent être liés deux fois.

0 votes

Comment communiquer entre un composant parent et un composant petit enfant, par exemple pour la validation d'un formulaire. Le composant parent est une page, l'enfant est un formulaire et le petit enfant est un élément de formulaire de saisie.

10voto

Sergey Panfilov Points 677

Ok, on peut communiquer entre frères et sœurs via le parent en utilisant v-on événements.

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

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


Sur Parent :

Modèle :

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

Ici :

  • v-on:select-item c'est un événement, qui sera appelé en List (voir ci-dessous) ;
  • setSelectedItem c'est un Parent pour mettre à jour selectedModel ;

JavaScript :

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

Sur List :

Modèle :

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

JavaScript :

//...
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 un article via select-item directement dans le parent. Et le parent l'enverra au Details vue.

6voto

Hector Lorenzo Points 773

Ce que je fais habituellement si je veux "pirater" les modèles normaux de communication dans Vue.js, surtout maintenant que .sync est déprécié, est de créer un simple EventEmitter qui gère la communication entre les composants. D'après un de mes derniers projets :

import {EventEmitter} from 'events'

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

Avec cette Transmitter que vous pouvez ensuite faire, dans n'importe quel composant :

import Transmitter from './Transmitter'

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

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

import Transmitter from './Transmitter'

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

Là encore, il s'agit d'utilisations très spécifiques. Ne basez pas toute votre application sur ce modèle, utilisez quelque chose comme Vuex à la place.

1 votes

Je suis déjà en train d'utiliser vuex mais encore une fois, dois-je créer un magasin Vuex pour chaque communication mineure ?

0 votes

C'est difficile pour moi de dire avec cette quantité d'informations, mais je dirais que si vous utilisez déjà vuex oui, allez-y. Utilisez-le.

2 votes

En fait, je ne suis pas d'accord pour dire que nous devons utiliser Vuex pour chaque communication mineure...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X