web-dev-qa-db-ja.com

イベントを使用して分離されたコンポーネント間の通信

たくさん(> 50)の小さな WebComponents が相互作用するWebアプリがあります。

すべてを切り離しておくために、原則として、コンポーネントが別のコンポーネントを直接参照することはできません。代わりに、コンポーネントがイベントを発生させ、(「メイン」アプリ内で)配線されて、別のコンポーネントのメソッドを呼び出します。

時間が経つにつれ、追加されるコンポーネントが増え、「メイン」のアプリファイルには次のようなコードチャンクが散らばっています。

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

これを改善するために、類似の機能をClassにグループ化し、関連性のある名前を付け、インスタンス化時に参加要素を渡し、Class内の「配線」を処理します。

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

そして、次のようにインスタンス化します。

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

私は自分自身のパターンを導入することに本当にうんざりしています。特に、より良いWordがないため、Classesを単なる初期化コンテナーとして使用しているため、実際にはOOP-yではないパターンを導入することにうんざりしています。

私が見逃しているこのタイプのタスクを処理するためのよりよく知られた定義済みのパターンはありますか?

8
Nik Kyriakides

あなたが持っているコードはかなり良いです。少し不快に思えるのは、初期化コードがオブジェクト自体の一部ではないということです。つまり、オブジェクトをインスタンス化できますが、そのワイヤリングクラスの呼び出しを忘れると、そのオブジェクトは役に立たなくなります。

通知センター(別名イベントバス)が次のように定義されているとします。

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

これはDIYのマルチディスパッチイベントハンドラです。その後、コンストラクタの引数としてNotificationCenterを要求するだけで、独自の配線を行うことができます。そこにメッセージを送信し、ペイロードを渡すのを待つのは、システムとの唯一の連絡先なので、非常にしっかりしています。

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

注:質問で使用されているスタイルと一致させるため、および簡単にするために、キーにインプレース文字列リテラルを使用しました。タイプミスのリスクがあるため、これはお勧めできません。代わりに、列挙または文字列定数の使用を検討してください。

上記のコードでは、ツールバーは、関心のあるイベントのタイプをNotificationCenterに通知し、notifyメソッドを介してすべての外部インタラクションを公開します。 toolbar-button-click-eventに関心のある他のクラスは、コンストラクターに登録するだけです。

このパターンの興味深いバリエーションは次のとおりです。

  • 複数のNCを使用してシステムのさまざまな部分を処理する
  • Notifyメソッドが逐次的にブロックするのではなく、通知ごとにスレッドを生成するようにする
  • NC内の通常のリストではなく優先リストを使用して、コンポーネントが最初に通知される部分的な順序付けを保証する
  • 後で登録解除するために使用できるIDを返す登録
  • メッセージ引数をスキップし、メッセージのクラス/タイプに基づいてディスパッチする

興味深い機能は次のとおりです。

  • NCの計測は、ロガーを登録してペイロードを印刷するのと同じくらい簡単です
  • 相互作用する1つ以上のコンポーネントのテストは、単にそれらをインスタンス化し、期待される結果のリスナーを追加し、メッセージを送信するだけです。
  • 古いメッセージをリッスンする新しいコンポーネントを追加するのは簡単です
  • 古いコンポーネントにメッセージを送信する新しいコンポーネントを追加するのは簡単です

興味深い落とし穴と可能な対策は次のとおりです。

  • 他のイベントをトリガーするイベントは、混乱する可能性があります。
    • 予期しないイベントのソースを特定するために、イベントに送信者IDを含めます。
  • 各コンポーネントは、システムの特定の部分がイベントを受信する前に稼働しているかどうかを認識していないため、初期のメッセージがドロップされる可能性があります。
    • これは、「システム準備完了」メッセージを送信するコンポーネントを作成するコードで処理できます。もちろん、関心のあるコンポーネントが登録する必要があります。
  • イベントバスは、コンポーネント間に暗黙のインターフェイスを作成します。つまり、コンパイラーが必要なすべてを実装したことを確認する方法はありません。
    • 静的と動的の間の標準的な引数がここに適用されます。
  • このアプローチは、必ずしも動作ではなく、コンポーネントをグループ化します。システムを介してイベントを追跡するには、OPのアプローチよりも多くの作業が必要になる場合があります。たとえば、OPでは、保存に関連するすべてのリスナーを一緒に設定し、削除に関連するリスナーを別の場所に一緒に設定することができます。
    • これは、適切なイベントの名前とフローチャートなどのドキュメントで軽減できます。 (はい、ドキュメンテーションはコードと段階的にずれています)。すべてのメッセージを取得し、誰が何をどの順序で送信したかを出力する、キャッチオールハンドラリストとポストキャッチオールハンドラリストを追加することもできます。
