10 Codezeilen, um die Schmerzen Ihres Vue-Projekts zu verringern

... oder Vertrautheit mit Vue JS-Plugins als Beispiel für einen integrierten Ereignisbus


Ein paar Worte über ...


Hallo alle zusammen! Ich werde sofort eine Reservierung vornehmen. Ich liebe VueJS wirklich, ich schreibe seit mehr als 2 Jahren aktiv darüber und ich glaube nicht, dass die Entwicklung zumindest in erheblichem Maße schaden kann :)
Andererseits versuchen wir immer, universelle Lösungen zu finden, die dazu beitragen, weniger Zeit für mechanische Arbeiten und mehr für das zu verwenden, was wirklich interessant ist. Manchmal ist die Lösung besonders erfolgreich. Eines davon möchte ich mit Ihnen teilen. Die 10 Zeilen, die besprochen werden (Spoiler: am Ende wird es etwas mehr geben), wurden bei der Arbeit am Cloud Blue - Connect-Projekt geboren, einer ziemlich großen Anwendung mit mehr als 400 Komponenten. Die gefundene Lösung ist bereits in verschiedene Punkte des Systems integriert und erfordert seit mehr als einem halben Jahr keine Korrekturen, sodass sie sicher als erfolgreich auf Stabilität getestet werden kann.

Und der Letzte. Bevor ich direkt zur Lösung übergehe, möchte ich noch etwas näher auf die Beschreibung der drei Arten der Interaktion zwischen Vue-Komponenten untereinander eingehen: die Prinzipien des unidirektionalen Flusses, das Muster des Speichers und des Ereignisbusses. Wenn diese Erklärung für Sie unnötig (oder langweilig) ist, gehen Sie direkt zum Abschnitt mit der Lösung - alles ist so kurz und technisch wie möglich.

Ein bisschen darüber, wie Vue-Komponenten miteinander kommunizieren


Vielleicht ist die erste Frage, die eine Person, die ihre erste Komponente geschrieben hat, aufwirft, wie sie Daten für die Arbeit erhält und wie sie wiederum die Daten überträgt, die sie "out" erhalten hat. Das im Vue JS-Framework verwendete Interaktionsprinzip heißt ...

Unidirektionaler Datenstrom


Kurz gesagt, dieses Prinzip klingt wie "Eigenschaften - unten, Ereignisse - oben". Das heißt, um Daten von außen ("von oben") zu empfangen, registrieren wir eine spezielle Eigenschaft innerhalb der Komponente, in die das Framework bei Bedarf unsere "von außen" empfangenen Daten schreibt. Um Daten "up" innerhalb der Komponente an der richtigen Stelle zu übertragen, rufen wir die spezielle $ emit-Framework-Methode auf, die unsere Daten an den Handler der übergeordneten Komponente weitergibt. Gleichzeitig können wir in Vue JS das Ereignis nicht einfach bis zu einer unbegrenzten Tiefe „übertragen“ (wie zum Beispiel in Angular 1.x). Es "erscheint" nur eine Ebene für den unmittelbaren Elternteil. Gleiches gilt für Veranstaltungen. Um sie auf die nächste Ebene zu übertragen, müssen Sie für jede von ihnen auch eine spezielle Schnittstelle registrieren - Eigenschaften und Ereignisse, die unsere „Nachricht“ weiterleiten.

Dies kann als ein Bürogebäude beschrieben werden, in dem Arbeiter nur von ihren Stockwerken in die benachbarten ziehen können - eins nach oben und eins nach unten. Um das „Dokument zur Unterschrift“ vom fünften in den zweiten Stock zu übertragen, ist eine Kette von drei Arbeitern erforderlich, die es vom fünften in den zweiten Stock liefern, und drei weitere, die es in den fünften zurückliefern.

"Aber das ist unpraktisch!" Natürlich ist dies aus entwicklungspolitischer Sicht nicht immer praktisch, aber wenn wir uns den Code jeder Komponente ansehen, können wir sehen, was und an wen sie weitergegeben wird. Wir müssen nicht die gesamte Struktur der Anwendung berücksichtigen, um zu verstehen, ob unsere Komponente auf dem Weg zur Veranstaltung ist oder nicht. Wir können dies an der übergeordneten Komponente sehen.

