Зв'язок між компонентами братів і сестер у VueJs 2.0


112

Огляд

У Vue.js 2.x model.syncбуде застарілим .

Отже, що є правильним способом зв’язку між компонентами братів та сестер у Vue.js 2.x ?


Фон

Як я розумію Vue 2.x, кращим методом для передачі побратимів є використання магазину або шини подій .

За словами Евана (творця Vue):

Варто також згадати "передача даних між компонентами", як правило, погана ідея, оскільки врешті-решт потік даних стає нерозбірливим і дуже важко налагоджувати.

Якщо частину даних потрібно ділитися кількома компонентами, віддайте перевагу глобальним магазинам або Vuex .

[ Посилання на дискусію ]

І:

.onceі .syncзастаріли. Реквізит завжди завжди в один бік. Щоб створити побічні ефекти в батьківській області, компонент повинен явно emitподія, а не покладатися на неявне прив'язування.

Отже, Еван пропонує використовувати $emit()і $on().


Побоювання

Що мене хвилює:

  • Кожен storeі eventмає глобальну видимість (виправте мене, якщо я помиляюся);
  • Надто марно створювати новий магазин для кожного другорядного спілкування;

Те, що я хочу, - це певна сфера events чи storesвидимість компонентів братів і сестер. (Або, можливо, я не зрозумів вищевказану ідею.)


Питання

Отже, що є правильним способом спілкування між компонентами рідних братів?


2
$emitпоєднується з v-modelдля емуляції .sync. Я думаю, ти повинен піти шляхом
Vuex

3
Тож я вважав те саме питання. Моє рішення полягає в тому, щоб використовувати емітер події з широкомовними каналами, що еквівалентно "масштабу" - тобто установка дитини / батька та братів і сестер використовують один і той же канал для спілкування. У моєму випадку я використовую радіотехнічну бібліотеку radio.uxder.com, оскільки це лише кілька рядків коду та його буленепроникність, але багато хто обирає вузол EventEmitter.
Tremendus Apps

Відповіді:


83

З Vue 2.0 я використовую механізм eventHub, як показано в документації .

  1. Визначте централізований центр подій.

    const eventHub = new Vue() // Single event hub
    
    // Distribute to components using global mixin
    Vue.mixin({
        data: function () {
            return {
                eventHub: eventHub
            }
        }
    })
  2. Тепер у своєму компоненті ви можете випромінювати події за допомогою

    this.eventHub.$emit('update', data)
  3. І слухати ви це робите

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

Оновлення Будь ласка, дивіться відповідь від @alex , де описано більш просте рішення.


3
Тільки головами вгору: слідкуйте за Global Mixins і намагайтеся уникати їх, коли це можливо, оскільки згідно з цим посиланням vuejs.org/v2/guide/mixins.html#Global-Mixin вони можуть впливати навіть на сторонні компоненти.
Vini.g.fer

6
Набагато простішим рішенням є використання того, що @Alex описав - this.$root.$emit()іthis.$root.$on()
Webnet

5
Для подальшого ознайомлення, будь ласка, не оновлюйте свою відповідь чужою відповіддю (навіть якщо ви вважаєте, що це краще, і ви посилаєтесь на неї). Посилайтесь на альтернативну відповідь або навіть попросіть ОП прийняти іншу, якщо ви вважаєте, що вони повинні - але копіювання їхньої відповіді у вашу власну - це погана форма і перешкоджає користувачам надавати кредит там, де це належить, оскільки вони можуть просто повернути лише ваш відповісти лише. Закликайте їх перейти до (і таким чином оновити) відповідь, на яку ви посилаєтесь, не включаючи цю відповідь у себе.
GrayedFox

4
Дякую за цінний відгук @GrayedFox, відповідно оновив мою відповідь.
каконі

2
Будь ласка , зверніть увагу , що це рішення більше не буде підтримуватися в Vue 3. Див stackoverflow.com/a/60895076/752916
AlexMA

145

Ви навіть можете скоротити його і використовувати екземпляр root Vue як глобальний центр подій:

Компонент 1:

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

Компонент 2:

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

2
Це працює краще, ніж визначати центр подій додавання та приєднувати його до будь-якого споживача події.
schad

2
Я великий шанувальник цього рішення, оскільки мені дуже не подобаються події, що мають розмах. Однак я не щодня з VueJS, тому мені цікаво, чи є хтось там, хто бачить проблеми з таким підходом.
Вебнет

