VueJs 2.0の兄弟コンポーネント間の通信


112

概観

Vue.js 2.xでは、model.syncされる非推奨

では、Vue.js 2.xの兄弟コンポーネント間で通信する適切な方法は何ですか?


バックグラウンド

私がVue 2.xを理解しているように、兄弟通信の好ましい方法は、ストアまたはイベントバスを使用することです。

エヴァン(ヴューの創設者)によると:

また、データフローが最終的に追跡できなくなり、デバッグが非常に困難になるため、「コンポーネント間でのデータの受け渡し」は一般に悪い考えです。

データの一部を複数のコンポーネントで共有する必要がある場合は、 グローバルストアまたはVuexを使用してください

[ ディスカッションへのリンク ]

そして:

.onceおよび.sync非推奨です。小道具は常に一方通行です。親スコープで副作用を生成するには、コンポーネントはemit暗黙的なバインディングに依存するのではなく、イベントを明示的に指定する必要があります。

したがって、Evan $emit()およびの使用を推奨してい$on()ます。


懸念

私が心配しているのは:

  • それぞれがグローバルな可視性storeevent持っています(私が間違っている場合は修正してください)。
  • マイナーなコミュニケーションごとに新しいストアを作成するのはもったいないです。

私が欲しいのは、兄弟コンポーネントのスコープ eventsまたはstores可視性です。(あるいは、おそらく私は上記の考えを理解していませんでした。)


質問

では、兄弟コンポーネント間で通信するための正しい方法は何ですか?


2
$emitと組み合わせv-modelてエミュレートし.syncます。Vuexの方法を使うべきだと思います
eltonkamami

3
だから私は同じ懸念を検討しました。私の解決策は、「スコープ」に相当するブロードキャストチャネルでイベントエミッターを使用することです。つまり、子/親と兄弟のセットアップは同じチャネルを使用して通信します。私の場合、無線ライブラリradio.uxder.comを使用しています。これは、ほんの数行のコードとその防弾ですが、多くの場合、ノードEventEmitterを選択します。
Tremendusアプリ2016

回答:


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
ただの注意です:このリンクvuejs.org/v2/guide/mixins.html#Global-Mixinによれば、サードパーティのコンポーネントにも影響を与える可能性があるため、グローバルミックスインに注意し、可能な限り回避するようにしてください。
Vini.g.fer 2017年

6
はるかに簡単な解決策は@Alexを説明するものを使用することである- this.$root.$emit()this.$root.$on()
Webnet

5
今後の参考のために、他の人の回答で回答を更新しないでください(それがより良いと考えて参照した場合でも)。代替回答にリンクするか、必要な場合はOPに他の回答を受け入れるよう依頼することもできますが、回答を自分自身にコピーすることは悪い形であり、ユーザーが自分だけを賛成する可能性があるため、期日どおりにクレジットを付与しないようにしてください。答えるだけ。自分の回答に自分の回答を含めないことで、参照している回答に移動する(したがって、賛成する)ように彼らに勧めます。
GrayedFox 2018年

4
貴重なフィードバックをありがとう@GrayedFox、それに応じて私の答えを更新しました。
kakoni

2
このソリューションは、もはやヴュー3参照のことでサポートされることをしてくださいノート stackoverflow.com/a/60895076/752916
AlexMA

145

さらに短くして、ルート Vueインスタンスをグローバルイベントハブとして使用することもできます。

コンポーネント1:

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

コンポーネント2:

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

2
これは、追加のイベントハブを定義して任意のイベントコンシューマーにアタッチするよりもうまく機能します。
schad

2
私はこのソリューションの大ファンです。スコープを持つイベントは本当に嫌いです。ただし、私は毎日VueJSを利用しているわけではないので、このアプローチで問題が発生している人がいるかどうか知りたいです。
Webnet

2
すべての答えの最も簡単な解決策
Vikashグプタ

1
素敵で短く、実装も簡単、理解も簡単
nada

1
直接兄弟の通信のみを使用する場合は、$ rootの代わりに$ parentを使用します
Malkev

47

通信タイプ

Vueアプリケーション(または実際にはコンポーネントベースのアプリケーション)を設計する場合、さまざまな通信タイプがあり、それらは私たちが扱っている懸念に依存し、それらには独自の通信チャネルがあります。

ビジネスロジック:アプリとその目標に固有のすべてを指します。

プレゼンテーションロジック:ユーザーが操作するもの、またはユーザーからの操作に起因するもの。

これらの2つの懸念は、次のタイプの通信に関連しています。

  • アプリケーションの状態
  • 親子
  • 子親
  • きょうだい

各タイプは適切な通信チャネルを使用する必要があります。


コミュニケーションチャンネル

