Comunicazione tra componenti disaccoppiati tramite eventi

8

Abbiamo un'app Web in cui abbiamo molto (> 50) di piccoli WebComponents che interagiscono tra loro.

Per mantenere tutto disaccoppiato, abbiamo di regola che nessun componente può fare riferimento direttamente a un altro. Invece, i componenti sparano eventi che sono poi (nell'app "principale") cablati per chiamare i metodi di un altro componente.

Con il passare del tempo, sempre più componenti sono stati aggiunti e il file dell'app "principale" è stato disseminato di blocchi di codice con questo aspetto:

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

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

// ... etc

Per migliorare questo abbiamo raggruppato funzionalità simili insieme, in un Class , chiamiamolo qualcosa di rilevante, passare gli elementi partecipanti durante l'istanziazione e gestire il "cablaggio" all'interno di Class , in questo modo:

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()
  }
}

e istanziamo in questo modo:

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

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

Sono molto stanco di introdurre dei pattern personali, specialmente quelli che non sono realmente OOP-y dal momento che sto usando Classes come semplici contenitori di inizializzazione, per mancanza di una parola migliore.

C'è un modello definito migliore / più noto per la gestione di questo tipo di attività che mi manca?

    
posta Nik Kyriakides 24.04.2018 - 19:03
fonte

3 risposte

6

Il codice che hai è abbastanza buono. La cosa che sembra un po 'scoraggiante è che il codice di inizializzazione non fa parte dell'oggetto stesso. Cioè, puoi istanziare un oggetto, ma se ti dimentichi di chiamare la sua classe di cablaggio, è inutile.

Considera un Centro di notifica (alias Event Bus) definito qualcosa come questo:

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)
            }
        }
    }
}

Questo è un gestore di eventi multi-dispatch fai-da-te. Sarai quindi in grado di eseguire il tuo cablaggio semplicemente richiedendo un NotificationCenter come argomento del costruttore. L'invio di messaggi in esso e in attesa che passi i payload è l'unico contatto che hai con il sistema, quindi è molto SOLIDO.

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)
    }
}

Nota: ho usato valori letterali stringa sul posto per le chiavi per essere coerenti con lo stile utilizzato nella domanda e per semplicità. Questo non è consigliabile a causa del rischio di errori di battitura. Invece, considera l'utilizzo di un'enumerazione o di costanti di stringa.

Nel codice sopra riportato, la barra degli strumenti ha la responsabilità di consentire a NotificationCenter di conoscere il tipo di eventi a cui è interessato e di pubblicare tutte le sue interazioni esterne tramite il metodo di notifica. Qualsiasi altra classe interessata al toolbar-button-click-event si registrerebbe semplicemente nel suo costruttore.

Variazioni interessanti su questo modello includono:

  • Utilizzo di più NC per gestire diverse parti del sistema
  • Avere il metodo Notifica genera un thread per ogni notifica, piuttosto che bloccare in serie
  • Uso di un elenco di priorità anziché di un elenco regolare all'interno dell'NC per garantire un ordinamento parziale su quali componenti vengono notificati per primi
  • Registrati per restituire un ID che può essere utilizzato per annullare la registrazione in seguito
  • Salta l'argomento del messaggio e invia solo in base alla classe / tipo del messaggio

Le caratteristiche interessanti includono:

  • Strumentare l'NC è facile come registrare i logger per stampare i payload
  • Testare uno o più componenti che interagiscono è semplicemente questione di istanziarli, aggiungere listener per i risultati previsti e inviare messaggi
  • L'aggiunta di nuovi componenti in ascolto di vecchi messaggi è banale
  • L'aggiunta di nuovi componenti che inviano messaggi a quelli vecchi è banale

Tratti interessanti e possibili rimedi includono:

  • Gli eventi che attivano altri eventi possono generare confusione.
    • Includi un ID mittente nell'evento per individuare l'origine di un evento imprevisto.
  • Ogni componente non ha idea se una determinata parte del sistema sia attiva e funzionante prima di ricevere un evento, quindi i messaggi iniziali potrebbero essere eliminati.
    • Questo può essere gestito dal codice che crea i componenti inviando un messaggio "sistema pronto", che ovviamente i componenti interessati dovrebbero registrare.
  • Il bus eventi crea un'interfaccia implicita tra i componenti, il che significa che non è possibile per il compilatore assicurarsi di aver implementato tutto ciò che si dovrebbe.
    • Gli argomenti standard tra statico e dinamico si applicano qui.
  • Questo approccio raggruppa le componenti, non necessariamente il comportamento. Tracciare gli eventi attraverso il sistema potrebbe richiedere più lavoro rispetto all'approccio dell'OP. Ad esempio, OP potrebbe avere tutti gli ascoltatori collegati al risparmio impostati insieme e gli ascoltatori correlati alla cancellazione impostati insieme altrove.
    • Questo può essere mitigato con una buona denominazione degli eventi e documentazione come un diagramma di flusso. (Sì, la documentazione è notoriamente fuori fase rispetto al codice). Puoi anche aggiungere elenchi di gestori pre- e post- catchall che ottengono tutti i messaggi e stampano chi ha inviato in quale ordine.
