10 líneas de código para reducir el dolor de su proyecto Vue

... o familiaridad con los complementos Vue JS como ejemplo de un bus de eventos integrado


Algunas palabras sobre ...


¡Hola a todos! Haré una reserva de inmediato. Realmente amo a VueJS, he estado escribiendo activamente sobre él durante más de 2 años y no creo que el desarrollo pueda dañar al menos en un grado significativo :)
Por otro lado, siempre estamos tratando de encontrar soluciones universales que ayuden a pasar menos tiempo en trabajos mecánicos y más en lo que es realmente interesante. A veces la solución es particularmente exitosa. Una de estas que quiero compartir con ustedes. Las 10 líneas que se discutirán (spoiler: al final habrá un poco más) nacieron en el proceso de trabajo en el proyecto Cloud Blue - Connect, que es una aplicación bastante grande con más de 400 componentes. La solución que encontramos ya está integrada en varios puntos del sistema y durante más de medio año nunca ha requerido correcciones, por lo que se puede considerar de forma segura probada con éxito para determinar la estabilidad.

Y el último. Antes de proceder directamente a la solución, me gustaría detenerme un poco más en la descripción de los tres tipos de interacción entre los componentes Vue entre ellos: los principios del flujo unidireccional, el patrón de la tienda y el bus de eventos. Si esta explicación es innecesaria (o aburrida) para usted, vaya directamente a la sección con la solución: todo es lo más breve y técnico posible.

Un poco sobre cómo los componentes Vue se comunican entre sí


Quizás la primera pregunta que plantea una persona que escribió su primer componente es cómo recibirá los datos para el trabajo y cómo, a su vez, transmitirá los datos que recibió "fuera". El principio de interacción adoptado en el marco Vue JS se llama ...

Flujo de datos unidireccional


En resumen, este principio suena como "propiedades - abajo, eventos - arriba". Es decir, para recibir datos del exterior ("desde arriba"), registramos una propiedad especial dentro del componente en el que el marco escribe, si es necesario, nuestros datos recibidos "del exterior". Para transferir datos "hacia arriba", dentro del componente en el lugar correcto, llamamos al método de marco especial $ emit, que pasa nuestros datos al controlador del componente principal. Al mismo tiempo, en Vue JS no podemos simplemente "transmitir" el evento hasta una profundidad ilimitada (como por ejemplo en Angular 1.x). "Emerge" solo un nivel, al padre inmediato. Lo mismo ocurre con los eventos. Para transferirlos al siguiente nivel, para cada uno de ellos también debe registrar una interfaz especial: propiedades y eventos que transmitirán nuestro "mensaje" aún más.

Esto puede describirse como un edificio de oficinas en el que los trabajadores solo pueden moverse de sus pisos a los vecinos, uno arriba y otro abajo. Entonces, para transferir el "documento de firma" del quinto piso al segundo, se requerirá una cadena de tres trabajadores que lo entregarán del quinto piso al segundo, y luego tres más que lo devolverán al quinto.

"¡Pero esto es inconveniente!" Por supuesto, esto no siempre es conveniente desde el punto de vista del desarrollo, pero al observar el código de cada componente, podemos ver qué y a quién le pasa. No necesitamos tener en cuenta toda la estructura de la aplicación para comprender si nuestro componente está "en camino" al evento o no. Podemos ver esto desde el componente padre.

Aunque las ventajas de este enfoque son comprensibles, también tiene desventajas obvias, a saber, la alta cohesión de los componentes. En pocas palabras, para que podamos colocar algún componente en la estructura, debemos superponerlo con las interfaces necesarias para administrar su estado. Para reducir esta conectividad, a menudo usan "herramientas de administración de estado". Quizás la herramienta más popular para Vue es ...

Vuex (lado)


Continuando con nuestra analogía con un edificio de oficinas, Vuex Stor es un servicio postal interno. Imagine que en cada piso de la oficina hay una ventana para emitir y recibir paquetes. En el quinto piso transfieren el documento No. 11 para su firma, y ​​en el segundo preguntan periódicamente: "¿Hay algún documento para la firma?", Firme los documentos existentes y devuélvalos. En el quinto también preguntan: "¿Hay firmantes?" Al mismo tiempo, los empleados pueden trasladarse a otros pisos o a otras habitaciones: el principio del trabajo no cambiará mientras el correo esté funcionando.