Obwohl die Vorteile dieses Ansatzes verständlich sind, weist er auch offensichtliche Nachteile auf, nämlich die hohe Kohäsion der Komponenten. Einfach ausgedrückt, damit wir eine Komponente in die Struktur einfügen können, müssen wir sie mit den erforderlichen Schnittstellen überlagern, um ihren Status zu verwalten. Um diese Konnektivität zu verringern, verwenden sie häufig „State Management Tools“. Das vielleicht beliebteste Tool für Vue ist ...

Vuex (Seite)


Vuex Stor setzt unsere Analogie zu einem Bürogebäude fort und ist ein interner Postdienst. Stellen Sie sich vor, auf jeder Etage des Büros befindet sich ein Fenster zum Ausstellen und Empfangen von Paketen. Im fünften Stock übertragen sie das Dokument Nr. 11 zur Unterschrift und im zweiten fragen sie regelmäßig: „Gibt es Dokumente zur Unterschrift?“, Unterschreiben Sie die vorhandenen und geben Sie sie zurück. Beim fünften fragen sie auch: "Gibt es Unterzeichner?" Gleichzeitig können Mitarbeiter in andere Stockwerke oder in andere Räume umziehen - das Arbeitsprinzip ändert sich nicht, während die Post arbeitet.

Ungefähr nach diesem Prinzip funktioniert auch das Muster Store. Über die Vuex-Schnittstelle wird ein globales Data Warehouse registriert und konfiguriert, und Komponenten abonnieren es. Und es spielt keine Rolle, auf welcher Ebene in welcher Struktur der Einspruch stattgefunden hat, das Geschäft gibt immer die richtigen Informationen.

Es scheint, dass damit alle Probleme bereits gelöst sind. Aber irgendwann in unserem metaphorischen Gebäude möchte ein Mitarbeiter einen anderen zum Mittagessen anrufen ... oder einen Fehler melden. Und hier beginnt das Seltsame. Die Nachricht selbst erfordert keine Übertragung als solche. Aber um die Mail nutzen zu können, müssen Sie etwas übertragen. Dann kommen unsere Mitarbeiter mit einem Code. Ein grüner Ball - zum Mittagessen gehen, zwei rote Würfel - ein Anwendungsfehler E-981273 ist aufgetreten, drei gelbe Münzen - überprüfen Sie Ihre Post und so weiter.

Es ist leicht zu erraten, dass ich mit Hilfe dieser umständlichen Metapher Situationen beschreibe, in denen wir sicherstellen müssen, dass unsere Komponente auf ein Ereignis reagiert, das in einer anderen Komponente aufgetreten ist, die an sich in keiner Weise mit dem Datenstrom verbunden ist. Das Speichern eines neuen Elements ist abgeschlossen - Sie müssen die Sammlung erneut durchführen. Es ist ein nicht autorisierter 403-Fehler aufgetreten. Sie müssen eine Benutzerabmeldung starten und so weiter. Die übliche (und bei weitem nicht die beste) Vorgehensweise besteht in diesem Fall darin, Flags im Geschäft zu erstellen oder die gespeicherten Daten und ihre Änderungen indirekt zu interpretieren. Dies führt schnell zu einer Verschmutzung sowohl des Geschäfts selbst als auch der Logik der umgebenden Komponenten.

In dieser Phase beginnen wir darüber nachzudenken, wie Ereignisse direkt unter Umgehung der gesamten Komponentenkette übergeben werden können. Und ein wenig googeln oder in der Dokumentation stöbern, stoßen wir auf ein Muster ...

Ereignisbus


Aus technischer Sicht ist der Ereignisbus ein Objekt, mit dem mit einer speziellen Methode ein „Ereignis“ gestartet und mit einer anderen abonniert werden kann. Mit anderen Worten, wenn Sie sich für das eventA-Ereignis anmelden, speichert dieses Objekt die übergebene Handlerfunktion in seiner Struktur, die es aufruft, wenn die Startmethode mit dem eventA-Schlüssel irgendwo in der Anwendung aufgerufen wird. Das Signieren oder Ausführen reicht aus, um durch Import oder Referenz darauf zuzugreifen, und fertig.

