Componentes web en lugar de reaccionar: otro intento

Hola Habr!

Recientemente decidí averiguar cómo debería ser una aplicación web del lado del cliente escrita completamente en componentes web de vainilla sin usar marcos. Resultó ser bastante funcional, al final esbocé la plantilla móvil de PWA , que ahora uso en prototipos. Partiendo de las siguientes premisas:

  • DOM es un estado. Como no tenemos un marco, nos olvidamos inmediatamente del funcionalismo y volvemos al imperativo OOP. Los componentes web son nodos DOM de larga duración que encapsulan su estado y tienen una API pública. No se recrean, sino que se cambian. Esto significa que deberíamos considerar el DOM no solo como una representación, sino como un repositorio de objetos comerciales, y por lo tanto, necesitamos construir una jerarquía de componentes teniendo en cuenta la conveniencia de su interacción.
  • La interacción de los componentes. Los componentes pueden interactuar a través de llamadas directas, intercambios de devolución de llamada o mediante eventos DOM de usuario arriba / abajo. El último método es el más preferible, ya que reduce el compromiso mutuo (acoplamiento) y ordena el gráfico de enlaces (ver ejemplo a continuación).
    Los eventos DOM solo funcionan dentro de la jerarquía: pueden aparecer de abajo hacia arriba en la cadena de antepasados ​​o transmitirse a todos los descendientes. En otros casos, la API estándar del navegador se usa para abordar el componente: document.querySelector ('page-home'), y algunos componentes pueden registrarse en la ventana y usarse globalmente: APP.route ('page-login').
  • Estilos interiores Shadow DOM no se utiliza, por lo tanto, los componentes heredan estilos globales, pero también pueden tener los suyos. Dado que es poco probable que <style scoped> se implemente en un futuro cercano, debe usar el prefijo del nombre del componente para declarar el estilo interno, sin embargo, esto es lacónico y funciona bien (vea el ejemplo a continuación).
  • HTML/DOM. DOM — , HTML (value, checked, innerHTML contenteditable=«true» ..). JS , — / , ( ). , , this.pass — , <input> . , DOM, , , , .
  • Navegación. Los componentes de la página viven dentro del contenedor <main>, y una vez creados, no se eliminan, sino que simplemente se ocultan. Esto le permite implementar la navegación usando location.hash, y los botones estándar del navegador funcionan correctamente. Al navegar a un componente existente, se llama al método onRoute (), donde puede actualizar los datos.

Estructura de la aplicación


Nuestra aplicación consta de:

  • el componente raíz <app-app>, accesible a través de window.APP, que contiene un enrutador de página y funcionalidad global;
  • paneles con botones contextuales (no los puse en un componente separado, sino que los hice parte del diseño <app-app> para simplificar el manejo de eventos);
  • menú desplegable (componente separado);
  • el contenedor <main> en el que se agregarán los componentes de la página: <page-home>, <page-login>, <page-work> cuando se abren.

Las páginas se apilan con navegación de ida y vuelta. Además, demostramos flujos de datos ascendentes y descendentes:

  • El estado de autorización y el nombre de usuario actual se almacenan en el componente <app-app>, pero provienen del componente <page-login> a través de un evento emergente.
  • Un temporizador marca el componente <app-app>, que envía el valor actual hacia abajo a través de un evento de difusión que se captura solo en el descendiente de <page-work>.

A través de eventos emergentes, también se implementa la instalación de botones sensibles al contexto en el panel de aplicaciones: los botones en sí, junto con los controladores, se crean en los componentes de la página, luego se envían a la parte superior, donde se interceptan en el nivel de la aplicación y se insertan en el panel.

Implementación


Para trabajar con el DOM interno del componente y enviar eventos posteriores , se utiliza la pequeña biblioteca WcMixin.js : menos de 200 líneas de código, la mitad de las cuales (unificación de eventos de entrada del usuario) también se pueden descartar. Todo lo demás es pura vainilla. Un componente típico (página de autorización) tiene este aspecto:

import wcmixin from './WcMixin.js'