Aproximadamente por este principio, el patrón llamado Tienda también funciona. Mediante la interfaz Vuex, se registra y configura un almacén de datos global, y los componentes se suscriben a él. Y no importa en qué nivel de estructura se produjo la apelación, la tienda siempre proporcionará la información correcta.

Parece que en esto todos los problemas ya han sido resueltos. Pero en algún momento de nuestro edificio metafórico, un empleado quiere llamar a otro para almorzar ... o informar algún tipo de error. Y aquí comienza lo extraño. El mensaje en sí no requiere transmisión como tal. Pero para usar el correo necesitas transferir algo. Entonces nuestros empleados inventan un código. Una bola verde - vaya a almorzar, dos cubos rojos - se produjo un error de aplicación E-981273, tres monedas amarillas - revise su correo y así sucesivamente.

Es fácil adivinar que con la ayuda de esta incómoda metáfora describo situaciones en las que necesitamos asegurar la respuesta de nuestro componente a un evento que ocurrió en otro componente, que en sí mismo no está conectado de ninguna manera con el flujo de datos. Se ha completado el guardado de un nuevo elemento: debe volver a tomar la colección. Se ha producido un error no autorizado 403: debe iniciar un cierre de sesión de usuario, etc. La práctica habitual (y lejos de ser la mejor) en este caso es crear banderas dentro de la tienda o interpretar indirectamente los datos almacenados y sus cambios. Esto conduce rápidamente a la contaminación tanto de la tienda como de la lógica de los componentes que la rodean.

En esta etapa, comenzamos a pensar en cómo pasar eventos directamente, sin pasar por toda la cadena de componentes. Y, un poco de google o hurgando en la documentación, nos encontramos con un patrón ...

Bus de eventos


Desde un punto de vista técnico, el bus de eventos es un objeto que permite usar un método especial para lanzar un "evento" y suscribirse a él usando otro. En otras palabras, cuando se suscribe al evento eventA, este objeto almacena la función del controlador transferido dentro de su estructura, que llamará cuando se llame al método de lanzamiento con la tecla eventA en algún lugar de la aplicación. Para firmar o ejecutar es suficiente acceder a él a través de importación o por referencia, y ya está.

Metafóricamente, en nuestro "edificio", un autobús es un chat común en el messenger. Los componentes se suscriben a un "chat general" al que otros componentes envían mensajes. Tan pronto como aparezca un "mensaje" en el "chat", al que se ha suscrito el componente, se iniciará el controlador.

Hay muchas formas diferentes de crear un bus de eventos. Puede escribirlo usted mismo o puede usar soluciones listas para usar, el mismo RxJS, que proporciona una gran funcionalidad para trabajar con secuencias enteras de eventos. Pero la mayoría de las veces cuando trabajan con VueJS usan, por extraño que parezca, VueJS en sí. La instancia de Vue creada a través del constructor (nuevo Vue ()) proporciona una interfaz de eventos hermosa y concisa, descrita en la documentación oficial.

Aquí nos acercamos a la siguiente pregunta ...

¿Qué queremos?


Y queremos construir un bus de eventos en nuestra aplicación. Pero tenemos dos requisitos adicionales:

  1. Debe ser fácilmente accesible en todos los componentes. Las importaciones separadas en cada una de las docenas de componentes nos parecen redundantes.
  2. Debe ser modular. No queremos tener en cuenta todos los nombres de eventos para evitar la situación en la que el evento "creado por elemento" dispara controladores de toda la aplicación. Por lo tanto, queremos poder separar fácilmente un pequeño fragmento del árbol de componentes en un módulo separado y transmitir sus eventos dentro y no fuera.

Para implementar una funcionalidad tan impresionante, utilizamos la potente interfaz de complemento que nos proporciona VueJS. Puede familiarizarse con él con más detalle aquí en la página con documentación oficial.

Registremos nuestro complemento primero. Para hacer esto, justo antes del punto de inicialización de nuestra aplicación Vue (antes de llamar a Vue. $ Mount ()) colocamos el siguiente bloque:

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