Metaphorisch gesehen ist in unserem „Gebäude“ ein Bus ein häufiger Chat im Messenger. Komponenten abonnieren einen "allgemeinen Chat", an den andere Komponenten Nachrichten senden. Sobald im "Chat", den die Komponente abonniert hat, eine "Nachricht" angezeigt wird, startet der Handler.

Es gibt viele verschiedene Möglichkeiten, einen Ereignisbus zu erstellen. Sie können es selbst schreiben oder vorgefertigte Lösungen verwenden - denselben RxJS, der eine enorme Funktionalität für die Arbeit mit ganzen Ereignisströmen bietet. Aber meistens verwenden sie bei der Arbeit mit VueJS seltsamerweise VueJS selbst. Die über den Konstruktor (new Vue ()) erstellte Vue-Instanz bietet eine schöne und übersichtliche Ereignisoberfläche, die in der offiziellen Dokumentation beschrieben wird.

Hier kommen wir der nächsten Frage nahe ...

Was wollen wir?


Und wir möchten in unserer Anwendung einen Ereignisbus erstellen. Wir haben jedoch zwei zusätzliche Anforderungen:

  1. Es sollte in jeder Komponente leicht zugänglich sein. Separate Importe in jede der Dutzenden von Komponenten erscheinen uns überflüssig.
  2. Es muss modular sein. Wir möchten nicht alle Ereignisnamen berücksichtigen, um die Situation zu vermeiden, in der das Ereignis "Element erstellt" Handler aus der gesamten Anwendung auslöst. Daher möchten wir in der Lage sein, ein kleines Fragment des Komponentenbaums einfach in ein separates Modul zu trennen und seine Ereignisse innerhalb und nicht außerhalb zu übertragen.

Um diese beeindruckende Funktionalität zu implementieren, verwenden wir die leistungsstarke Plug-In-Schnittstelle, die uns VueJS zur Verfügung stellt. Sie können sich hier auf der Seite mit der offiziellen Dokumentation ausführlicher damit vertraut machen .

Registrieren wir zuerst unser Plugin. Dazu platzieren wir unmittelbar vor dem Initialisierungspunkt unserer Vue-Anwendung (vor dem Aufruf von Vue. $ Mount ()) den folgenden Block:

Vue.use({   
  install(vue) { }, 
});