2
Найпростіше рішення з усіх відповідей
Вікаш Гупта,

1
приємний, короткий і простий у здійсненні, легко зрозуміти також
нада

1
Якщо ви хочете, щоб виключно пряме спілкування братів і сестер, використовуйте $ parent замість $ root
Малкев

47

Типи зв’язку

При розробці програми Vue (або насправді будь-якого додатка на основі компонентів) існують різні типи комунікацій, які залежать від того, з якими питаннями ми маємо справу, і вони мають свої канали зв'язку.

Бізнес-логіка: стосується всього конкретного для вашої програми та його мети.

Логіка презентації: все, з чим користувач взаємодіє або є результатом взаємодії користувача.

Ці два питання пов'язані з цими типами спілкування:

  • Стан програми
  • Батько-дитина
  • Дитина-батько
  • Брати і сестри

Кожен тип повинен використовувати правильний канал зв'язку.


Канали зв'язку

Канал - це вільний термін, який я буду використовувати для позначення конкретних реалізацій для обміну даними навколо програми Vue.

Реквізит: логіка презентації батько-дитина

Найпростіший канал зв'язку у Vue для прямого спілкування батько-дитина . Він здебільшого повинен використовуватися для передачі даних, що стосуються логіки презентації, або обмеженого набору даних вниз по ієрархії.

Реферати та методи: Анти-шаблон презентації

Коли немає сенсу використовувати опори, щоб дозволити дитині обробляти події від батьків, налаштування refна дочірній компонент та виклик його методів просто чудово.

Не робіть цього, це анти-шаблон. Перегляньте свою архітектуру компонентів та потік даних. Якщо ви хочете зателефонувати на метод дочірнього компонента від батьків, певно, настав час підняти стан або розглянути інші способи, описані тут або в інших відповідях.

Події: логіка презентації дитина-батько

$emitі $on. Найпростіший канал зв'язку для прямого спілкування дитина-батько. Знову ж таки, слід використовувати для логіки презентації.

Автобус події

Більшість відповідей дають хороші альтернативи шині подій, яка є одним із каналів зв'язку, доступних для віддалених компонентів, або будь-що насправді.

Це може стати корисним при передачі реквізитів по всьому місцю далеко вниз до глибоко вкладених дочірніх компонентів, причому майже ніяких інших компонентів вони не потребують між ними. Економно використовуйте для ретельно підібраних даних.

Будьте уважні: Подальше створення компонентів, які прив'язуються до шини подій, буде пов'язано не один раз - це призведе до запуску декількох обробників та протікань. Я особисто ніколи не відчував потреби в шині подій у всіх програмах на одній сторінці, які я розробляв у минулому.

Далі показано, як проста помилка призводить до витоку, коли Itemкомпонент все ще спрацьовує, навіть якщо його видалено з DOM.

Не забудьте видалити слухачів у destroyedжиттєвий цикл.

Централізований магазин (Бізнес-логіка)

Vuex - це шлях з Vue для управління державою . Він пропонує набагато більше, ніж просто події, і він готовий до повномасштабного застосування.

А тепер ви запитаєте :

[S] Чи можу я створити магазин vuex для кожного другорядного спілкування?

Він дійсно світить, коли:

  • робота з вашою бізнес-логікою,
  • спілкування із заднім числом (або будь-яким шаром збереження даних, наприклад локальним сховищем)

Таким чином, ваші компоненти можуть дійсно зосередитись на речах, керуючи інтерфейсами користувача.

Це не означає, що ви не можете використовувати його для логіки компонентів, але я застосував би цю логіку до модуля Vuex, розміщеного на імена, лише з необхідним глобальним станом інтерфейсу користувача.

Щоб не стикатися з великим безладом всього глобального стану, магазин повинен бути розділений на кілька модулів, що мають простір імен.


Типи компонентів

Для того, щоб оркеструвати всі ці комунікації та полегшити повторну використання, ми повинні розглядати компоненти як два різних типи.

  • Контейнери для конкретних програм
  • Родові компоненти

Знову ж таки, це не означає, що загальний компонент слід використовувати повторно або що контейнер, який використовується для додатків, не можна повторно використовувати, але вони несуть різні обов'язки.

Контейнери для конкретних програм

