Comunicazione tra componenti di pari livello in VueJs 2.0


113

Panoramica

In Vue.js 2.x, model.syncsarà deprecato .

Allora, qual è un modo corretto per comunicare tra i componenti di pari livello in Vue.js 2.x ?


sfondo

Da quanto ho capito Vue 2.x, il metodo preferito per la comunicazione tra fratelli è utilizzare un negozio o un bus di eventi .

Secondo Evan (creatore di Vue):

Vale anche la pena ricordare che "il passaggio di dati tra i componenti" è generalmente una cattiva idea, perché alla fine il flusso di dati diventa non tracciabile e molto difficile da eseguire il debug.

Se un dato deve essere condiviso da più componenti, preferisci gli archivi globali o Vuex .

[ Link alla discussione ]

E:

.oncee .syncsono deprecati. Gli oggetti di scena ora sono sempre unidirezionali verso il basso. Per produrre effetti collaterali nell'ambito padre, un componente deve esplicitamente emitun evento invece di fare affidamento sull'associazione implicita.

Quindi, Evan suggerisce di usare $emit()e $on().


preoccupazioni

Quello che mi preoccupa è:

  • Ciascuno storee eventha una visibilità globale (correggetemi se sbaglio);
  • È troppo dispendioso creare un nuovo negozio per ogni comunicazione minore;