チャネルとは、Vueアプリの周囲でデータを交換するための具体的な実装を指すために使用する緩い用語です。

小道具:親子プレゼンテーションロジック

直接の親子通信のためのVueで最も単純な通信チャネル。これは主に、プレゼンテーションロジックに関連するデータや、階層を下って制限されたデータセットを渡すために使用する必要があります。

参照と方法:プレゼンテーションのアンチパターン

プロップを使用して子が親からのイベントを処理できるようにしても意味がない場合は、子コンポーネントにを設定してrefそのメソッドを呼び出すだけで問題ありません。

それをしないでください、それはアンチパターンです。コンポーネントアーキテクチャとデータフローを再考します。親から子コンポーネントのメソッドを呼び出したい場合は、状態を上げるか、ここまたは他の回答で説明されている他の方法を検討するときです。

イベント:子親プレゼンテーションロジック

$emit$on。直接の親子通信のための最も単純な通信チャネル。この場合も、プレゼンテーションロジックに使用する必要があります。

イベントバス

ほとんどの回答は、遠方のコンポーネントや実際には何でも使用できる通信チャネルの1つであるイベントバスの優れた代替案を示しています。

これは、他のコンポーネントがこれらの間にほとんど必要とせずに、小道具を遠くから下に深くネストされた子コンポーネントに渡すときに役立ちます。慎重に選択したデータには慎重に使用してください。

注意:イベントバスに自分自身をバインドしているコンポーネントのその後の作成は、複数回バインドされ、複数のハンドラーがトリガーされてリークします。個人的に、過去にデザインしたすべてのシングルページアプリでイベントバスの必要性を感じたことはありません。

以下は、単純なミスがItem、DOMから削除された場合でもコンポーネントが引き続きトリガーされるリークにつながる方法を示しています。

destroyedライフサイクルフックでリスナーを削除することを忘れないでください。

一元化されたストア(ビジネスロジック)

Vuexは、Vueを使用して状態を管理する方法です。イベントだけでなく、本格的なアプリケーションの準備ができています。

そして今あなたは尋ねます

[S]マイナーコミュニケーションごとにvuexのストアを作成する必要がありますか?

それは本当に輝きます:

  • あなたのビジネスロジックを扱う、
  • バックエンド(またはローカルストレージなどのデータ永続化レイヤー)との通信

そのため、コンポーネントは、ユーザーインターフェースの管理など、本来あるべきことに集中できます。

コンポーネントロジックに使用できないという意味ではありませんが、必要なグローバルUI状態のみを持つ名前空間付きのVuexモジュールにそのロジックのスコープを設定します。

グローバル状態のすべての大きな混乱を処理することを回避するには、ストアを複数の名前空間モジュールに分離する必要があります。


コンポーネントタイプ

これらすべての通信を調整し、再利用を容易にするために、コンポーネントを2つの異なるタイプと考える必要があります。

  • アプリ固有のコンテナ
  • 一般的なコンポーネント

繰り返しになりますが、これは汎用コンポーネントを再利用する必要があることや、アプリ固有のコンテナを再利用できないことを意味するものではありませんが、責任は異なります。

アプリ固有のコンテナ

これらは、他のVueコンポーネント(汎用または他のアプリ固有のコンテナー)をラップする単純なVueコンポーネントです。これは、Vuexストア通信が発生する場所であり、このコンテナーは、小道具やイベントリスナーなどの他のより単純な手段を通じて通信する必要があります。

これらのコンテナーは、ネイティブDOM要素をまったく持たず、汎用コンポーネントがテンプレートとユーザーの対話を処理できるようにすることもできます。

スコープ何とかeventsまたはstores兄弟コンポーネントの可視性

スコーピングはここで行われます。ほとんどのコンポーネントはストアについて認識していません。このコンポーネントは、(ほとんどの場合)限定されたセットの1つの名前空間付きストアモジュールを使用し、提供されたVuexバインディングヘルパーgettersactions適用する必要があります

一般的なコンポーネント

これらは、小道具からデータを受け取り、独自のローカルデータに変更を加え、単純なイベントを発行する必要があります。ほとんどの場合、Vuexストアが存在することをまったく知らないはずです。

他のUIコンポーネントへのディスパッチが唯一の責任である可能性があるため、これらはコンテナーと呼ばれることもあります。


兄弟のコミュニケーション

それでは、この後、2つの兄弟コンポーネント間でどのように通信する必要がありますか?

例を使用すると理解しやすくなります。入力ボックスがあり、そのデータはアプリ全体で共有され(ツリー内の異なる場所にある兄弟)、バックエンドで永続化されている必要があるとします。

最悪のシナリオから始めて、私たちのコンポーネントはプレゼンテーションビジネスロジックを混合します。

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