De hecho, los complementos de Vue son una forma de extender la funcionalidad del marco a todo el nivel de aplicación. La interfaz del complemento proporciona varias formas de integrarse en el componente, pero hoy presentaremos la interfaz mixin. Este método acepta un objeto que amplía el descriptor de cada componente antes de comenzar el ciclo de vida en la aplicación.(El código de componente que escribimos es más probable que no sea el componente en sí, sino una descripción de su comportamiento y encapsulación de una cierta parte de la lógica, que es utilizada por el marco en varias etapas durante el ciclo de vida. La inicialización del complemento está fuera del ciclo de vida del componente, precediéndolo, por lo tanto, decimos "descriptor", no un componente, para enfatizar que exactamente el código que está escrito en nuestro archivo, y no alguna entidad que sea el producto del trabajo del marco) será transferido a la sección mixin del complemento) .

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

Es este objeto vacío el que contendrá las extensiones para nuestros componentes. Pero para empezar, otra parada. En nuestro caso, queremos crear una interfaz para acceder al bus en el nivel de cada componente. Agreguemos el campo '$ broadcast' a nuestro descriptor, almacenará un enlace a nuestro bus. Para hacer esto, use Vue.prototype:

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

Ahora necesitamos crear el bus en sí, pero primero, recordemos el requisito de modularidad y supongamos que en el descriptor de componentes declararemos el nuevo módulo con el campo "$ module" con algún valor de texto (lo necesitaremos un poco más adelante). Si el campo $ module se especifica en el componente mismo, crearemos un nuevo bus para él; de lo contrario, pasaremos el enlace al padre a través del campo $ parent. Tenga en cuenta que los campos del descriptor estarán disponibles para nosotros a través del campo $ options.

Colocaremos la creación de nuestro autobús en la etapa más temprana posible, en el gancho beforeCreate.

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

Finalmente, completemos las ramas lógicas. Si el descriptor contiene una nueva declaración de módulo, cree una nueva instancia de bus; de lo contrario, tome el enlace 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;  // <--
        } 
      }, 
    }); 
  }, 
});

Descartamos el anuncio del complemento, consideramos ... 1, 2, 3, 4 ... 10 líneas, como lo prometí.

¿Podemos hacerlo mejor?


Por supuesto que podemos. Este código es fácilmente extensible. Por ejemplo, en nuestro caso, además de $ broadcast, decidimos agregar la interfaz $ rootBroadcast, que brinda acceso a un solo bus para toda la aplicación. Los eventos que el usuario ejecuta en el bus $ broadcast se duplican en el bus $ rootBroadcast para que pueda suscribirse a todos los eventos de un módulo en particular (en este caso, el nombre del evento se pasará al controlador como primer argumento) o a todos los eventos de la aplicación en general (luego el nombre del módulo se pasará al controlador con el primer argumento, el nombre del evento con el segundo y los datos transmitidos con el evento se pasarán con los siguientes argumentos). Este diseño nos permitirá establecer la interacción entre los módulos, así como colgar un solo controlador en los eventos de los diferentes módulos.

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

Veamos cómo podemos lograr esto:

Primero, cree un solo bus, al que se accederá a través de $ rootBroadcast, y el campo en sí con un enlace:

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

Ahora necesitamos la membresía del módulo en cada componente, así que ampliemos la definición de modularidad como esta:

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

A continuación, debemos hacer que el evento en el bus local modular se refleje en la forma en que lo necesitamos para la raíz. Para hacer esto, primero tenemos que crear una interfaz proxy simple y colocar el bus en la propiedad condicionalmente privada 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 };  // <--
        } 
      }, 
    }); 
  }, 
});

Y finalmente, agregue métodos proxy al objeto, porque ahora el campo $ broadcast no proporciona acceso directo al 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);
        };
        // <<<
      }, 
    }); 
  }, 
});

Bueno, como toque final, recordemos que tenemos acceso al bus al cerrar, lo que significa que los controladores agregados una vez no se borrarán con el componente, sino que vivirán durante todo el tiempo de trabajo con la aplicación. Esto puede causar efectos secundarios desagradables, así que agreguemos una función de limpieza de escucha a nuestro bus al final del ciclo de vida del componente:

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

Por lo tanto, esta opción proporciona una funcionalidad más interesante, aunque menos concisa. Con él, puede implementar un sistema completo de comunicación alternativa entre componentes. Además, está completamente bajo nuestro control y no trae dependencias externas a nuestro proyecto.

Espero que después de leer haya adquirido o actualizado su conocimiento de los complementos de Vue, y tal vez la próxima vez que necesite agregar alguna funcionalidad genérica a su aplicación, pueda implementarlo de manera más eficiente, sin agregar dependencias externas.

All Articles