Komunikasi antar komponen saudara di VueJs 2.0

113

Gambaran

Di Vue.js 2.x, tidak model.syncakan digunakan lagi .

Jadi, apa cara yang tepat untuk berkomunikasi antar komponen saudara di Vue.js 2.x ?


Latar Belakang

Seperti yang saya pahami Vue 2.x, metode yang disukai untuk komunikasi saudara adalah menggunakan toko atau bus acara .

Menurut Evan (pencipta Vue):

Perlu juga disebutkan bahwa "melewatkan data antar komponen" umumnya merupakan ide yang buruk, karena pada akhirnya aliran data menjadi tidak dapat dilacak dan sangat sulit untuk di-debug.

Jika sepotong data perlu dibagikan oleh banyak komponen, pilih penyimpanan global atau Vuex .

[ Tautan ke diskusi ]

Dan:

.oncedan .synctidak digunakan lagi. Alat peraga sekarang selalu turun satu arah. Untuk menghasilkan efek samping dalam lingkup induk, sebuah komponen perlu secara eksplisit emitsebuah peristiwa daripada mengandalkan pengikatan implisit.

Jadi, Evan menyarankan untuk menggunakan $emit()dan $on().


Kekhawatiran

Yang membuat saya khawatir adalah:

  • Masing store- masing dan eventmemiliki visibilitas global (perbaiki saya jika saya salah);
  • Terlalu boros untuk membuat toko baru untuk setiap komunikasi kecil;

Yang saya inginkan adalah beberapa ruang lingkup events atau storesvisibilitas untuk komponen saudara kandung. (Atau mungkin saya tidak memahami gagasan di atas.)


Pertanyaan

Jadi, bagaimana cara yang benar untuk berkomunikasi antar komponen sibling?

Sergei Panfilov
sumber
2
$emitdikombinasikan dengan v-modelmeniru .sync. saya pikir Anda harus pergi ke jalan
Vuex
3
Jadi saya telah mempertimbangkan perhatian yang sama. Solusi saya adalah menggunakan pemancar acara dengan saluran siaran yang setara dengan 'cakupan' - yaitu pengaturan anak / orang tua dan saudara menggunakan saluran yang sama untuk berkomunikasi. Dalam kasus saya, saya menggunakan perpustakaan radio radio.uxder.com karena itu hanya beberapa baris kode dan antipeluru, tetapi banyak yang akan memilih node EventEmitter.
Aplikasi Tremendus

Jawaban:

84

Dengan Vue 2.0, saya menggunakan mekanisme eventHub seperti yang ditunjukkan dalam dokumentasi .

  1. Tentukan hub acara terpusat.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Sekarang dalam komponen Anda, Anda dapat mengeluarkan acara dengan

    this.eventHub.$emit('update', data)
  3. Dan untuk mendengarkan yang Anda lakukan

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

Pembaruan Silakan lihat jawaban oleh @alex , yang menjelaskan solusi yang lebih sederhana.

kakoni
sumber
3
Perlu diketahui : awasi Global Mixins, dan coba hindari jika memungkinkan, karena menurut tautan ini vuejs.org/v2/guide/mixins.html#Global-Mixin, mereka dapat memengaruhi bahkan komponen pihak ketiga.
Vini.g.fer
6
Solusi yang lebih sederhana adalah menggunakan apa yang dijelaskan this.$root.$emit()this.$root.$on()
@Alex
5
Untuk referensi di masa mendatang, jangan perbarui jawaban Anda dengan jawaban orang lain (meskipun menurut Anda itu lebih baik dan Anda merujuknya). Tautkan ke jawaban alternatif, atau bahkan minta OP untuk menerima jawaban lain jika Anda pikir mereka harus - tetapi menyalin jawaban mereka ke jawaban Anda sendiri adalah bentuk yang buruk dan membuat pengguna enggan memberikan kredit di tempat yang seharusnya, karena mereka mungkin hanya jawab saja. Dorong mereka untuk menavigasi ke (dan dengan demikian memberi suara positif) jawaban yang Anda referensikan dengan tidak memasukkan jawaban itu ke dalam jawaban Anda sendiri.
GrayedFox
4
Terima kasih atas umpan balik yang berharga @GrayedFox, perbarui jawaban saya.
kakoni
2
Harap dicatat bahwa solusi ini tidak lagi didukung di Vue 3. Lihat stackoverflow.com/a/60895076/752916
AlexMA
146

