Komunikacja między komponentami rodzeństwa w VueJs 2.0

113

Przegląd

W Vue.js 2.x model.synczostanie wycofany .

Więc jaki jest właściwy sposób komunikacji między komponentami rodzeństwa w Vue.js 2.x ?


tło

Jak rozumiem Vue 2.x, preferowaną metodą komunikacji z rodzeństwem jest użycie sklepu lub magistrali zdarzeń .

Według Evana (twórcy Vue):

Warto również wspomnieć, że „przekazywanie danych między komponentami” jest generalnie złym pomysłem, ponieważ ostatecznie przepływ danych staje się niemożliwy do śledzenia i bardzo trudny do debugowania.

Jeśli część danych musi być współdzielona przez wiele komponentów, preferuj sklepy globalne lub Vuex .

[ Link do dyskusji ]

I:

.oncei .syncsą przestarzałe. Rekwizyty są teraz zawsze w dół. Aby uzyskać efekty uboczne w zakresie nadrzędnym, składnik musi jawnie jawnie emitzdarzenie zamiast polegać na niejawnym powiązaniu.

Tak więc Evan sugeruje użycie $emit()i $on().


Obawy

Martwi mnie:

  • Każdy storei eventma globalną widoczność (popraw mnie, jeśli się mylę);
  • Tworzenie nowego sklepu dla każdej drobnej komunikacji jest zbyt marnotrawne;

To, czego chcę, to pewien zakres events lub storeswidoczność komponentów rodzeństwa. (A może nie rozumiałem powyższego pomysłu.)


Pytanie

Jaki jest więc prawidłowy sposób komunikacji między komponentami rodzeństwa?

Siergiej Panfiłow
źródło
2
$emitw połączeniu z v-modelemulacją .sync. Myślę, że powinieneś pójść drogą Vuex
eltonkamami
3
Więc rozważyłem ten sam problem. Moim rozwiązaniem jest użycie emitera zdarzeń z kanałem transmisji, który jest równoważny z „zakresem” - tj. Konfiguracja dziecka / rodzica i rodzeństwa używają tego samego kanału do komunikacji. W moim przypadku korzystam z biblioteki radiowej radio.uxder.com, ponieważ jest to tylko kilka wierszy kodu i jest ona kuloodporna, ale wielu wybrałoby węzeł EventEmitter.
Tremendus Apps

Odpowiedzi:

84

W Vue 2.0 używam mechanizmu eventHub, jak pokazano w dokumentacji .

  1. Zdefiniuj scentralizowane centrum zdarzeń.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Teraz w swoim komponencie możesz emitować zdarzenia za pomocą

    this.eventHub.$emit('update', data)
  3. I słuchasz

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

Aktualizacja Zobacz odpowiedź @alex , która opisuje prostsze rozwiązanie.

kakoni
źródło
3
Uwaga: miej oko na Global Mixins i staraj się ich unikać, gdy tylko jest to możliwe, ponieważ zgodnie z tym linkiem vuejs.org/v2/guide/mixins.html#Global-Mixin mogą wpływać nawet na komponenty stron trzecich.
Vini.g.fer
6
O wiele prostszym rozwiązaniem jest użycie tego, co opisał @Alex - this.$root.$emit()ithis.$root.$on()
Webnet.
5
Na przyszłość nie aktualizuj swojej odpowiedzi odpowiedzią innej osoby (nawet jeśli uważasz, że jest lepsza i odwołujesz się do niej). Podaj link do alternatywnej odpowiedzi, a nawet poproś OP, aby zaakceptował drugą, jeśli uważasz, że powinien - ale skopiowanie ich odpowiedzi do własnej jest złą formą i zniechęca użytkowników do udzielania kredytu tam, gdzie jest to należne, ponieważ mogą po prostu zagłosować tylko za tylko odpowiedz. Zachęć ich do przejścia do odpowiedzi, do której się odnosisz, (a tym samym do głosowania za nią), nie włączając tej odpowiedzi do swojej własnej.
GrayedFox
4
Dziękuję za cenne opinie @GrayedFox, odpowiednio zaktualizowałem moją odpowiedź.
kakoni
2
Pamiętaj, że to rozwiązanie nie będzie już obsługiwane w Vue 3. Zobacz stackoverflow.com/a/60895076/752916
AlexMA
146

