10 lignes de code pour réduire la douleur de votre projet Vue

... ou familiarité avec les plugins Vue JS comme exemple de bus d'événements intégré


Quelques mots sur ...


Bonjour à tous! Je ferai une réservation tout de suite. J'adore vraiment VueJS, j'écris activement dessus depuis plus de 2 ans et je ne pense pas que son développement puisse faire mal au moins dans une certaine mesure :)
D'un autre côté, nous essayons toujours de trouver des solutions universelles qui aideront à passer moins de temps sur les travaux mécaniques et plus sur ce qui est vraiment intéressant. Parfois, la solution est particulièrement réussie. Je veux partager avec vous l'un d'entre eux. Les 10 lignes qui seront discutées (spoiler: à la fin il y en aura un peu plus) sont nées dans le processus de travail sur le projet Cloud Blue - Connect, qui est une application assez grande avec plus de 400 composants. La solution que nous avons trouvée est déjà intégrée à divers points du système et pendant plus de six mois, elle n'a jamais nécessité de corrections, elle peut donc être considérée avec succès comme testée avec succès pour la stabilité.

Et la dernière. Avant de passer directement à la solution, je voudrais m'attarder un peu plus sur la description des trois types d'interaction entre les composants Vue entre eux: les principes du flux unidirectionnel, le modèle du magasin et le bus d'événements. Si cette explication est inutile (ou ennuyeuse) pour vous, allez directement à la section avec la solution - tout est aussi bref et technique que possible.

Un peu sur la façon dont les composants Vue communiquent entre eux


Peut-être que la première question qu'une personne qui a rédigé son premier volet soulève est de savoir comment il recevra les données pour le travail et comment, à son tour, il transmettra les données qu'il a reçues «à l'extérieur». Le principe d'interaction adopté dans le framework Vue JS s'appelle ...

Flux de données unidirectionnel


En bref, ce principe sonne comme «propriétés - vers le bas, événements - vers le haut». Autrement dit, pour recevoir des données de l'extérieur («d'en haut»), nous enregistrons une propriété spéciale à l'intérieur du composant dans lequel le framework écrit, si nécessaire, nos données reçues «de l'extérieur». Afin de transférer les données «vers le haut», à l'intérieur du composant au bon endroit, nous appelons la méthode spéciale $ emit framework, qui transmet nos données au gestionnaire du composant parent. Dans le même temps, dans Vue JS, nous ne pouvons pas simplement «diffuser» l'événement jusqu'à une profondeur illimitée (comme par exemple dans Angular 1.x). Il "n'apparaît" qu'un seul niveau, au parent immédiat. Il en va de même pour les événements. Pour les transférer au niveau suivant, pour chacun d'eux, vous devez également enregistrer une interface spéciale - les propriétés et les événements qui transmettront notre «message» plus loin.

Cela peut être décrit comme un immeuble de bureaux dans lequel les travailleurs ne peuvent se déplacer que de leurs étages vers les étages voisins - un de haut en bas. Ainsi, afin de transférer le «document à signer» du cinquième étage au deuxième, une chaîne de trois travailleurs sera requise qui le livrera du cinquième étage au deuxième, puis trois autres qui le remettront au cinquième.

"Mais ce n'est pas pratique!" Bien sûr, cela n'est pas toujours pratique du point de vue du développement, mais en regardant le code de chaque composant, nous pouvons voir ce qu'il transmet et à qui il passe. Nous n'avons pas besoin de garder à l'esprit toute la structure de l'application pour comprendre si notre composant est «en route» vers l'événement ou non. Nous pouvons voir cela à partir du composant parent.

Bien que les avantages de cette approche soient compréhensibles, elle présente également des inconvénients évidents, à savoir la forte cohésion des composants. Autrement dit, pour que nous puissions placer un composant dans la structure, nous devons le superposer avec les interfaces nécessaires afin de gérer son état. Afin de réduire cette connectivité, ils utilisent souvent des «outils de gestion des états». L'outil le plus populaire pour Vue est peut-être ...

Vuex (côté)


Poursuivant notre analogie avec un immeuble de bureaux, Vuex Stor est un service postal interne. Imaginez qu'à chaque étage du bureau il y ait une fenêtre pour émettre et recevoir des colis. Au cinquième étage, ils transfèrent le document n ° 11 pour signature, et au deuxième ils demandent périodiquement: "Y a-t-il des documents à signer?", Signez les documents existants et restituez-les. Sur le cinquième, ils demandent également: "Y a-t-il des signataires?" Dans le même temps, les employés peuvent déménager à d'autres étages ou dans d'autres pièces - le principe du travail ne changera pas pendant le travail du courrier.