Quello che voglio è un certo ambito events o storesvisibilità per i componenti dei fratelli. (O forse non ho capito l'idea di cui sopra.)


Domanda

Allora, qual è il modo corretto di comunicare tra i componenti di pari livello?


2
$emitcombinato con v-modelemulare .sync. Penso che dovresti seguire la via
Vuex

3
Quindi ho considerato la stessa preoccupazione. La mia soluzione è utilizzare un emettitore di eventi con un canale di trasmissione equivalente a "ambito", ovvero una configurazione figlio / genitore e fratello utilizza lo stesso canale per comunicare. Nel mio caso, utilizzo la libreria radio radio.uxder.com perché sono solo poche righe di codice ed è a prova di proiettile, ma molti sceglierebbero il nodo EventEmitter.
Tremendus Apps

Risposte:


84

Con Vue 2.0, sto utilizzando il meccanismo eventHub come dimostrato nella documentazione .

  1. Definisci hub eventi centralizzato.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Ora nel tuo componente puoi emettere eventi con

    this.eventHub.$emit('update', data)
  3. E per ascoltare lo fai

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

Aggiorna Si prega di vedere la risposta di @alex , che descrive una soluzione più semplice.


3
Solo un avvertimento: tieni d'occhio Global Mixin e cerca di evitarli quando possibile, poiché secondo questo link vuejs.org/v2/guide/mixins.html#Global-Mixin possono influenzare anche componenti di terze parti.
Vini.g.fer

6
Una soluzione molto più semplice è usare ciò che @Alex ha descritto - this.$root.$emit()ethis.$root.$on()
Webnet

5
Per riferimento futuro, non aggiornare la tua risposta con la risposta di qualcun altro (anche se pensi che sia migliore e fai riferimento ad essa). Link alla risposta alternativa, o anche chiedere all'OP di accettare l'altra se pensi che dovrebbe, ma copiare la loro risposta nella tua è una cattiva forma e scoraggia gli utenti dal dare credito dove è dovuto, in quanto potrebbero semplicemente votare solo il tuo rispondi solo. Incoraggiali a navigare (e quindi a votare a favore) la risposta a cui fai riferimento non includendola nella tua.
GrayedFox

4
Grazie per il prezioso feedback @GrayedFox, ha aggiornato di conseguenza la mia risposta.
kakoni

2
Si prega di notare che questa soluzione non sarà più supportato in Vue 3. Vedere stackoverflow.com/a/60895076/752916
AlexMA

146

Puoi persino renderlo più breve e utilizzare l' istanza di root Vue come Hub eventi globale:

Componente 1:

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

Componente 2:

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

2
Funziona meglio che definire un hub eventi aggiuntivo e collegarlo a qualsiasi consumatore di eventi.
schad

2
Sono un grande fan di questa soluzione perché non mi piacciono gli eventi che hanno uno scopo. Tuttavia, non uso VueJS tutti i giorni, quindi sono curioso di sapere se c'è qualcuno là fuori che vede problemi con questo approccio.
Webnet

2
Soluzione più semplice di tutte le risposte
Vikash Gupta

1
bello, breve e facile da implementare, anche facile da capire
nada

1
Se vuoi solo comunicazioni dirette esclusivamente tra fratelli, usa $ parent invece di $ root
Malkev

47

Tipi di comunicazione

Quando si progetta un'applicazione Vue (o in effetti, qualsiasi applicazione basata su componenti), ci sono diversi tipi di comunicazione che dipendono da quali problemi abbiamo a che fare e hanno i propri canali di comunicazione.

Logica aziendale: si riferisce a tutto ciò che è specifico della tua app e del suo obiettivo.

Logica di presentazione: tutto ciò con cui l'utente interagisce o che risulta dall'interazione dell'utente.

Queste due preoccupazioni sono legate a questi tipi di comunicazione:

  • Stato dell'applicazione
  • Genitore-figlio
  • Bambino-genitore
  • fratelli

Ogni tipo dovrebbe utilizzare il giusto canale di comunicazione.


Canali di comunicazione

Un canale è un termine generico che userò per fare riferimento a implementazioni concrete per lo scambio di dati su un'app Vue.

Props: logica di presentazione genitore-figlio

Il canale di comunicazione più semplice in Vue per genitori-figli diretti comunicazione . Dovrebbe essere utilizzato principalmente per passare i dati relativi alla logica di presentazione o un insieme limitato di dati lungo la gerarchia.

Rif. E metodi: presentazione anti-pattern

Quando non ha senso usare un oggetto di scena per consentire a un bambino di gestire un evento da un genitore, impostare un refsul componente figlio e chiamare i suoi metodi va bene.

Non farlo, è un anti-pattern. Ripensa l'architettura dei componenti e il flusso di dati. Se ti ritrovi a voler chiamare un metodo su un componente figlio da un genitore, è probabilmente il momento di sollevare lo stato o considerare gli altri modi descritti qui o nelle altre risposte.

Eventi: logica di presentazione figlio-genitore

$emit e $on . Il canale di comunicazione più semplice per la comunicazione diretta bambino-genitore. Anche in questo caso, dovrebbe essere utilizzato per la logica di presentazione.

Bus per eventi

La maggior parte delle risposte fornisce buone alternative per il bus di eventi, che è uno dei canali di comunicazione disponibili per componenti distanti o qualsiasi altra cosa.

Questo può diventare utile quando si passano oggetti di scena dappertutto da componenti figli molto più in alto fino a componenti figli profondamente annidati, con quasi nessun altro componente che ne ha bisogno nel mezzo. Utilizzare con parsimonia per dati accuratamente selezionati.

Stai attento: creazione successiva di componenti che si legano al bus degli eventi verrà associata più di una volta, causando l'attivazione di più gestori e perdite. Personalmente non ho mai sentito il bisogno di un bus di eventi in tutte le app a pagina singola che ho progettato in passato.

Quanto segue dimostra come un semplice errore porti a una perdita in cui il Itemcomponente si attiva ancora anche se rimosso dal DOM.

Ricordarsi di rimuovere i listener destroyednell'hook del ciclo di vita.

Negozio centralizzato (logica aziendale)

Vuex è la strada da percorrere con Vue per la gestione dello stato . Offre molto di più che semplici eventi ed è pronto per l'applicazione su vasta scala.

E ora chiedi :

[S] dovrei creare il negozio di vuex per ogni comunicazione minore?

Brilla davvero quando:

  • affrontare la tua logica aziendale,
  • comunicare con un backend (o qualsiasi livello di persistenza dei dati, come l'archiviazione locale)

Quindi i tuoi componenti possono davvero concentrarsi sulle cose che dovrebbero essere, gestendo le interfacce utente.

Ciò non significa che non sia possibile utilizzarlo per la logica dei componenti, ma definirei tale logica in un modulo Vuex con spazio dei nomi con solo lo stato dell'interfaccia utente globale necessario.

Per evitare di dover gestire un grosso casino di tutto in uno stato globale, il negozio dovrebbe essere separato in più moduli con spazio dei nomi.


Tipi di componenti

Per orchestrare tutte queste comunicazioni e facilitare il riutilizzo, dovremmo pensare ai componenti come a due tipi diversi.

  • Contenitori specifici dell'app
  • Componenti generici

Ancora una volta, ciò non significa che un componente generico debba essere riutilizzato o che un contenitore specifico dell'app non possa essere riutilizzato, ma hanno responsabilità diverse.

Contenitori specifici dell'app

Questi sono solo semplici componenti Vue che avvolgono altri componenti Vue (contenitori generici o altri contenitori specifici dell'app). È qui che dovrebbe avvenire la comunicazione del negozio Vuex e questo contenitore dovrebbe comunicare attraverso altri mezzi più semplici come oggetti di scena e ascoltatori di eventi.

Questi contenitori potrebbero anche non avere alcun elemento DOM nativo e lasciare che i componenti generici gestiscano i modelli e le interazioni degli utenti.

ambito in qualche modo eventso storesvisibilità per i componenti dei fratelli

È qui che avviene lo scoping. La maggior parte dei componenti non conosce il negozio e questo componente dovrebbe (principalmente) utilizzare un modulo di archivio con spazio dei nomi con un set limitato di getterse actionsapplicato con gli helper di binding Vuex forniti .

Componenti generici

Questi dovrebbero ricevere i dati dagli oggetti di scena, apportare modifiche ai propri dati locali ed emettere eventi semplici. Il più delle volte, non dovrebbero sapere che esiste un negozio Vuex.

Potrebbero anche essere chiamati contenitori poiché la loro unica responsabilità potrebbe essere l'invio ad altri componenti dell'interfaccia utente.


Comunicazione tra fratelli

Quindi, dopo tutto questo, come dovremmo comunicare tra due componenti fratelli?

È più facile da capire con un esempio: supponiamo di avere una casella di input e i suoi dati dovrebbero essere condivisi attraverso l'app (fratelli in punti diversi dell'albero) e persistenti con un backend.

Partendo dallo scenario peggiore , il nostro componente mescolerebbe presentazione e logica aziendale .

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

Per separare queste due preoccupazioni, dovremmo racchiudere il nostro componente in un contenitore specifico dell'app e mantenere la logica di presentazione nel nostro componente di input generico.

Il nostro componente di input è ora riutilizzabile e non conosce il backend né i fratelli.

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

Il nostro contenitore specifico per app può ora essere il ponte tra la logica aziendale e la comunicazione di presentazione.

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

Poiché le azioni del negozio Vuex riguardano la comunicazione back-end, il nostro contenitore qui non ha bisogno di conoscere axios e backend.


3
D'accordo con il commento sui metodi che sono " lo stesso accoppiamento di quello dell'uso degli oggetti di scena "
ghybs

Mi piace questa risposta. Ma potresti per favore approfondire Event Bus e "Attenzione:" nota? Forse puoi fare qualche esempio, non capisco come i componenti possano legarsi due volte.
vandroid

Come comunichi tra il componente padre e il componente nipote, ad esempio la convalida del modulo. Dove il componente principale è una pagina, il figlio è il modulo e il nipote è l'elemento del modulo di input?
Lord Zed,

1
@vandroid Ho creato un semplice esempio che mostra una perdita quando gli ascoltatori non vengono rimossi correttamente, come tutti gli esempi in questo thread.
Emile Bergeron

@LordZed Dipende davvero, ma dalla mia comprensione della tua situazione, sembra un problema di progettazione. Vue dovrebbe essere usato principalmente per la logica di presentazione. La convalida del modulo dovrebbe essere eseguita altrove, come nell'interfaccia API JS vanilla, che un'azione Vuex chiamerebbe con i dati dal modulo.
Emile Bergeron

10

Ok, possiamo comunicare tra fratelli tramite i genitori utilizzando gli v-oneventi.

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

Supponiamo di voler aggiornare il Detailscomponente quando facciamo clic su un elemento in List.


in Parent:

Modello:

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

Qui:

  • v-on:select-itemè un evento, che verrà chiamato in Listcomponent (vedi sotto);
  • setSelectedItemè un Parentmetodo di aggiornamento selectedModel;

JS:

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

In List:

Modello:

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

Qui:

  • this.$emit('select-item', item)invierà l'elemento tramite select-itemdirettamente nel genitore. E il genitore lo invierà alla Detailsvista

5

Quello che di solito faccio se voglio "hackerare" i normali schemi di comunicazione in Vue, specialmente ora che .syncè deprecato, è creare un semplice EventEmitter che gestisce la comunicazione tra i componenti. Da uno dei miei ultimi progetti:

import {EventEmitter} from 'events'

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

Con questo Transmitteroggetto puoi quindi fare, in qualsiasi componente:

import Transmitter from './Transmitter'

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

E per creare un componente "ricevente":

import Transmitter from './Transmitter'

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

Ancora una volta, questo è per usi davvero specifici. Non basare l'intera applicazione su questo modello, usa Vuexinvece qualcosa di simile .


1
Lo sto già utilizzando vuex, ma ancora una volta, devo creare il negozio di vuex per ogni comunicazione minore?
Sergei Panfilov

È difficile per me dirlo con questa quantità di informazioni, ma direi che se stai già utilizzando vuexsì, fallo. Usalo.
Hector Lorenzo

1
In realtà non sarei d'accordo sul fatto che dobbiamo usare vuex per ogni comunicazione minore ...
Victor

No, certo che no, dipende tutto dal contesto. In realtà la mia risposta si allontana da vuex. D'altra parte, ho scoperto che più usi vuex e il concetto di un oggetto di stato centrale, meno mi affido alla comunicazione tra gli oggetti. Ma sì, d'accordo, dipende tutto.
Hector Lorenzo

3

Il modo in cui gestire la comunicazione tra fratelli dipende dalla situazione. Ma prima voglio sottolineare che l'approccio del bus di eventi globali sta scomparendo in Vue 3 . Vedi questo RFC . Ecco perché ho deciso di scrivere una nuova risposta.

Modello antenato comune più basso (o "LCA")

Per casi semplici, consiglio vivamente di utilizzare il pattern Lowest Common Ancestor (noto anche come "data down, events up"). Questo modello è facile da leggere, implementare, testare ed eseguire il debug.

In sostanza, questo significa che se due componenti devono comunicare, mettere il loro stato condiviso nel componente più vicino che entrambi condividono come antenato. Passa i dati dal componente padre al componente figlio tramite oggetti di scena e passa le informazioni dal figlio al genitore emettendo un evento (vedi esempio di questo in fondo a questa risposta).

Per un esempio artificioso, in un'app di posta elettronica, se il componente "A" avesse bisogno di interagire con il componente "corpo del messaggio", lo stato di tale interazione potrebbe risiedere nel genitore (forse un componente chiamato email-form). Potresti avere un prop nel email-formchiamato in addresseemodo che il corpo del messaggio possa anteporre automaticamente Dear {{addressee.name}}l'e-mail in base all'indirizzo e-mail del destinatario.

L'LCA diventa oneroso se la comunicazione deve percorrere lunghe distanze con molti componenti di intermediari. Rimando spesso i colleghi a questo eccellente post sul blog . (Ignora il fatto che i suoi esempi utilizzano Ember; le sue idee sono applicabili a molti framework di interfaccia utente.)

Pattern contenitore dati (ad esempio, Vuex)

Per casi o situazioni complesse in cui la comunicazione genitore-figlio coinvolgerebbe troppi intermediari, utilizzare Vuex o una tecnologia di contenitore di dati equivalente. Quando appropriato, usa i moduli con spazio dei nomi .

Ad esempio, potrebbe essere ragionevole creare uno spazio dei nomi separato per una raccolta complessa di componenti con molte interconnessioni, come un componente di calendario completo.

Pattern di pubblicazione / sottoscrizione (bus eventi)

Se il pattern del bus degli eventi (o "pubblica / iscriviti") è più appropriato per le tue esigenze, il team principale di Vue ora consiglia di utilizzare una libreria di terze parti come mitt . (Vedere la RFC a cui si fa riferimento nel paragrafo 1.)

Divagazioni e codice bonus

Ecco un esempio di base della soluzione Lowest Common Ancestor per la comunicazione tra fratelli, illustrata tramite il gioco whack-a-mole .

Un approccio ingenuo potrebbe essere pensare, "la talpa 1 dovrebbe dire alla talpa 2 di apparire dopo che è stata colpita". Ma Vue scoraggia questo tipo di approccio, poiché vuole che pensiamo in termini di strutture ad albero .

Questa è probabilmente una cosa molto buona. Un'app non banale in cui i nodi comunicano direttamente tra loro attraverso gli alberi DOM sarebbe molto difficile da eseguire il debug senza una sorta di sistema di contabilità (come fornisce Vuex). Inoltre, i componenti che utilizzano "dati giù, eventi su" tendono a mostrare un basso accoppiamento e un'elevata riutilizzabilità, entrambi tratti altamente desiderabili che aiutano le applicazioni di grandi dimensioni a scalare.

In questo esempio, quando una talpa viene colpita, emette un evento. Il componente del gestore di gioco decide qual è il nuovo stato dell'app, e quindi la talpa fratello sa cosa fare implicitamente dopo il rendering di Vue. È un esempio piuttosto banale di "antenato comune più basso".

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>

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.