const me = 'page-login'
customElements.define(me, class extends HTMLElement {
   _but = null

   connectedCallback() {
      this.innerHTML = `
         <style scoped>
            ${me} {
               height: 90%; width: 100%;
               display: flex; flex-direction: column;
               justify-content: center; align-items: center;
            }
            ${me} input { width: 60%; }
         </style>
         <input w-id='userInp/user' placeholder='user'/> 
         <input w-id='passInp/pass' type='password' placeholder='password'/>
      `
      wcmixin(this)

      this.userInp.oninput = (ev) => {
         this.bubbleEvent('login-change', {logged: false, user: this.user})
      }

      this.passInp.onkeypress = (ev) => {
         if (ev.key === 'Enter') this.login()
      }
   }

   onRoute() {
      this.userInp.focus()
      this._but = document.createElement('button')
      this._but.innerHTML = 'Log in<br>⇒'
      this._but.onclick = () => this.login()
      this.bubbleEvent('set-buts', { custom: [this._but] })
   }

   async login() {
      APP.msg = 'Authorization...'
      this._but.disabled = true
      setTimeout(() => {
         this._but.disabled = false
         if (this.user) {
            this.bubbleEvent('login-change', {logged: true, user: this.user})
            APP.route('page-work')
         } else {
            APP.msg = 'Empty user !'
            this.userInp.focus()
         }
      }, 1500)
   }
})

Primero, aquí vemos los estilos locales del componente. En segundo lugar, el único atributo no estándar w-id = `` userInp / user '' se ha agregado al marcado HTML. La función wcmixin () procesa todos los elementos marcados con este atributo y agrega variables al componente actual: this.userInp se refiere al elemento <input> en sí (que le permite colgar el controlador), y this.user es el valor del elemento (nombre de usuario). Si no se necesita acceso al elemento, puede especificar w-id = `` / user '', y solo se creará el valor.

Al ingresar un nombre de usuario, enviamos el valor actual a través de un evento emergente, creamos un botón sensible al contexto en el método onRoute () y también lo enviamos.

Es importante que el componente de autorización no sepa nada sobre los componentes superiores de la aplicación / panel, es decir, no está enganchado. Simplemente envía eventos arriba, y quien los intercepte depende del desarrollador. La recepción de eventos desde la aplicación <app-app> en el componente <page-work> se implementa de la misma manera:

import wcmixin from './WcMixin.js'

const me = 'page-work'
customElements.define(me, class extends HTMLElement {

   connectedCallback() {
      this.innerHTML = `
         <p w-id='/msg'>Enter text:</p>
         <p w-id='textDiv/text' contenteditable='true'>1)<br>2)<br>3)</p>
      `
      wcmixin(this)

      this.addEventListener('notify-timer', (ev) => {
         this.msg = `Enter text (elapsed ${ev.val}s):`
      })
   }

   async onRoute() {
      this.textDiv.focus()
      document.execCommand('selectAll',false,null)
      const but = document.createElement('button')
      but.innerHTML = 'Done<br>⇒'
      but.onclick = () => alert(this.text)
      this.bubbleEvent('set-buts', { custom: [but] })
   }
})

Y en el componente <app-app> escribimos:

setInterval(() => {
   this._elapsed += 1
   this.drownEvent('notify-timer', this._elapsed)
}, 1000)

El componente <app-app> tampoco sabe nada acerca de los componentes de la página que desean usar su contador, es decir, no está conectado a sus descendientes. Es suficiente que el desarrollador acuerde las firmas del evento. Los eventos DOM son livianos, los eventos descendentes se envían solo a los componentes web (no elementos simples), y los eventos ascendentes pasan a través de toda la cadena de antepasados ​​como estándar.

En realidad, eso es todo lo que quería decir.

→  Código completo del proyecto

Objeciones y sugerencias.


A menudo se me objeta que este enfoque combina la lógica empresarial y la visualización en un componente "grueso", que viola los patrones generalmente aceptados. Sin embargo, solo estamos hablando de la lógica para mostrar y validar la entrada del usuario, el resto de la lógica de negocios se puede mover fácilmente a clases JS separadas o incluso a servicios, con su propia jerarquía y métodos de interacción.

¿Por qué es esto necesario? Todavía hay un problema de rendimiento de representación (la recolección de basura no es gratuita), y un enfoque imperativo con herramientas nativas siempre será más rápido y menos intensivo en recursos que el declarativo / funcional con bibliotecas JS y VDOM. Si lo desea, estoy listo para competir con un representante de cualquier marco, en un TOR acordado, si asume la función de evaluación comparativa (puedo hacerlo mal).

Gracias por la atención.

All Articles