Environ par ce principe, le modèle appelé Store fonctionne également. À l'aide de l'interface Vuex, un entrepôt de données global est enregistré et configuré, et les composants s'y abonnent. Et peu importe le niveau de la structure de l'appel, le magasin fournira toujours les bonnes informations.

Il semblerait que tous les problèmes aient déjà été résolus. Mais à un moment donné dans notre bâtiment métaphorique, un employé veut appeler un autre pour le déjeuner ... ou signaler une sorte d'erreur. Et ici commence l'étrange. Le message lui-même ne nécessite pas de transmission en tant que tel. Mais pour utiliser le courrier, vous devez transférer quelque chose. Ensuite, nos employés élaborent un code. Une balle verte - allez déjeuner, deux cubes rouges - une erreur d'application E-981273 s'est produite, trois pièces jaunes - vérifiez votre courrier et ainsi de suite.

Il est facile de deviner qu'avec l'aide de cette métaphore maladroite, je décris des situations où nous devons assurer la réponse de notre composant à un événement qui s'est produit dans un autre composant, qui en soi n'est en aucun cas lié au flux de données. L'enregistrement d'un nouvel élément est terminé - vous devez reprendre la collection. Une erreur 403 non autorisée s'est produite - vous devez démarrer une déconnexion utilisateur, etc. La pratique habituelle (et loin d'être la meilleure) dans ce cas est de créer des indicateurs à l'intérieur du magasin ou d'interpréter indirectement les données stockées et leurs modifications. Cela entraîne rapidement une pollution à la fois du magasin lui-même et de la logique des composants qui l'entourent.

À ce stade, nous commençons à réfléchir à la façon de passer directement les événements, en contournant toute la chaîne des composants. Et, un peu sur Google ou en fouillant dans la documentation, on tombe sur un schéma ...

Bus d'événement


D'un point de vue technique, le bus d'événements est un objet qui permet d'utiliser une méthode spéciale pour lancer un «événement» et s'y abonner en utilisant une autre. En d'autres termes, lors de l'abonnement à l'événement eventA, cet objet stocke la fonction de gestionnaire transmis dans sa structure, qu'il appellera lorsque la méthode de lancement avec la clé eventA est appelée quelque part dans l'application. Pour signer ou exécuter, il suffit d'y accéder via l'importation ou par référence, et vous avez terminé.

Métaphoriquement, dans notre «bâtiment», un bus est une conversation courante dans le messager. Les composants s'abonnent à une «conversation générale» à laquelle d'autres composants envoient des messages. Dès qu'un «message» apparaît sur le «chat» auquel le composant s'est abonné, le gestionnaire démarre.

Il existe différentes manières de créer un bus d'événements. Vous pouvez l'écrire vous-même ou vous pouvez utiliser des solutions prêtes à l'emploi - le même RxJS, qui offre une énorme fonctionnalité pour travailler avec des flux entiers d'événements. Mais le plus souvent, lorsqu'ils utilisent VueJS, ils utilisent, curieusement, VueJS lui-même. L'instance Vue créée via le constructeur (new Vue ()) fournit une interface événementielle belle et concise, décrite dans la documentation officielle.

Ici, nous approchons de la question suivante ...

Que voulons-nous?


Et nous voulons construire un bus d'événements dans notre application. Mais nous avons deux exigences supplémentaires:

  1. Il doit être facilement accessible dans tous les composants. Des importations distinctes dans chacune des dizaines de composants nous semblent redondantes.
  2. Il doit être modulaire. Nous ne voulons pas garder à l'esprit tous les noms d'événements afin d'éviter la situation où l'événement «créé par un élément» déclenche des gestionnaires de l'application entière. Par conséquent, nous voulons pouvoir séparer facilement un petit fragment de l'arborescence des composants dans un module séparé et diffuser ses événements à l'intérieur, et non à l'extérieur.

Afin de mettre en œuvre une fonctionnalité aussi impressionnante, nous utilisons la puissante interface de plug-in que VueJS nous fournit. Vous pouvez vous y familiariser plus en détail ici sur la page de documentation officielle.