Możesz nawet go skrócić i używać wystąpienia głównego Vue jako globalnego centrum zdarzeń:

Składnik 1:

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

Składnik 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
źródło
2
Działa to lepiej niż definiowanie centrum zdarzeń dodawania i dołączanie go do dowolnego konsumenta zdarzenia.
schad
2
Jestem wielkim fanem tego rozwiązania, bo nie lubię wydarzeń o zasięgu. Jednak nie robię tego na co dzień z VueJS, więc jestem ciekawy, czy jest ktoś, kto widzi problemy z tym podejściem.
Webnet
2
Najprostsze rozwiązanie wszystkich odpowiedzi
Vikash Gupta
1
ładne, krótkie i łatwe do wdrożenia, łatwe do zrozumienia
nada
1
Jeśli chcesz wyłącznie bezpośrednią komunikację z rodzeństwem, użyj $ parent zamiast $ root
Malkev
47

Typy komunikacji

Projektując aplikację Vue (a właściwie dowolną aplikację opartą na komponentach), istnieją różne typy komunikacji, które zależą od problemów, z którymi mamy do czynienia i mają własne kanały komunikacji.

Logika biznesowa: odnosi się do wszystkiego, co dotyczy Twojej aplikacji i jej celu.

Logika prezentacji: wszystko, z czym użytkownik wchodzi w interakcje lub co wynika z interakcji użytkownika.

Te dwa problemy są związane z tymi rodzajami komunikacji:

  • Stan aplikacji
  • Rodzic-dziecko
  • Dziecko-rodzic
  • Rodzeństwo

Każdy typ powinien korzystać z odpowiedniego kanału komunikacji.


Kanały komunikacji

Kanał to luźny termin, którego będę używał w odniesieniu do konkretnych implementacji służących do wymiany danych wokół aplikacji Vue.

Rekwizyty: logika prezentacji rodzic-dziecko

Najprostszy kanał komunikacji w Vue dla bezpośredniego rodzica-dziecka komunikacji . Powinien być używany głównie do przekazywania danych związanych z logiką prezentacji lub ograniczonym zestawem danych w dół hierarchii.

Odniesienia i metody: Prezentacja anty-wzorca

Kiedy nie ma sensu używanie właściwości pozwalającej dziecku na obsługę zdarzenia rodzica, ustawienie a refna komponencie potomnym i wywołanie jego metod jest w porządku.

Nie rób tego, to anty-wzór. Przemyśl swoją architekturę komponentów i przepływ danych. Jeśli okaże się, że chcesz wywołać metodę na komponencie potomnym od rodzica, prawdopodobnie nadszedł czas, aby podnieść stan lub rozważyć inne sposoby opisane tutaj lub w innych odpowiedziach.

Zdarzenia: logika prezentacji dziecko-rodzic

$emit i $on . Najprostszy kanał komunikacji do bezpośredniej komunikacji dziecko-rodzic. Ponownie powinno być używane do logiki prezentacji.

Autobus imprezowy

Większość odpowiedzi podaje dobre alternatywy dla magistrali zdarzeń, która jest jednym z kanałów komunikacyjnych dostępnych dla odległych komponentów, lub czymkolwiek w rzeczywistości.

Może się to przydać przy przekazywaniu rekwizytów w każdym miejscu z daleka w dół do głęboko zagnieżdżonych komponentów potomnych, prawie żadne inne komponenty nie potrzebują ich pomiędzy. Używaj oszczędnie w przypadku starannie wybranych danych.

Uważaj: późniejsze tworzenie komponentów, które są powiązane z magistralą zdarzeń, będzie wiązane więcej niż raz - co prowadzi do wyzwolenia wielu procedur obsługi i wycieków. Osobiście nigdy nie odczuwałem potrzeby korzystania z magistrali zdarzeń we wszystkich aplikacjach z jedną stroną, które zaprojektowałem w przeszłości.