Tatsächlich sind Vue-Plugins eine Möglichkeit, die Funktionalität des Frameworks auf der gesamten Anwendungsebene zu erweitern. Die Plugin-Schnittstelle bietet verschiedene Möglichkeiten zur Integration in die Komponente. Heute werden wir jedoch die Mixin-Schnittstelle vorstellen. Diese Methode akzeptiert ein Objekt, das den Deskriptor jeder Komponente erweitert, bevor der Lebenszyklus in der Anwendung gestartet wird.(Der Komponentencode, den wir schreiben, ist eher nicht die Komponente selbst, sondern eine Beschreibung ihres Verhaltens und der Kapselung eines bestimmten Teils der Logik, die das Framework in verschiedenen Phasen seines Lebenszyklus verwendet. Die Plug-In-Initialisierung liegt außerhalb des Komponentenlebenszyklus und liegt daher vor uns Wir sagen "Deskriptor", keine Komponente, um zu betonen, dass genau der Code, der in unserer Datei geschrieben ist, und nicht irgendeine Entität, die ein Produkt der Arbeit des Frameworks ist, in den Mixin-Abschnitt des Plugins übertragen wird .

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

Dieses leere Objekt enthält die Erweiterungen für unsere Komponenten. Aber für den Anfang noch eine Station. In unserem Fall möchten wir eine Schnittstelle für den Zugriff auf den Bus auf der Ebene jeder Komponente erstellen. Fügen wir unserem Deskriptor das Feld '$ Broadcast' hinzu, in dem ein Link zu unserem Bus gespeichert wird. Verwenden Sie dazu Vue.prototype:

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

Jetzt müssen wir den Bus selbst erstellen, aber zuerst wollen wir uns an die Modularitätsanforderung erinnern und davon ausgehen, dass wir im Komponentendeskriptor das neue Modul mit dem Feld "$ module" mit einem Textwert deklarieren (wir werden es etwas später benötigen). Wenn das Feld $ module in der Komponente selbst angegeben ist, erstellen wir einen neuen Bus dafür. Wenn nicht, übergeben wir den Link über das Feld $ parent an das übergeordnete Element. Beachten Sie, dass uns die Deskriptorfelder über das Feld $ options zur Verfügung stehen.

Wir werden die Erstellung unseres Busses zum frühestmöglichen Zeitpunkt platzieren - im beforeCreate-Hook.

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

Zum Schluss füllen wir die logischen Zweige aus. Wenn der Deskriptor eine neue Moduldeklaration enthält, erstellen Sie eine neue Businstanz. Wenn nicht, nehmen Sie den Link von $ parent.

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

Wir verwerfen die Ankündigung des Plugins und betrachten ... 1, 2, 3, 4 ... 10 Zeilen, wie ich versprochen habe!

Können wir es besser machen?


Klar können wir. Dieser Code ist leicht erweiterbar. In unserem Fall haben wir beispielsweise zusätzlich zu $ ​​Broadcast beschlossen, die $ rootBroadcast-Schnittstelle hinzuzufügen, die den Zugriff auf einen einzelnen Bus für die gesamte Anwendung ermöglicht. Ereignisse, die der Benutzer auf dem $ Broadcast-Bus ausführt, werden auf dem $ rootBroadcast-Bus dupliziert, sodass Sie entweder alle Ereignisse eines bestimmten Moduls (in diesem Fall wird der Ereignisname als erstes Argument an den Handler übergeben) oder alle Anwendungsereignisse im Allgemeinen (dann) abonnieren können Der Modulname wird mit dem ersten Argument an den Handler übergeben, der Ereignisname mit dem zweiten, und die mit dem Ereignis übertragenen Daten werden mit den folgenden Argumenten übergeben. Dieses Design ermöglicht es uns, die Interaktion zwischen den Modulen herzustellen und einen einzelnen Handler an die Ereignisse verschiedener Module zu hängen.

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

Mal sehen, wie wir dies erreichen können:

Erstellen Sie zunächst einen einzelnen Bus, der über $ rootBroadcast organisiert wird, und das Feld selbst mit einem Link:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Jetzt benötigen wir eine Modulmitgliedschaft in jeder Komponente. Erweitern wir also die Definition der Modularität wie folgt:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

Als nächstes müssen wir das Ereignis auf dem modularen lokalen Bus so reflektieren, wie wir es zur Wurzel benötigen. Dazu müssen wir zunächst eine einfache Proxy-Schnittstelle erstellen und den Bus selbst in das bedingt private Eigentum von $ bus stellen:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

Fügen Sie dem Objekt schließlich Proxy-Methoden hinzu, da das Feld $ Broadcast jetzt keinen direkten Zugriff auf den Bus bietet:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

Lassen Sie uns abschließend daran denken, dass wir durch Schließen Zugriff auf den Bus erhalten. Dies bedeutet, dass die einmal hinzugefügten Handler nicht mit der Komponente gelöscht werden, sondern während der gesamten Zeit der Arbeit mit der Anwendung aktiv sind. Dies kann zu unangenehmen Nebenwirkungen führen. Fügen Sie unserem Bus am Ende des Lebenszyklus der Komponente eine Listener-Bereinigungsfunktion hinzu:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

Daher bietet diese Option eine interessantere Funktionalität, wenn auch weniger präzise. Damit können Sie ein komplettes System alternativer Kommunikation zwischen Komponenten implementieren. Darüber hinaus ist er vollständig unter unserer Kontrolle und bringt keine externen Abhängigkeiten in unser Projekt ein.

Ich hoffe, dass Sie nach dem Lesen Ihr Wissen über Vue-Plugins erworben oder aktualisiert haben und das nächste Mal, wenn Sie Ihrer Anwendung einige allgemeine Funktionen hinzufügen müssen, diese effizienter implementieren können - ohne externe Abhängigkeiten hinzuzufügen.

All Articles