Anda bahkan dapat membuatnya lebih pendek dan menggunakan instance root Vue sebagai Event Hub global:

Komponen 1:

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

Komponen 2:

mounted() {
    this.$root.$on('eventing', data => {
        console.log(data);
    });
}
Alex
sumber
2
Ini bekerja lebih baik daripada menentukan hub acara tambahan dan memasangnya ke konsumen acara mana pun.
schad
2
Saya penggemar berat solusi ini karena saya sangat tidak suka acara yang memiliki cakupan. Namun, saya tidak menggunakan VueJS setiap hari jadi saya ingin tahu apakah ada orang di luar sana yang melihat masalah dengan pendekatan ini.
Webnet
2
Solusi paling sederhana dari semua jawaban
Vikash Gupta
1
bagus, pendek dan mudah diimplementasikan, juga mudah dimengerti
nada
1
Jika Anda hanya ingin komunikasi saudara langsung secara eksklusif, gunakan $ parent daripada $ root
Malkev
47

Jenis komunikasi

Saat mendesain aplikasi Vue (atau pada kenyataannya, aplikasi berbasis komponen apa pun), ada tipe komunikasi berbeda yang bergantung pada masalah mana yang kita hadapi dan mereka memiliki saluran komunikasinya sendiri.

Logika bisnis: mengacu pada semua yang spesifik untuk aplikasi Anda dan tujuannya.

Logika presentasi: segala sesuatu yang berinteraksi dengan pengguna atau yang dihasilkan dari interaksi dari pengguna.

Kedua masalah ini terkait dengan jenis komunikasi berikut:

  • Status aplikasi
  • Orang tua-anak
  • Anak-orang tua
  • Saudara kandung

Setiap tipe harus menggunakan saluran komunikasi yang tepat.


Saluran komunikasi

Saluran adalah istilah longgar yang akan saya gunakan untuk merujuk pada implementasi konkret untuk bertukar data di sekitar aplikasi Vue.

Properti: Logika presentasi Orang Tua-Anak

Saluran komunikasi paling sederhana di Vue for direct Parent-Child komunikasi . Ini sebagian besar harus digunakan untuk meneruskan data yang berkaitan dengan logika presentasi atau kumpulan data terbatas di bawah hierarki.

Referensi dan metode: Presentasi anti-pola

Jika tidak masuk akal menggunakan prop untuk membiarkan anak menangani kejadian dari induk, menyiapkan refkomponen anak dan memanggil metodenya sudah cukup.

Jangan lakukan itu, itu anti-pola. Pikirkan kembali arsitektur komponen dan aliran data Anda. Jika Anda menemukan diri Anda ingin memanggil metode pada komponen anak dari orang tua, mungkin inilah saatnya untuk mengangkat status atau mempertimbangkan cara lain yang dijelaskan di sini atau di jawaban lain.

Events: Logika presentasi Child-Parent

$emit dan $on . Saluran komunikasi paling sederhana untuk komunikasi langsung Child-Parent. Sekali lagi, harus digunakan untuk logika presentasi.

Bus acara

Sebagian besar jawaban memberikan alternatif yang baik untuk bus acara, yang merupakan salah satu saluran komunikasi yang tersedia untuk komponen yang jauh, atau apa pun sebenarnya.

Ini bisa menjadi berguna saat meneruskan props ke semua tempat dari jauh ke bawah hingga komponen anak yang sangat bertingkat, dengan hampir tidak ada komponen lain yang membutuhkan ini di antaranya. Gunakan secukupnya untuk data yang dipilih dengan cermat.