Poniżej pokazano, jak prosty błąd prowadzi do wycieku, w którym Itemkomponent nadal jest wyzwalany, nawet jeśli zostanie usunięty z DOM.

Pamiętaj, aby usunąć detektory w destroyedhaku cyklu życia.

Scentralizowany sklep (logika biznesowa)

Vuex to sposób na zarządzanie stanem dzięki Vue . Oferuje znacznie więcej niż tylko wydarzenia i jest gotowy do aplikacji na pełną skalę.

A teraz pytasz :

[S] Czy powinienem stworzyć sklep vuex dla każdej drobnej komunikacji?

Naprawdę błyszczy, gdy:

  • radzenie sobie z logiką biznesową,
  • komunikowanie się z zapleczem (lub dowolną warstwą trwałości danych, taką jak pamięć lokalna)

Dzięki temu komponenty mogą naprawdę skupić się na tym, czym mają być, zarządzając interfejsami użytkownika.

Nie oznacza to, że nie możesz go użyć do logiki komponentów, ale chciałbym ograniczyć tę logikę do modułu Vuex z przestrzenią nazw z tylko niezbędnym globalnym stanem interfejsu użytkownika.

Aby uniknąć bałaganu wszystkiego w stanie globalnym, sklep powinien być podzielony na wiele modułów z przestrzenią nazw.


Typy komponentów

Aby zorganizować całą tę komunikację i ułatwić ponowne użycie, powinniśmy traktować komponenty jako dwa różne typy.

  • Kontenery specyficzne dla aplikacji
  • Komponenty ogólne

Ponownie, nie oznacza to, że należy ponownie użyć komponentu ogólnego lub że nie można ponownie użyć kontenera specyficznego dla aplikacji, ale mają inne obowiązki.

Kontenery specyficzne dla aplikacji

To tylko prosty komponent Vue, który otacza inne komponenty Vue (ogólne lub inne kontenery specyficzne dla aplikacji). To tutaj powinna mieć miejsce komunikacja sklepu Vuex, a ten kontener powinien komunikować się za pomocą innych prostszych środków, takich jak rekwizyty i nasłuchiwanie zdarzeń.

Te kontenery mogą nawet nie mieć w ogóle natywnych elementów DOM i pozwolić komponentom generycznym zajmować się tworzeniem szablonów i interakcjami użytkownika.

w jakiś sposób eventslub storeswidoczność komponentów rodzeństwa

W tym miejscu odbywa się określanie zakresu. Większość komponentów nie wie o sklepie, a ten komponent powinien (głównie) używać jednego modułu sklepu z przestrzenią nazw z ograniczonym zestawem gettersi actionszastosowanych za pomocą dostarczonych pomocników powiązań Vuex .

Komponenty ogólne

Powinny one otrzymywać dane z rekwizytów, wprowadzać zmiany we własnych danych lokalnych i emitować proste zdarzenia. W większości przypadków nie powinni wiedzieć, że sklep Vuex w ogóle istnieje.

Można je również nazwać kontenerami, ponieważ ich wyłączną odpowiedzialnością może być wysyłanie do innych składników interfejsu użytkownika.


Komunikacja z rodzeństwem

Jak więc po tym wszystkim powinniśmy komunikować się między dwoma komponentami rodzeństwa?

Łatwiej to zrozumieć na przykładzie: powiedzmy, że mamy pole wprowadzania, a jego dane powinny być udostępniane w całej aplikacji (rodzeństwo w różnych miejscach w drzewie) i utrwalane na zapleczu.

Zaczynając od najgorszego scenariusza , nasz komponent łączyłby prezentację i logikę biznesową .

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

Aby oddzielić te dwa problemy, powinniśmy opakować nasz składnik w kontenerze specyficznym dla aplikacji i zachować logikę prezentacji w naszym ogólnym składniku wejściowym.

Nasz komponent wejściowy jest teraz wielokrotnego użytku i nie wie o zapleczu ani rodzeństwie.

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