risposta data 01.05.2018 - 13:49
fonte
3

Avevo introdotto un "event bus" di qualche tipo, e negli anni successivi ho iniziato a fare affidamento sempre di più sul modello di oggetti documento per comunicare eventi per codice UI.

In un browser, il DOM è l'unica dipendenza sempre presente, anche durante il caricamento della pagina. La chiave sta utilizzando Eventi personalizzati in JavaScript e basandosi sul bubbling degli eventi per comunicare quegli eventi.

Prima che le persone inizino a gridare "aspettando che il documento sia pronto" prima di collegare i sottoscrittori, la proprietà document.documentElement fa riferimento all'elemento <html> dal momento in cui JavaScript inizia l'esecuzione, indipendentemente da dove è stato importato lo script o dall'ordine in che appare nel tuo markup.

Qui puoi iniziare ad ascoltare gli eventi.

È molto comune vivere un componente (o un widget) JavaScript all'interno di un determinato tag HTML sulla pagina. L'elemento "root" del componente è dove puoi attivare i tuoi eventi di bubbling. Gli iscritti sull'elemento <html> riceveranno queste notifiche proprio come qualsiasi altro evento generato dall'utente.

Solo alcuni esempi di codice piastra della caldaia:

(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);

Quindi il modello diventa:

  1. Utilizza eventi personalizzati
  2. Iscriviti a questi eventi sulla proprietà document.documentElement (non è necessario attendere che il documento sia pronto)
  3. Pubblica eventi su un elemento principale per il tuo componente o document.documentElement .

Questo dovrebbe funzionare sia per basi di codice funzionali e orientate agli oggetti.

    
risposta data 01.05.2018 - 17:48
fonte
0

Per quello che vale, sto facendo qualcosa come parte di un progetto di back-end e ho adottato un approccio simile:

  • Il mio sistema non include widget (componenti web) ma "adattatori" astratti, implementazioni concrete di cui gestire protocolli diversi.
  • Un protocollo è modellato come un insieme di possibili "conversazioni". L'adattatore di protocollo attiva queste conversazioni a seconda di un evento in arrivo.
  • Esiste un bus eventi che è fondamentalmente un oggetto Rx.
  • Il bus eventi si abbona all'output di tutti gli adattatori e tutti gli adattatori si abbonano all'output del bus eventi.
  • L''adattatore' è modellato come flusso aggregato di tutte le sue 'conversazioni'. Una conversazione è un flusso sottoscritto all'output del bus eventi, che genera messaggi sul bus eventi, pilotato da una Macchina a stati.

Come ho gestito le tue sfide di costruzione / cablaggio:

  • Un protocollo (implementato dall'adattatore) definisce l'avvio della conversazione criteri come filtri sul flusso di input a cui è iscritto. In C # queste sono query LINQ su flussi. In ReactJS questi sarebbero. Dove o. Operatori .Filter.
  • Una conversazione decide che cosa è un messaggio rilevante utilizzando i propri filtri.
  • In generale, qualsiasi cosa sottoscritta al bus è un flusso e il bus è sottoscritto a tali flussi.

L'analogia con la barra degli strumenti:

  • La classe della barra degli strumenti è una .Map di un input osservabile (il bus), che è un osservabile degli eventi della barra degli strumenti, a cui è abbonato il bus
  • Un osservabile di barre degli strumenti (se hai più barre secondarie) significa che puoi avere più osservabili, quindi la tua barra degli strumenti è osservabile. Questi sarebbero RxJ. Immessi in una singola uscita sul bus.

Problemi che potresti incontrare:

  • Garantire che gli eventi non siano ciclici e sospendere il processo.
  • Concorrenza (non so se questo è rilevante per WebComponents): per operazioni asincrone o operazioni che possono essere in esecuzione a lungo termine, il gestore di eventi potrebbe bloccare il thread osservabile se non viene eseguito come attività in background. Gli scheduler RxJS possono indirizzarlo (per impostazione predefinita è possibile .ObserveOn un programma di pianificazione predefinito per tutte le sottoscrizioni dei bus, ad esempio)
  • Scenari più complessi che non possono essere modellati senza alcuna nozione di conversazione (ad esempio: gestire un evento inviando un messaggio e aspettando una risposta, che è di per sé un evento). In questo caso, una macchina a stati è utile per specificare in modo dinamico quali eventi si desidera gestire (conversazione nel mio modello). Lo faccio avendo lo stream di conversazione .filter in base allo stato (in realtà, l'implementazione è più funzionale - la conversazione è una mappa piatta di oggetti osservabili da un osservabile di eventi di cambiamento di stato).

Quindi, in sintesi, puoi considerare l'intero dominio del problema come osservabile, o 'funzionalmente' / 'dichiarativamente' e considerare i tuoi webcomponents come flussi di eventi, come osservabili, derivati dal bus (un osservabile), a cui il anche il bus (un osservatore) è iscritto. L'istanziazione di oggetti osservabili (ad esempio una nuova barra degli strumenti) è dichiarativa, in quanto l'intero processo può essere visto come osservabile di% osservabili% d_de% 'd dal flusso di input.

    
risposta data 03.05.2018 - 11:37
fonte