6
Joel Harmon

以前は、ある種の「イベントバス」を導入していましたが、UIコードのイベントを伝達するために、ドキュメントオブジェクトモデル自体にますます依存するようになりました。

ブラウザでは、DOMは、ページのロード中であっても常に存在する1つの依存関係です。重要なのは、JavaScriptで カスタムイベント を利用し、イベントバブリングを利用してそれらのイベントを伝達することです。

サブスクライバーを接続する前に、「ドキュメントの準備ができるのを待つ」と叫ぶ前に、document.documentElementプロパティは、スクリプトがインポートされた場所や順序に関係なく、JavaScriptが実行を開始した瞬間から<html>要素を参照しますマークアップに表示されます。

ここからイベントのリスニングを開始できます。

ページ上の特定のHTMLタグ内にJavaScriptコンポーネント(またはウィジェット)を配置することは非常に一般的です。コンポーネントの「ルート」要素は、バブリングイベントをトリガーできる場所です。 <html>要素のサブスクライバーは、他のユーザーが生成したイベントと同様に、これらの通知を受け取ります。

ボイラープレートコードの例:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

したがって、パターンは次のようになります。

  1. カスタムイベントを使用する
  2. document.documentElementプロパティでこれらのイベントをサブスクライブします(ドキュメントの準備が整うまで待つ必要はありません)。
  3. コンポーネントのルート要素、またはdocument.documentElementでイベントを公開します。

これは、関数型とオブジェクト指向の両方のコードベースで機能します。

3
Greg Burghardt

Unity 3Dを使用したビデオゲーム開発でも同じスタイルを使用しています。 Health、Input、Stats、Soundなどのコンポーネントを作成し、それらをゲームオブジェクトに追加して、そのゲームオブジェクトを構築します。 Unityには、コンポーネントをゲームオブジェクトに追加するメカニズムがすでにあります。しかし、私が見つけたのは、ほとんどの人がコンポーネントを照会したり、他のコンポーネント内のコンポーネントを直接参照していることです(それらがインターフェイスを使用していたとしても、私が好むおかげでさらに結合されています)。他のコンポーネントとの依存関係がまったくなく、コンポーネントを分離して作成できるようにしたいそのため、データが変更されたときにコンポーネントにイベントを発生させ(コンポーネントに固有)、基本的にデータを変更するメソッドを宣言しました。次に、ゲームオブジェクトでクラスを作成し、すべてのコンポーネントイベントを他のコンポーネントメソッドに接着しました。

私がこれについて気に入っているのは、ゲームオブジェクトのコンポーネントのすべての相互作用を確認するために、この1つのクラスだけを見ることができるということです。 Contactクラスは私のゲームオブジェクトクラスとよく似ているようです(MainPlayer、Orcなどのオブジェクトにゲームオブジェクトの名前を付けます)。

これらのクラスは、一種のマネージャークラスです。コンポーネント自体には、コンポーネントのインスタンスとそれらを接続するコード以外は何もありません。ここで、直接接続できる他のコンポーネントメソッドを呼び出すだけのメソッドを作成する理由がわかりません。このクラスのポイントは、実際にはイベントのフックアップを整理することだけです。

イベント登録の補足として、filterコールバックとargsコールバックを追加しました。イベントがトリガーされると(私は独自のカスタムイベントクラスを作成しました)、フィルターコールバックが存在する場合はコールバックし、trueを返す場合はargsコールバックに移動します。フィルターコールバックのポイントは、柔軟性を与えることでした。イベントはさまざまな理由でトリガーされる可能性がありますが、チェックがtrueの場合にのみフックされたイベントを呼び出します。例として、OnKeyHitイベントを持つ入力コンポーネントがあります。 MoveForward()MoveBackward()などのメソッドを持つMovementコンポーネントがある場合、OnKeyHit + = MoveForwardをフックできますが、キーを押して前方に移動したくありません。キーが「w」キーの場合にのみ、それを実行します。 OnKeyHitは渡される引数を入力していて、そのうちの1つがヒットしたキーであるため、フィルターコールバック内でそれを確認し、「w」がtrueを返す場合はfalseを返します。