Nasz kontener przeznaczony dla aplikacji może teraz pełnić rolę pomostu między logiką biznesową a komunikacją prezentacyjną.

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

Ponieważ przechowywanie Vuex działania czynienia z komunikacji backend, nasz pojemnik tutaj nie musi wiedzieć o Axios i backend.

Emile Bergeron
źródło
3
Zgadzam się z komentarzem, że metody są „ tym samym sprzężeniem, co używanie rekwizytów
ghybs
Podoba mi się ta odpowiedź. Ale czy mógłbyś rozwinąć temat Event Bus i notatkę „Uważaj:”? Może mógłbyś podać jakiś przykład, nie rozumiem, jak komponenty mogą zostać połączone dwukrotnie.
vandroid
Jak komunikujesz się między komponentem nadrzędnym a komponentem grand child, na przykład walidacja formularza. Gdzie komponent nadrzędny to strona, element potomny to formularz, a wnuk to element formularza wejściowego?
Lord Zed,
1
@vandroid Stworzyłem prosty przykład, który pokazuje wyciek, gdy słuchacze nie są prawidłowo usuwani, jak każdy przykład w tym wątku.
Emile Bergeron
@LordZed To naprawdę zależy, ale z mojego zrozumienia twojej sytuacji wygląda na problem projektowy. Vue powinno być używane głównie do logiki prezentacji. Walidację formularza należy przeprowadzić w innym miejscu, np. W interfejsie API vanilla JS, który wywoła akcja Vuex z danymi z formularza.
Emile Bergeron
10

OK, możemy komunikować się między rodzeństwem za pośrednictwem rodziców za pomocą v-onwydarzeń.

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

Załóżmy, że chcemy aktualizować Detailskomponent po kliknięciu jakiegoś elementu List.


w Parent:

Szablon:

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

Tutaj:

  • v-on:select-itemjest to zdarzenie, które zostanie wywołane w Listkomponencie (patrz poniżej);
  • setSelectedItemjest to Parentmetoda aktualizacji selectedModel;

JS:

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

W List:

Szablon:

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

Tutaj:

  • this.$emit('select-item', item)wyśle ​​przedmiot select-itembezpośrednio do rodzica. A rodzic wyśle ​​go do Detailswidoku
Siergiej Panfiłow
źródło
5

To, co zwykle robię, jeśli chcę „zhakować” normalne wzorce komunikacji w Vue, szczególnie teraz, gdy .syncjest przestarzałe, to utworzenie prostego EventEmittera, który obsługuje komunikację między komponentami. Z jednego z moich najnowszych projektów:

import {EventEmitter} from 'events'

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

Za pomocą tego Transmitterobiektu możesz następnie wykonać w dowolnym komponencie:

import Transmitter from './Transmitter'

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

Aby utworzyć komponent „odbierający”:

import Transmitter from './Transmitter'

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

Ponownie, to jest do naprawdę konkretnych zastosowań. Nie opieraj całej aplikacji na tym wzorcu, Vuexzamiast tego użyj czegoś podobnego .

Hector Lorenzo
źródło
1
Już używam vuex, ale znowu, czy powinienem utworzyć sklep vuex dla każdej drobnej komunikacji?
Siergiej Panfiłow
Trudno mi powiedzieć przy takiej ilości informacji, ale powiedziałbym, że jeśli już używasz vuextak, zrób to. Użyj tego.
Hector Lorenzo
1
Właściwie nie zgodziłbym się z tym, że musimy używać vuex do każdej drobnej komunikacji ...
Victor
Nie, oczywiście, że nie, wszystko zależy od kontekstu. Właściwie moja odpowiedź odchodzi od vuex. Z drugiej strony odkryłem, że im częściej używasz vuex i koncepcji obiektu stanu centralnego, tym mniej polegam na komunikacji między obiektami. Ale tak, zgadzam się, wszystko zależy.
Hector Lorenzo,
3