Hati-hati: Pembuatan komponen selanjutnya yang mengikat dirinya sendiri ke bus acara akan terikat lebih dari satu kali - menyebabkan beberapa penangan terpicu dan bocor. Saya pribadi tidak pernah merasakan kebutuhan akan bus acara di semua aplikasi satu halaman yang telah saya rancang sebelumnya.

Berikut ini menunjukkan bagaimana kesalahan sederhana menyebabkan kebocoran di mana Itemkomponen masih terpicu meskipun dihapus dari DOM.

Ingatlah untuk menghapus listener di destroyedhook siklus proses.

Toko terpusat (logika bisnis)

Vuex adalah cara terbaik untuk menggunakan Vue untuk manajemen negara . Ini menawarkan lebih dari sekadar acara dan siap untuk aplikasi skala penuh.

Dan sekarang Anda bertanya :

[S] haruskah saya membuat toko vuex untuk setiap komunikasi kecil?

Ini benar-benar bersinar ketika:

  • berurusan dengan logika bisnis Anda,
  • berkomunikasi dengan backend (atau lapisan persistensi data apa pun, seperti penyimpanan lokal)

Jadi komponen Anda benar-benar dapat fokus pada hal-hal yang seharusnya, mengelola antarmuka pengguna.

Ini tidak berarti bahwa Anda tidak dapat menggunakannya untuk logika komponen, tetapi saya akan memperluas logika itu ke modul Vuex dengan namespace hanya dengan status UI global yang diperlukan.

Untuk menghindari kekacauan besar dalam segala hal dalam keadaan global, penyimpanan harus dipisahkan dalam beberapa modul dengan namespace.


Jenis komponen

Untuk mengatur semua komunikasi ini dan untuk memudahkan penggunaan kembali, kita harus memikirkan komponen sebagai dua jenis yang berbeda.

  • Penampung khusus aplikasi
  • Komponen umum

Sekali lagi, ini tidak berarti bahwa komponen generik harus digunakan kembali atau wadah khusus aplikasi tidak dapat digunakan kembali, tetapi mereka memiliki tanggung jawab yang berbeda.

Penampung khusus aplikasi

Ini hanyalah komponen Vue sederhana yang membungkus komponen Vue lainnya (generik atau wadah khusus aplikasi lainnya). Di sinilah komunikasi penyimpanan Vuex harus terjadi dan wadah ini harus berkomunikasi melalui cara lain yang lebih sederhana seperti alat peraga dan pendengar acara.

Penampung ini bahkan bisa saja tidak memiliki elemen DOM asli sama sekali dan membiarkan komponen generik menangani template dan interaksi pengguna.

cakupan entah bagaimana eventsatau storesvisibilitas untuk komponen saudara

Di sinilah pelingkupan terjadi. Sebagian besar komponen tidak tahu tentang penyimpanan dan komponen ini harus (kebanyakan) menggunakan satu modul penyimpanan berspasi nama dengan satu set terbatas gettersdan actionsditerapkan dengan pembantu pengikat Vuex yang disediakan .

Komponen umum

Ini harus menerima data mereka dari alat peraga, membuat perubahan pada data lokal mereka sendiri, dan mengeluarkan kejadian sederhana. Sebagian besar waktu, mereka seharusnya tidak tahu bahwa toko Vuex ada.

Mereka juga bisa disebut kontainer karena tanggung jawab mereka bisa untuk mengirim ke komponen UI lainnya.


Komunikasi saudara

Jadi, setelah semua ini, bagaimana seharusnya kita berkomunikasi antara dua komponen bersaudara?

Lebih mudah dipahami dengan sebuah contoh: katakanlah kita memiliki kotak input dan datanya harus dibagikan di seluruh aplikasi (saudara kandung di tempat berbeda di pohon) dan dipertahankan dengan backend.

Dimulai dengan skenario terburuk , komponen kami akan menggabungkan presentasi dan logika bisnis .

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

Untuk memisahkan kedua masalah ini, kita harus membungkus komponen kita dalam wadah khusus aplikasi dan menyimpan logika presentasi ke dalam komponen masukan generik kita.