私にとって、特定のゲームオブジェクトマネージャークラスのサブスクリプションは次のようになります。

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

コンポーネントは分離して開発できるため、複数のプログラマーがそれらをコーディングすることができます。上記の例では、入力コーダーは引数オブジェクトにKeyという名前の変数を与えました。ただし、Movementコンポーネントの開発者はKeyを使用していなかった可能性があります(引数を調べる必要がある場合、この場合はおそらくそうではありませんが、渡された引数の値を使用します)。この通信要件を削除するために、argsコールバックはコンポーネント間の引数のマッピングとして機能します。したがって、このゲームオブジェクトマネージャークラスを作成する人は、2つのクライアントを接続してその時点でマッピングを実行するときに、2つのクライアント間のarg変数名を知る必要があるだけです。このメソッドは、フィルター関数の後に呼び出されます。

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

したがって、上記の状況では、入力担当者はargsオブジェクト内の変数に「Key」という名前を付けましたが、Movementでは「keyPressed」という名前にしました。これは、コンポーネントが開発されているときにコンポーネント自体をさらに分離し、マネージャークラスの実装者に配置して、正しく接続するのに役立ちます。

1
user441521

価値があることについて、私はバックエンドプロジェクトの一部として何かをしており、同様のアプローチをとっています。

  • 私のシステムにはウィジェット(Webコンポーネント)は含まれていませんが、抽象的な「アダプター」、つまりさまざまなプロトコルを処理する具体的な実装が含まれています。
  • プロトコルは、可能な「会話」のセットとしてモデル化されます。プロトコルアダプターは、着信イベントに応じてこれらの会話をトリガーします。
  • 基本的にRxサブジェクトであるイベントバスがあります。
  • イベントバスはすべてのアダプターの出力をサブスクライブし、すべてのアダプターはイベントバスの出力をサブスクライブします。
  • 「アダプター」は、そのすべての「会話」の集約ストリームとしてモデル化されます。会話は、イベントバスの出力にサブスクライブされたストリームであり、ステートマシンによって駆動されるメッセージをイベントバスに生成します。

あなたの建設/配線の課題への対処方法:

  • プロトコル(アダプターによって実装される)は、サブスクライブする入力ストリームに対するフィルターとして会話開始基準を定義します。 C#では、これらはストリームに対するLINQクエリです。 ReactJSでは、これらは.Whereまたは.Filter演算子になります。
  • 会話は、独自のフィルターを使用して関連メッセージを決定します。
  • 一般に、バスにサブスクライブされるものはすべてストリームであり、バスはそれらのストリームにサブスクライブされます。

ツールバーとの類似性:

  • ツールバークラスは、バスがサブスクライブされているツールバーイベントの監視可能な入力監視可能な(バス)の.Mapです。
  • ツールバーのオブザーバブル(複数のサブツールバーがある場合)は、複数のオブザーバブルがある可能性があるため、ツールバーはオブザーバブルのオブザーバブルです。これらはRxJであり、バスへの単一の出力にマージされます。

直面する可能性のある問題:

  • イベントが循環的ではなく、プロセスがハングすることを確認します。
  • 同時実行性(これがWebComponentsに関連するかどうかわからない):非同期操作または長時間実行される可能性のある操作の場合、バックグラウンドタスクとして実行されない場合、イベントハンドラーが監視可能なスレッドをブロックすることがあります。 RxJSスケジューラーはこれに対処できます(たとえば、デフォルトでは、すべてのバスサブスクリプションのデフォルトスケジューラーを.ObserveOnできます)。
  • 会話の概念がないとモデル化できない、より複雑なシナリオ(例:メッセージを送信してイベントを処理し、それ自体がイベントである応答を待つ)。この場合、ステートマシンは、処理するイベント(モデル内の会話)を動的に指定するのに役立ちます。私はこれを会話ストリーム.filter状態に応じて(実際には、実装はより機能的です。会話は、状態変化イベントのオブザーバブルからのオブザーバブルのフラットマップです)。

したがって、要約すると、問題のドメイン全体をオブザーバブルとして、または「機能的に」/「宣言的に」見て、バス(オブザーバブル)から派生したバス(オブザーバブル)から、イベントコンポーネントとしてオブザーバーとしてWebコンポーネントを検討できます。オブザーバー)も購読しています。オブザーバブルのインスタンス化(例:新しいツールバー)は宣言的であり、プロセス全体をオブザーバブルのオブザーバブルと見なすことができます.map 'd入力ストリームから。

0
Sentinel