Sposób radzenia sobie z komunikacją między rodzeństwem zależy od sytuacji. Ale najpierw chcę podkreślić, że globalne podejście do magistrali zdarzeń odchodzi w Vue 3 . Zobacz ten dokument RFC . Dlatego zdecydowałem się napisać nową odpowiedź.

Najniższy wspólny wzorzec przodka (lub „LCA”)

W prostych przypadkach zdecydowanie zalecam użycie wzorca Lowest Common Ancestor (znanego również jako „data down, event up”). Ten wzorzec jest łatwy do odczytania, zaimplementowania, przetestowania i debugowania.

W istocie oznacza to, że jeśli dwa komponenty muszą się komunikować, umieść ich wspólny stan w najbliższym komponencie, który ma wspólny przodek. Przekaż dane z komponentu nadrzędnego do komponentu podrzędnego za pomocą rekwizytów i przekaż informacje od dziecka do rodzica, emitując zdarzenie (zobacz przykład na dole odpowiedzi).

W wymyślonym przykładzie, w aplikacji e-mail, jeśli komponent „Do” musiałby współdziałać z komponentem „treść wiadomości”, stan tej interakcji mógłby istnieć w ich rodzicu (może być wywołany komponent email-form). Możesz mieć prop w email-formwywołaniu, addresseeaby treść wiadomości mogła być automatycznie dołączana Dear {{addressee.name}}do wiadomości e-mail na podstawie adresu e-mail odbiorcy.

LCA staje się uciążliwe, jeśli komunikacja musi odbywać się na duże odległości z wieloma komponentami pośredników. Często odsyłam kolegów do tego wspaniałego wpisu na blogu . (Zignoruj ​​fakt, że jego przykłady wykorzystują Ember; jego pomysły mają zastosowanie w wielu strukturach interfejsu użytkownika).

Wzorzec kontenera danych (np. Vuex)

W złożonych przypadkach lub sytuacjach, w których komunikacja między rodzicem a dzieckiem wymagałaby zbyt wielu pośredników, użyj Vuex lub równoważnej technologii kontenera danych. W razie potrzeby użyj modułów w przestrzeni nazw .

Na przykład rozsądne może być utworzenie oddzielnej przestrzeni nazw dla złożonej kolekcji składników z wieloma połączeniami, na przykład w pełni funkcjonalnego składnika kalendarza.

Wzorzec publikowania / subskrypcji (magistrala zdarzeń)

Jeśli wzorzec magistrali zdarzeń (lub wzorca „publikuj / subskrybuj”) jest bardziej odpowiedni dla Twoich potrzeb, główny zespół Vue zaleca teraz korzystanie z biblioteki innej firmy, takiej jak rękawica . (Zobacz dokument RFC, o którym mowa w akapicie 1.)

Bonusowe wędrówki i kod

Oto podstawowy przykład rozwiązania Lowest Common Ancestor do komunikacji między rodzeństwem, zilustrowany za pomocą gry Whack-a-mole .

Naiwnym podejściem może być myślenie, „kret 1 powinien powiedzieć kretowi 2, aby pojawił się po uderzeniu go”. Ale Vue odradza tego rodzaju podejście, ponieważ chce, abyśmy myśleli w kategoriach struktur drzewiastych .

To prawdopodobnie bardzo dobra rzecz. Nietrywialna aplikacja, w której węzły komunikują się bezpośrednio ze sobą między drzewami DOM, byłaby bardzo trudna do debugowania bez jakiegoś systemu księgowego (jak zapewnia Vuex). Co więcej, komponenty wykorzystujące „dane w dół, zdarzenia w górę” mają zwykle niskie sprzężenie i dużą możliwość ponownego użycia - obie bardzo pożądane cechy, które pomagają skalować duże aplikacje.

W tym przykładzie, gdy kret zostanie uderzony, emituje zdarzenie. Komponent menedżera gier decyduje o nowym stanie aplikacji, dzięki czemu kret rodzeństwo wie, co zrobić niejawnie po ponownym renderowaniu Vue. To dość trywialny przykład „najniższego wspólnego przodka”.

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
źródło