Це просто прості компоненти Vue, які обгортають інші компоненти Vue (загальні або інші спеціальні контейнери). Тут має відбуватися спілкування магазину Vuex, і цей контейнер повинен спілкуватися за допомогою інших простих засобів, таких як реквізити та слухачі подій.

У цих контейнерах навіть не може бути вбудованих елементів DOM і дозволяти загальним компонентам мати справу з шаблонуванням та взаємодією користувачів.

Обсяг яким - то чином eventsабо storesвидимість для сибсов компонентів

Тут відбувається скопінг. Більшість компонентів не знають про магазин, і цей компонент повинен (в основному) використовувати один просторовий модуль магазину з обмеженим набором gettersі actionsзастосовувати разом із наданими помічниками Vuex, що зв'язують .

Родові компоненти

Вони повинні отримувати свої дані від реквізиту, вносити зміни до власних локальних даних та випробовувати прості події. Здебільшого вони не повинні знати, що магазин Vuex взагалі існує.

Їх також можна назвати контейнерами, оскільки їх єдиною відповідальністю може бути відправлення на інші компоненти інтерфейсу.


Близький зв’язок

Отже, після всього цього, як нам спілкуватися між двома компонентами братів і сестер?

Це легше зрозуміти на прикладі: скажімо, у нас є поле для введення даних, і його дані слід обмінюватись у додатку (братів і сестер у різних місцях дерева) та зберігатись із доповненням.

Починаючи з найгіршого випадку , наш компонент змішав би презентацію та бізнес- логіку.

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

Щоб розділити ці дві проблеми, ми повинні загортати наш компонент у контейнер для конкретного додатка та зберігати логіку презентації у нашому загальному компоненті введення.

Наш вхідний компонент тепер багаторазовий і не знає ні про вихід, ні про побратимів.

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

Тепер наш конкретний контейнер може стати мостом між діловою логікою та презентаційною комунікацією.

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

Так як магазин Vuex дії мати справу з серверної зв'язку, наш контейнер тут не потрібно знати про Вардар і внутрішньому інтерфейсі.


3
Погодьтеся з коментарем щодо того, що методи є " тим самим з'єднанням, що і для використання реквізиту "
ghybs

Мені подобається ця відповідь. Але ви можете детальніше розглянути питання про автобус подій та примітку "Будьте уважні:" Можливо, ви можете навести якийсь приклад, я не розумію, як компоненти можуть бути прив’язані двічі.
vandroid

Як ви спілкуєтесь між батьківським компонентом та основним дочірнім компонентом, наприклад, перевірка форми. Де батьківський компонент - це сторінка, дочір - це форма, а внук - це елемент форми введення?
Лорд Зед

1
@vandroid Я створив простий приклад, який показує протікання, коли слухачі не видаляються належним чином, як і всі приклади в цій темі.
Еміль Бержерон

@LordZed Це дійсно залежить, але від мого розуміння вашої ситуації це виглядає як проблема дизайну. Vue слід використовувати переважно для логіки презентації. Перевірка форми повинна проводитися в іншому місці, як в інтерфейсі API ванільного JS, щоб дія Vuex викликала дані з форми.
Еміль Бержерон

10

Гаразд, ми можемо спілкуватися між братами та сестрами через батьків, використовуючи v-onподії.

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

Припустимо, що ми хочемо оновити Detailsкомпонент, коли натискаємо якийсь елемент у List.


в Parent:

Шаблон:

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

Тут:

  • v-on:select-itemце подія, яка буде викликана в складі List(див. нижче);
  • setSelectedItemце Parentметод оновлення selectedModel;

JS:

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

В List:

Шаблон:

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

Тут:

  • this.$emit('select-item', item)відправить товар select-itemбезпосередньо через батьківський. І батько відправить його на Detailsперегляд

5

Що я зазвичай роблю, якщо хочу "зламати" звичайні моделі спілкування у Vue, особливо зараз .syncзастарілу, - це створити простий EventEmitter, який обробляє зв'язок між компонентами. З одного з моїх останніх проектів:

import {EventEmitter} from 'events'

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

З цим Transmitterоб'єктом ви зможете в будь-якому компоненті:

import Transmitter from './Transmitter'

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

І щоб створити "приймаючий" компонент:

import Transmitter from './Transmitter'

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