Komponen input kita sekarang dapat digunakan kembali dan tidak mengetahui tentang backend maupun siblingnya.

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

Container khusus aplikasi kami sekarang dapat menjadi jembatan antara logika bisnis dan komunikasi presentasi.

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

Karena tindakan penyimpanan Vuex berhubungan dengan komunikasi backend, container kita di sini tidak perlu mengetahui tentang axios dan backend.

Emile Bergeron
sumber
3
Setuju dengan komentar tentang metode yang menjadi " penggabungan yang sama dengan menggunakan alat peraga "
ghybs
Saya suka jawaban ini. Tapi bisakah Anda menjelaskan lebih lanjut tentang Bus Acara dan catatan "Hati-hati:"? Mungkin Anda bisa memberikan beberapa contoh, saya tidak mengerti bagaimana komponen bisa diikat dua kali.
vandroid
Bagaimana Anda berkomunikasi antara komponen induk dan komponen anak grand misalnya validasi formulir. Di mana komponen induk adalah halaman, anak adalah bentuk, dan anak adalah elemen bentuk masukan?
Lord Zed
1
@vandroid Saya membuat contoh sederhana yang menunjukkan kebocoran ketika pendengar tidak dihapus dengan benar, seperti setiap contoh di utas ini.
Emile Bergeron
@LordZed Itu benar-benar tergantung, tetapi dari pemahaman saya tentang situasi Anda, sepertinya masalah desain. Vue harus digunakan sebagian besar untuk logika presentasi. Validasi formulir harus dilakukan di tempat lain, seperti di antarmuka vanilla JS API, yang akan dipanggil oleh tindakan Vuex dengan data dari formulir.
Emile Bergeron
10

Oke, kita bisa berkomunikasi antar saudara lewat orang tua menggunakan v-onevent.

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

Mari kita asumsikan bahwa kita ingin mengupdate Detailskomponen ketika kita mengklik beberapa elemen di List.


di Parent:

Template:

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

Sini:

  • v-on:select-itemitu sebuah acara, yang akan dipanggil dalam Listkomponen (lihat di bawah);
  • setSelectedItemitu adalah Parentmetode untuk memperbarui selectedModel;

JS:

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

Masuk List:

Template:

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

Sini:

  • this.$emit('select-item', item)akan mengirimkan barang melalui select-itemlangsung di orang tua. Dan orang tua akan mengirimkannya ke Detailstampilan
Sergei Panfilov
sumber
5

Apa yang biasanya saya lakukan jika saya ingin "meretas" pola komunikasi normal di Vue, khususnya sekarang yang .syncsudah usang, adalah membuat EventEmitter sederhana yang menangani komunikasi antar komponen. Dari salah satu proyek terbaru saya:

import {EventEmitter} from 'events'

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

Dengan Transmitterobjek ini, Anda dapat melakukannya, dalam komponen apa pun:

import Transmitter from './Transmitter'

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

Dan untuk membuat komponen "penerima":

import Transmitter from './Transmitter'

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

Sekali lagi, ini untuk penggunaan yang sangat spesifik. Jangan mendasarkan seluruh aplikasi Anda pada pola ini, gunakan sesuatu seperti itu Vuex.

Hector Lorenzo
sumber
1
Saya sudah menggunakan vuex, tetapi sekali lagi, haruskah saya membuat toko vuex untuk setiap komunikasi kecil?
Sergei Panfilov
Sulit bagi saya untuk mengatakan dengan jumlah informasi ini, tetapi saya akan mengatakan bahwa jika Anda sudah menggunakan vuexya, lakukanlah. Gunakan.
Hector Lorenzo
1
Sebenarnya saya tidak setuju bahwa kita perlu menggunakan vuex untuk setiap komunikasi kecil ...
Victor
Tidak, tentu saja tidak, itu semua tergantung konteksnya. Sebenarnya jawaban saya menjauh dari vuex. Di sisi lain, saya telah menemukan bahwa semakin banyak Anda menggunakan vuex dan konsep objek status pusat, semakin sedikit saya mengandalkan komunikasi antar objek. Tapi ya, setuju, itu semua tergantung.
Hector Lorenzo
3