Enregistrons d'abord notre plugin. Pour ce faire, juste avant le point d'initialisation de notre application Vue (avant d'appeler Vue. $ Mount ()) nous plaçons le bloc suivant:

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

En fait, les plugins Vue sont un moyen d'étendre les fonctionnalités du framework à l'ensemble du niveau de l'application. L'interface du plugin offre plusieurs façons de s'intégrer dans le composant, mais aujourd'hui, nous allons présenter l'interface mixin. Cette méthode accepte un objet qui étend le descripteur de chaque composant avant de démarrer le cycle de vie dans l'application.(Le code du composant que nous écrivons n'est probablement pas le composant lui-même, mais une description de son comportement et l'encapsulation d'une certaine partie de la logique que le framework utilise à différentes étapes de son cycle de vie. L'initialisation du plug-in est en dehors du cycle de vie du composant, le précédant, donc nous nous disons «descripteur», pas un composant, pour souligner qu'exactement le code qui est écrit dans notre fichier, et non une entité qui est un produit du travail du framework, sera transféré dans la section mixin du plugin) .

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

C'est cet objet vide qui contiendra les extensions de nos composants. Mais pour commencer, un autre arrêt. Dans notre cas, nous voulons créer une interface pour accéder au bus au niveau de chaque composant. Ajoutons le champ '$ broadcast' à notre descripteur, il stockera un lien vers notre bus. Pour ce faire, utilisez Vue.prototype:

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

Maintenant, nous devons créer le bus lui-même, mais rappelons d'abord l'exigence de modularité et supposons que dans le descripteur de composant, nous déclarerons un nouveau module avec le champ «$ module» avec une valeur de texte (nous en aurons besoin un peu plus tard). Si le champ $ module est spécifié dans le composant lui-même, nous allons créer un nouveau bus pour lui; sinon, nous transmettrons le lien au parent via le champ $ parent. Notez que les champs descripteurs seront disponibles pour nous via le champ $ options.

Nous placerons la création de notre bus le plus tôt possible - dans le crochet avantCréer.

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

Enfin, remplissons les branches logiques. Si le descripteur contient une nouvelle déclaration de module, créez une nouvelle instance de bus, sinon, prenez le lien de $ 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;  // <--
        } 
      }, 
    }); 
  }, 
});

Nous rejetons l'annonce du plugin, nous considérons ... 1, 2, 3, 4 ... 10 lignes, comme je l'ai promis!

Pouvons-nous faire mieux?


Bien sûr on peut. Ce code est facilement extensible. Par exemple, dans notre cas, en plus de $ broadcast, nous avons décidé d'ajouter l'interface $ rootBroadcast, qui donne accès à un seul bus pour toute l'application. Les événements que l'utilisateur exécute sur le bus $ broadcast sont dupliqués sur le bus $ rootBroadcast afin que vous puissiez vous abonner à tous les événements d'un module particulier (dans ce cas, le nom de l'événement sera transmis au gestionnaire comme premier argument) ou à tous les événements d'application en général (puis le nom du module sera transmis au gestionnaire avec le premier argument, le nom de l'événement avec le second, et les données transmises avec l'événement seront transmises avec les arguments suivants). Cette conception nous permettra d'établir une interaction entre les modules, ainsi que de suspendre un seul gestionnaire sur les événements des différents modules.

// 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) => {…});

Voyons comment nous pouvons y parvenir: tout d'

abord, créez un seul bus, qui sera organisé via $ rootBroadcast, et le champ lui-même avec un lien:

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

Maintenant, nous avons besoin d'appartenance à un module dans chaque composant, développons donc la définition de la modularité comme ceci:

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

Ensuite, nous devons faire en sorte que l'événement sur le bus local modulaire se reflète dans la manière dont nous avons besoin à la racine. Pour ce faire, nous devons d'abord créer une interface proxy simple et placer le bus lui-même dans la propriété conditionnellement privée de $ bus:

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 };  // <--
        } 
      }, 
    }); 
  }, 
});

Et enfin, ajoutez des méthodes proxy à l'objet - car maintenant le champ $ broadcast ne fournit pas un accès direct au bus:

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);
        };
        // <<<
      }, 
    }); 
  }, 
});

Eh bien, comme touche finale, rappelons-nous que nous avons accès au bus en fermant, ce qui signifie que les gestionnaires ajoutés une fois ne seront pas effacés avec le composant, mais vivront pendant tout le temps de travail avec l'application. Cela peut provoquer des effets secondaires désagréables, alors ajoutons une fonction de nettoyage de l'auditeur à notre bus à la fin du cycle de vie du composant:

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);   // <--
        };
      }, 
    }); 
  }, 
});

Ainsi, cette option offre une fonctionnalité plus intéressante, bien que moins concise. Avec lui, vous pouvez implémenter un système complet de communication alternative entre les composants. De plus, il est entièrement sous notre contrôle et n'apporte pas de dépendances externes dans notre projet.

J'espère qu'après avoir lu ou acquis votre connaissance des plugins Vue, et peut-être que la prochaine fois que vous aurez besoin d'ajouter des fonctionnalités génériques à votre application, vous pourrez les implémenter plus efficacement - sans ajouter de dépendances externes.

All Articles