Знову ж таки, це дійсно для конкретних цілей використання. Не базуйте всю свою заявку на цій схемі, використовуйте щось подібне Vuex.


1
Я вже користуюся vuex, але знову ж таки, чи варто створити магазин vuex для кожного другорядного спілкування?
Сергій Панфілов

Мені важко сказати з такою кількістю інформації, але я б сказав, що якщо ви вже використовуєте vuexтак, продовжуйте це робити. Використай це.
Гектор Лоренцо

1
Насправді я не погоджуюся з тим, що нам потрібно використовувати vuex для кожного другорядного спілкування ...
Віктор

Ні, звичайно ні, все залежить від контексту. Насправді моя відповідь відходить від vuex. З іншого боку, я виявив, що чим більше ви використовуєте vuex та поняття об'єкта центрального стану, тим менше я покладаюся на спілкування між об'єктами. Але так, погодьтеся, все залежить.
Гектор Лоренцо

3

Як здійснювати спілкування між братами і сестрами, залежить від ситуації. Але спершу хочу підкреслити, що глобальний підхід автобусного події відходить у Vue 3 . Дивіться цей RFC . Тому я вирішив написати нову відповідь.

Найнижча загальна картина предків (або "LCA")

Для простих випадків я настійно рекомендую використовувати найменший шаблон загального предка (також відомий як "дані вниз, події вгору"). Цей шаблон легко читати, реалізовувати, тестувати та налагоджувати.

По суті, це означає, що якщо двом компонентам потрібно спілкуватися, перевести їх спільний стан у найближчий компонент, який обидва поділяють як предка. Передайте дані від батьківського компонента до дочірнього компонента через реквізити та передайте інформацію від дочірнього до батьківського, випромінюючи подію (див. Приклад цього внизу цієї відповіді).

На прикладі надуманого прикладу в додатку електронної пошти, якщо компонент "Кому" потрібен для взаємодії з компонентом "тіло повідомлення", стан цієї взаємодії може жити в їхньому батьківському (можливо, компоненті, який називається email-form). Ви могли б мати опору в email-formназивається addresseeтак , що тіло повідомлення може автоматично PREPEND Dear {{addressee.name}}на адресу електронної пошти на основі адреси електронної пошти одержувача.

LCA стає обтяжливим, якщо комунікація повинна проїхати великі відстані з великою кількістю компонентів посередників. Я часто посилаюсь на колег із цієї чудової публікації в блозі . (Ігноруйте той факт, що в його прикладах використовується Ember; її ідеї застосовні у багатьох структурах інтерфейсу користувача.)

Шаблон контейнера даних (наприклад, Vuex)

Для складних випадків або ситуацій, коли спілкування між батьком і дитиною залучає занадто багато посередників, використовуйте Vuex або еквівалентну технологію контейнера даних. За необхідності використовуйте модулі з простором імен .

Наприклад, може бути доцільним створити окремий простір імен для складної колекції компонентів з безліччю взаємозв'язків, таких як повнофункціональний компонент календаря.

Опублікувати / Підписатися (Шина подій) Шаблон

Якщо модель шини подій (або «опублікувати / підписатись») більше відповідає вашим потребам, основна команда Vue рекомендує використовувати сторонні бібліотеки, такі як mitt . (Див. RFC, на який посилається пункт 1).

Бонусні перемоги та код

Ось основний приклад найменшого рішення загального предка для спілкування між братами та братами, проілюстрованого за допомогою гри whack-a-mol .

Наївним підходом може бути думка: "моль 1 повинен сказати, що моль 2 з'явиться після його удару". Але Vue стримує подібний підхід, оскільки він хоче, щоб ми думали з точки зору деревних структур .

Це, мабуть, дуже гарна річ. Нетривіальне додаток, де вузли спілкуються безпосередньо один з одним через дерева DOM, було б дуже важко налагоджувати без якоїсь системи обліку (як надає Vuex). На додаток до цього компоненти, які використовують "знизу даних, події вгору", як правило, демонструють низьку взаємозв'язок та високу повторну використання - обидва вкрай бажані риси, які допомагають масштабувати великі програми.

У цьому прикладі, коли моль ударений, він випромінює подію. Компонент диспетчера ігор вирішує, який новий стан програми, і, отже, родичка знає, що робити неявно після повторного надання Vue. Це дещо тривіальний приклад "найнижчого загального предка".

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>

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.