これら2つの問題を分離するには、コンポーネントをアプリ固有のコンテナーにラップし、プレゼンテーションロジックを汎用入力コンポーネントに保持する必要があります。

これで、入力コンポーネントは再利用可能になり、バックエンドも兄弟も知りません。

// 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ストアアクションはバックエンド通信を処理するため、ここでのコンテナーはaxios とバックエンドについて知る必要はありません。


3
「である方法についてのコメントに同意するもの小道具使用して、同じカップリング
ghybsを

私はこの答えが好きです。しかし、イベントバスについて詳しく説明して、「注意してください」というメモはありませんか。多分あなたはいくつかの例を挙げることができます、私はコンポーネントが2回バインドされるようになることができる方法を理解していません。
vandroid 2018

フォームの検証など、親コンポーネントと孫コンポーネントの間でどのように通信しますか。親コンポーネントがページ、子がフォーム、孫が入力フォーム要素の場合、
主ゼッド

1
@vandroidこのスレッドのすべての例のように、リスナーが適切に削除されていないときにリークを示す簡単な例を作成しました。
エミールベルジェロン2018年

@LordZed状況によって異なりますが、私の状況を理解すると、設計上の問題のように見えます。Vueは主にプレゼンテーションロジックに使用する必要があります。フォームの検証は、通常のJS APIインターフェースのように、Vuexアクションがフォームのデータを使用して呼び出す他の場所で行う必要があります。
エミールベルジェロン2018年

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を作成することです。私の最新プロジェクトの1つから:

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のストアを作成する必要がありますか
セルゲイパンフィロフ2016

この量の情報で言うのは難しいですが、すでにvuex「はい」を使用している場合は、それでいいと思います。これを使って。
Hector Lorenzo

1
実際には、マイナーなコミュニケーションごとにvuexを使用する必要があるとは思いません...
ビクター

いいえ、もちろんそうではありません。すべて状況に依存します。実際、私の答えはvuexから遠ざかっています。一方、vuexと中央状態オブジェクトの概念を使用すればするほど、オブジェクト間の通信に依存することが少なくなることがわかりました。しかし、はい、同意します。すべてに依存します。
Hector Lorenzo

3

兄弟間の通信の処理方法は状況によって異なります。ただし、最初に、Vue 3ではグローバルイベントバスアプローチが廃止されることを強調しておきます。このRFCを参照してください。だから私はなぜ新しい答えを書くことにしました。

最も一般的な祖先パターン(または「LCA」)

単純なケースでは、最も低い共通祖先パターン(「データダウン、イベントアップ」とも呼ばれます)を使用することを強くお勧めします。このパターンは、読み取り、実装、テスト、およびデバッグが簡単です。

つまり、2つのコンポーネントが通信する必要がある場合、それらの共有状態を、両方が祖先として共有する最も近いコンポーネントに入れます。プロップを介して親コンポーネントから子コンポーネントにデータを渡し、イベントを発行して子から親に情報を渡します(この回答の下部にあるこの例を参照)。

不自然な例として、メールアプリで「To」コンポーネントが「メッセージ本文」コンポーネントとやり取りする必要がある場合、そのやり取りの状態は親(おそらくと呼ばれるコンポーネントemail-form)に存在する可能性があります。受信者の電子メールアドレスに基づいてメッセージ本文が自動的に電子メールの先頭に追加できるように、email-form呼び出し先に小道具をaddressee含めることができDear {{addressee.name}}ます。

通信が多くの仲介者コンポーネントで長距離を移動する必要がある場合、LCAは面倒になります。私はよく同僚にこの優れたブログ投稿を紹介します。(その例がEmberを使用しているという事実は無視してください。そのアイデアは多くのUIフレームワークに適用できます。)

データコンテナーパターン(Vuexなど)

親子のコミュニケーションに仲介者が多すぎる複雑なケースや状況では、Vuexまたは同等のデータコンテナテクノロジーを使用します。必要に応じて、名前空間モジュールを使用します

たとえば、完全に機能するカレンダーコンポーネントなど、多くの相互接続を持つコンポーネントの複雑なコレクションに対して個別の名前空間を作成するのが妥当な場合があります。

パブリッシュ/サブスクライブ(イベントバス)パターン

イベントバス(または「パブリッシュ/サブスクライブ」)パターンがニーズに適している場合、Vueコアチームはmittなどのサードパーティライブラリの使用を推奨しています。(段落1で参照されているRFCを参照してください。)

ボーナスとりとめとコード

これは、ゲームwhack-a-moleを介して示されている、兄弟間通信の最も一般的な祖先ソリューションの基本的な例です。

単純なアプローチは、「もぐら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>

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.