Cara menangani komunikasi antar saudara tergantung pada situasinya. Tapi pertama-tama saya ingin menekankan bahwa pendekatan bus acara global akan dihentikan di Vue 3 . Lihat RFC ini . Oleh karena itu mengapa saya memutuskan untuk menulis jawaban baru.

Pola Leluhur Umum Terendah (atau "LCA")

Untuk kasus sederhana, saya sangat menyarankan menggunakan pola Leluhur Umum Terendah (juga dikenal sebagai "data turun, peristiwa naik"). Pola ini mudah dibaca, diterapkan, diuji, dan di-debug.

Intinya, ini berarti jika dua komponen perlu berkomunikasi, letakkan status bersama mereka di komponen terdekat yang sama-sama berbagi sebagai leluhur. Meneruskan data dari komponen induk ke komponen anak melalui props, dan meneruskan informasi dari anak ke induk dengan memancarkan sebuah peristiwa (lihat contohnya di bagian bawah jawaban ini).

Untuk contoh yang dibuat-buat, dalam aplikasi email, jika komponen "Kepada" perlu berinteraksi dengan komponen "badan pesan", status interaksi tersebut dapat berada di induknya (mungkin disebut komponen email-form). Anda mungkin memiliki prop di email-formdisebut addresseesehingga tubuh pesan dapat secara otomatis tambahkan Dear {{addressee.name}}ke email berdasarkan alamat email penerima.

LCA menjadi berat jika komunikasi harus menempuh jarak jauh dengan banyak komponen perantara. Saya sering merujuk rekan kerja ke posting blog yang luar biasa ini . (Abaikan fakta bahwa contohnya menggunakan Ember; idenya dapat diterapkan di banyak kerangka kerja UI.)

Pola Penampung Data (misalnya, Vuex)

Untuk kasus atau situasi kompleks di mana komunikasi orang tua-anak akan melibatkan terlalu banyak perantara, gunakan Vuex atau teknologi wadah data yang setara. Jika sesuai, gunakan modul dengan spasi nama .

Misalnya, mungkin masuk akal untuk membuat namespace terpisah untuk kumpulan komponen yang kompleks dengan banyak interkoneksi, seperti komponen kalender berfitur lengkap.

Publikasikan / Berlangganan (Bus Acara) Pola

Jika pola event bus (atau "publish / subscribe") lebih sesuai untuk kebutuhan Anda, tim inti Vue sekarang merekomendasikan menggunakan perpustakaan pihak ketiga seperti mitt . (Lihat RFC yang direferensikan di paragraf 1.)

Bonus bertele-tele dan kode

Berikut adalah contoh dasar dari solusi Leluhur Umum Terendah untuk komunikasi saudara-ke-saudara, yang diilustrasikan melalui permainan whack-a-mole .

Pendekatan yang naif mungkin berpikir, "tahi lalat 1 harus memberitahu tahi lalat 2 untuk muncul setelah dihantam". Tetapi Vue melarang pendekatan semacam ini, karena ia ingin kita berpikir dalam kerangka struktur pohon .

Ini mungkin hal yang sangat bagus. Aplikasi non-sepele tempat node berkomunikasi secara langsung satu sama lain di seluruh pohon DOM akan sangat sulit untuk di-debug tanpa semacam sistem akuntansi (seperti yang disediakan Vuex). Selain itu, komponen yang menggunakan "data turun, peristiwa naik" cenderung menunjukkan kopling rendah dan kegunaan kembali yang tinggi — keduanya merupakan sifat yang sangat diinginkan yang membantu aplikasi berskala besar.

Dalam contoh ini, ketika tahi lalat dipukul, itu memancarkan peristiwa. Komponen pengelola game memutuskan apa status baru aplikasi tersebut, dan dengan demikian mol saudara tahu apa yang harus dilakukan secara implisit setelah Vue merender ulang. Ini adalah contoh “nenek moyang terendah” yang agak sepele.

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
sumber