Migración de Frankenstein: Enfoque agnóstico del marco (Parte 2)

Publicado: 2022-03-10
Resumen rápido ↬ Recientemente discutimos qué es la "migración de Frankenstein", la comparamos con los tipos convencionales de migraciones y mencionamos dos bloques de construcción principales: microservicios y componentes web . También obtuvimos una base teórica de cómo funciona este tipo de migración. Si no leyó u olvidó esa discusión, es posible que desee volver a la Parte 1 primero porque ayuda a comprender todo lo que cubriremos en esta segunda parte del artículo.

En este artículo, pondremos a prueba toda la teoría realizando la migración paso a paso de una aplicación, siguiendo las recomendaciones de la parte anterior. Para simplificar las cosas, reducir las incertidumbres, las incógnitas y las conjeturas innecesarias, para el ejemplo práctico de la migración, decidí demostrar la práctica en una aplicación simple de tareas pendientes.

Es hora de poner a prueba la teoría.
Es hora de poner a prueba la teoría. (Vista previa grande)

En general, asumo que tiene una buena comprensión de cómo funciona una aplicación genérica de tareas pendientes. Este tipo de aplicación se adapta muy bien a nuestras necesidades: es predecible, pero tiene un número mínimo viable de componentes necesarios para demostrar diferentes aspectos de la migración de Frankenstein. Sin embargo, sin importar el tamaño y la complejidad de su aplicación real, el enfoque es bien escalable y se supone que es adecuado para proyectos de cualquier tamaño.

Una vista predeterminada de una aplicación TodoMVC
Una vista predeterminada de una aplicación TodoMVC (vista previa grande)

Para este artículo, como punto de partida, elegí una aplicación jQuery del proyecto TodoMVC, un ejemplo que quizás ya les resulte familiar a muchos de ustedes. jQuery es lo suficientemente heredado, puede reflejar una situación real con sus proyectos y, lo que es más importante, requiere un mantenimiento significativo y modificaciones para impulsar una aplicación dinámica moderna. (Esto debería ser suficiente para considerar la migración a algo más flexible).

¿Qué es ese “más flexible” al que vamos a migrar entonces? Para mostrar un caso muy práctico útil en la vida real, tuve que elegir entre los dos marcos más populares en estos días: React y Vue. Sin embargo, cualquiera que elija, perderemos algunos aspectos de la otra dirección.

¡Más después del salto! Continúe leyendo a continuación ↓

Entonces, en esta parte, revisaremos los dos siguientes:

  • Una migración de una aplicación jQuery a React , y
  • Una migración de una aplicación jQuery a Vue .
Nuestros objetivos: resultados de la migración a React y Vue
Nuestros objetivos: resultados de la migración a React y Vue. (Vista previa grande)

Repositorios de código

Todo el código mencionado aquí está disponible públicamente y puede acceder a él cuando lo desee. Hay dos repositorios disponibles para que juegues:

  • Frankenstein TodoMVC
    Este repositorio contiene aplicaciones TodoMVC en diferentes marcos/bibliotecas. Por ejemplo, puede encontrar ramas como vue , angularjs , react y jquery en este repositorio.
  • Demostración de Frankenstein
    Contiene varias ramas, cada una de las cuales representa una dirección de migración particular entre aplicaciones, disponible en el primer repositorio. Hay ramas como migration/jquery-to-react y migration/jquery-to-vue , en particular, que veremos más adelante.

Ambos repositorios son un trabajo en progreso y se deben agregar regularmente nuevas sucursales con nuevas aplicaciones y direcciones de migración. ( ¡También puede contribuir libremente! ) El historial de confirmaciones en las ramas de migración está bien estructurado y podría servir como documentación adicional con incluso más detalles de los que podría cubrir en este artículo.

¡Ahora, ensuciémonos las manos! Tenemos un largo camino por delante, así que no espere que sea un camino tranquilo. Depende de usted decidir cómo quiere seguir este artículo, pero podría hacer lo siguiente:

  • Clone la rama jquery del repositorio Frankenstein TodoMVC y siga estrictamente todas las instrucciones a continuación.
  • Alternativamente, puede abrir una rama dedicada a la migración a React o la migración a Vue desde el repositorio de demostración de Frankenstein y seguir el historial de confirmaciones.
  • Alternativamente, puede relajarse y seguir leyendo porque voy a resaltar el código más crítico aquí, y es mucho más importante comprender la mecánica del proceso que el código real.

Me gustaría mencionar una vez más que seguiremos estrictamente los pasos presentados en la primera parte teórica del artículo.

¡Vamos a sumergirnos!

  1. Identificar microservicios
  2. Permitir acceso de anfitrión a extranjero
  3. Escribir un microservicio/componente alienígena
  4. Escribir envoltorio de componente web alrededor del servicio alienígena
  5. Reemplazar servicio de host con componente web
  6. Enjuague y repita para todos sus componentes
  7. Cambiar a extranjero

1. Identificar Microservicios

Como sugiere la Parte 1, en este paso, tenemos que estructurar nuestra aplicación en pequeños servicios independientes dedicados a un trabajo en particular . El lector atento puede notar que nuestra aplicación de tareas ya es pequeña e independiente y puede representar un solo microservicio por sí solo. Así es como lo trataría yo mismo si esta aplicación viviera en un contexto más amplio. Recuerde, sin embargo, que el proceso de identificación de microservicios es completamente subjetivo y no hay una respuesta correcta.

Entonces, para ver el proceso de migración de Frankenstein con más detalle, podemos ir un paso más allá y dividir esta aplicación de tareas pendientes en dos microservicios independientes:

  1. Un campo de entrada para agregar un nuevo elemento.
    Este servicio también puede contener el encabezado de la aplicación, basado únicamente en el posicionamiento de proximidad de estos elementos.
  2. Una lista de elementos ya agregados.
    Este servicio es más avanzado y, junto con la lista en sí, también contiene acciones como filtrado, acciones de elementos de la lista, etc.
Aplicación TodoMVC dividida en dos microservicios independientes
Aplicación TodoMVC dividida en dos microservicios independientes. (Vista previa grande)

Sugerencia : para verificar si los servicios seleccionados son realmente independientes, elimine el marcado HTML que representa cada uno de estos servicios. Asegúrese de que las funciones restantes aún funcionen. En nuestro caso, debería ser posible agregar nuevas entradas en localStorage (que esta aplicación está usando como almacenamiento) desde el campo de entrada sin la lista, mientras que la lista aún representa las entradas de localStorage incluso si falta el campo de entrada. Si su aplicación arroja errores cuando elimina el marcado para un microservicio potencial, eche un vistazo a la sección "Refactorizar si es necesario" en la Parte 1 para ver un ejemplo de cómo lidiar con tales casos.

Por supuesto, podríamos continuar y dividir el segundo servicio y la lista de elementos aún más en microservicios independientes para cada elemento en particular. Sin embargo, podría ser demasiado granular para este ejemplo. Entonces, por ahora, concluimos que nuestra aplicación va a tener dos servicios; son independientes, y cada uno de ellos trabaja hacia su propia tarea particular. Por lo tanto, hemos dividido nuestra aplicación en microservicios .

2. Permitir el acceso de host a extranjero

Permítanme recordarles brevemente cuáles son.

  • Anfitrión
    Así se llama nuestra aplicación actual. Está escrito con el marco del que estamos a punto de alejarnos . En este caso particular, nuestra aplicación jQuery.
  • Extraterrestre
    En pocas palabras, esta es una reescritura gradual de Host en el nuevo marco al que estamos a punto de pasar . Nuevamente, en este caso particular, es una aplicación React o Vue.

La regla general al dividir Host y Alien es que debe poder desarrollar e implementar cualquiera de ellos sin romper el otro, en cualquier momento.

Mantener a Host y Alien independientes entre sí es crucial para la migración de Frankenstein. Sin embargo, esto hace que organizar la comunicación entre los dos sea un poco desafiante. ¿Cómo permitimos que Host acceda a Alien sin aplastar a los dos?

Agregar Alien como un submódulo de su host

Aunque hay varias formas de lograr la configuración que necesitamos, la forma más sencilla de organizar su proyecto para cumplir con este criterio es probablemente submódulos de git. Esto es lo que vamos a utilizar en este artículo. Te dejaré a ti leer detenidamente sobre cómo funcionan los submódulos en git para comprender las limitaciones y los errores de esta estructura.

Los principios generales de la arquitectura de nuestro proyecto con submódulos git deberían verse así:

  • Tanto Host como Alien son independientes y se mantienen en repositorios git separados;
  • Host hace referencia a Alien como un submódulo. En esta etapa, Host elige un estado particular (confirmación) de Alien y lo agrega como una subcarpeta en la estructura de carpetas de Host.
React TodoMVC agregado como un submódulo git en la aplicación jQuery TodoMVC
React TodoMVC agregado como un submódulo git en la aplicación jQuery TodoMVC. (Vista previa grande)

El proceso de agregar un submódulo es el mismo para cualquier aplicación. Enseñar git submodules está más allá del alcance de este artículo y no está directamente relacionado con la migración de Frankenstein en sí. Así que echemos un breve vistazo a los posibles ejemplos.

En los fragmentos a continuación, usamos la dirección Reaccionar como ejemplo. Para cualquier otra dirección de migración, reemplace react con el nombre de una rama de Frankenstein TodoMVC o ajuste los valores personalizados donde sea necesario.

Si sigue usando la aplicación jQuery TodoMVC original:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Si sigue la rama de migration/jquery-to-react (o cualquier otra dirección de migración) desde el repositorio de demostración de Frankenstein, la aplicación Alien ya debería estar allí como un git submodule , y debería ver una carpeta respectiva. Sin embargo, la carpeta está vacía de forma predeterminada y debe actualizar e inicializar los submódulos registrados.

Desde la raíz de tu proyecto (tu Host):

 $ git submodule update --init $ cd react $ npm i

Tenga en cuenta que, en ambos casos, instalamos dependencias para la aplicación Alien, pero se convierten en un espacio aislado en la subcarpeta y no contaminarán nuestro Host.

Después de agregar la aplicación Alien como un submódulo de su Host, obtiene aplicaciones Alien y Host independientes (en términos de microservicios). Sin embargo, Host considera a Alien como una subcarpeta en este caso y, obviamente, eso le permite a Host acceder a Alien sin problemas.

3. Escriba un microservicio/componente alienígena

En este paso, tenemos que decidir qué microservicio migrar primero y escribirlo/usarlo en el lado de Alien. Sigamos el mismo orden de servicios que identificamos en el Paso 1 y comencemos con el primero: campo de entrada para agregar un nuevo elemento. Sin embargo, antes de comenzar, aceptemos que más allá de este punto, vamos a utilizar un término más favorable componente en lugar de microservicio o servicio a medida que avanzamos hacia las premisas de los marcos frontend y el término componente sigue las definiciones de casi todos los modernos. estructura.

Las ramas del repositorio de Frankenstein TodoMVC contienen un componente resultante que representa el primer servicio "Campo de entrada para agregar un nuevo elemento" como un componente de encabezado:

  • Componente de encabezado en React
  • Componente de encabezado en Vue

Escribir componentes en el marco de su elección está más allá del alcance de este artículo y no es parte de la migración de Frankenstein. Sin embargo, hay un par de cosas a tener en cuenta al escribir un componente de Alien.

Independencia

En primer lugar, los componentes de Alien deben seguir el mismo principio de independencia, establecido previamente en el lado del Host: los componentes no deben depender de otros componentes de ninguna manera.

interoperabilidad

Gracias a la independencia de los servicios, lo más probable es que los componentes de su Host se comuniquen de alguna manera bien establecida, ya sea un sistema de gestión de estado, comunicación a través de algún almacenamiento compartido o directamente a través de un sistema de eventos DOM. La "interoperabilidad" de los componentes de Alien significa que deberían poder conectarse a la misma fuente de comunicación, establecida por Host, para enviar información sobre sus cambios de estado y escuchar los cambios en otros componentes. En la práctica, esto significa que si los componentes de su Host se comunican a través de eventos DOM, la creación de su componente Alien exclusivamente teniendo en cuenta la gestión de estado no funcionará perfectamente para este tipo de migración, lamentablemente.

Como ejemplo, observe el archivo js/storage.js que es el principal canal de comunicación para nuestros componentes jQuery:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

Aquí, usamos localStorage (ya que este ejemplo no es crítico para la seguridad) para almacenar nuestros elementos pendientes, y una vez que se registran los cambios en el almacenamiento, enviamos un evento DOM personalizado en el elemento del document que cualquier componente puede escuchar.

Al mismo tiempo, en el lado de Alien (digamos React) podemos configurar una comunicación de gestión de estado tan compleja como queramos. Sin embargo, probablemente sea inteligente mantenerlo para el futuro: para integrar con éxito nuestro componente Alien React en Host, debemos conectarnos al mismo canal de comunicación que utiliza Host. En este caso, es localStorage . Para simplificar las cosas, simplemente copiamos el archivo de almacenamiento de Host en Alien y le conectamos nuestros componentes:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

Ahora, nuestros componentes Alien pueden hablar el mismo idioma que los componentes Host y viceversa.

4. Escribir envoltorio de componente web alrededor del servicio alienígena

Aunque ahora solo estamos en el cuarto paso, hemos logrado bastante:

  • Hemos dividido nuestra aplicación Host en servicios independientes que están listos para ser reemplazados por servicios Alien;
  • Hemos configurado Host y Alien para que sean completamente independientes entre sí, pero muy bien conectados a través de git submodules ;
  • Hemos escrito nuestro primer componente Alien utilizando el nuevo marco.

Ahora es el momento de establecer un puente entre el Host y Alien para que el nuevo componente Alien pueda funcionar en el Host.

Recordatorio de la Parte 1 : asegúrese de que su Host tenga un paquete de paquetes disponible. En este artículo, confiamos en Webpack, pero eso no significa que la técnica no funcione con Rollup o cualquier otro paquete de su elección. Sin embargo, dejo el mapeo de Webpack para sus experimentos.

Convenio de denominación

Como se mencionó en el artículo anterior, vamos a utilizar Web Components para integrar Alien en Host. Del lado del Host, creamos un nuevo archivo: js/frankenstein-wrappers/Header-wrapper.js . (Será nuestro primer envoltorio de Frankenstein). Tenga en cuenta que es una buena idea nombrar sus envoltorios de la misma manera que sus componentes en la aplicación Alien, por ejemplo, simplemente agregando un sufijo " -wrapper ". Verá más adelante por qué es una buena idea, pero por ahora, aceptemos que esto significa que si el componente Alien se llama Header.js (en React) o Header.vue (en Vue), el contenedor correspondiente en el El lado del host debe llamarse Header-wrapper.js .

En nuestro primer contenedor, comenzamos con el modelo básico para registrar un elemento personalizado:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

A continuación, tenemos que inicializar Shadow DOM para este elemento.

Consulte la Parte 1 para obtener un razonamiento sobre por qué usamos Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

Con esto, tenemos todas las partes esenciales del Componente Web configurado, y es hora de agregar nuestro componente Alien a la mezcla. En primer lugar, al comienzo de nuestro envoltorio de Frankenstein, debemos importar todos los bits responsables de la representación del componente Alien.

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

Aquí tenemos que hacer una pausa por un segundo. Tenga en cuenta que no importamos las dependencias de Alien desde los node_modules de Host. Todo proviene del propio Alien que se encuentra en la subcarpeta react/ . Es por eso que el Paso 2 es tan importante, y es crucial asegurarse de que el Anfitrión tenga acceso total a los activos de Alien.

Ahora, podemos renderizar nuestro componente Alien dentro del Shadow DOM de Web Component:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

Nota : En este caso, React no necesita nada más. Sin embargo, para representar el componente Vue, debe agregar un nodo envolvente para contener su componente Vue como el siguiente:

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

La razón de esto es la diferencia en cómo React y Vue representan los componentes: React agrega el componente al nodo DOM al que se hace referencia, mientras que Vue reemplaza el nodo DOM al que se hace referencia con el componente. Por lo tanto, si hacemos .$mount(this.shadowRoot) para Vue, esencialmente reemplaza el Shadow DOM.

Eso es todo lo que tenemos que hacer con nuestro envoltorio por ahora. El resultado actual para el contenedor de Frankenstein en las direcciones de migración de jQuery-to-React y jQuery-to-Vue se puede encontrar aquí:

  • Frankenstein Wrapper para el componente React
  • Frankenstein Wrapper para el componente Vue

Para resumir la mecánica del envoltorio de Frankenstein:

  1. Crear un elemento personalizado,
  2. Iniciar Shadow DOM,
  3. Importe todo lo necesario para renderizar un componente Alien,
  4. Renderiza el componente Alien dentro del Shadow DOM del elemento personalizado.

Sin embargo, esto no hace que nuestro Alien in Host sea automático. Tenemos que reemplazar el marcado Host existente con nuestro nuevo contenedor Frankenstein.

Abróchense los cinturones, ¡puede que no sea tan sencillo como cabría esperar!

5. Reemplace el servicio de host con un componente web

Sigamos y agreguemos nuestro nuevo archivo Header-wrapper.js a index.html y reemplacemos el marcado del encabezado existente con el elemento personalizado <frankenstein-header-wrapper> recién creado.

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

Desafortunadamente, esto no funcionará tan simple como eso. Si abre un navegador y comprueba la consola, allí está el Uncaught SyntaxError no capturado esperándolo. Según el navegador y su compatibilidad con los módulos ES6, estará relacionado con las importaciones de ES6 o con la forma en que se representa el componente Alien. De cualquier manera, tenemos que hacer algo al respecto, pero el problema y la solución deberían ser familiares y claros para la mayoría de los lectores.

5.1. Actualice Webpack y Babel donde sea necesario

Deberíamos involucrar un poco de magia de Webpack y Babel antes de integrar nuestro envoltorio de Frankenstein. Discutir estas herramientas está más allá del alcance de este artículo, pero puede echar un vistazo a las confirmaciones correspondientes en el repositorio de demostración de Frankenstein:

  • Configuración para la migración a React
  • Configuración para la migración a Vue

Básicamente, configuramos el procesamiento de los archivos, así como un nuevo punto de entrada frankenstein en la configuración de Webpack para contener todo lo relacionado con los envoltorios de Frankenstein en un solo lugar.

Una vez que Webpack en Host sepa cómo procesar el componente Alien y los componentes web, estaremos listos para reemplazar el marcado de Host con el nuevo contenedor de Frankenstein.

5.2. Reemplazo del componente real

El reemplazo del componente debería ser sencillo ahora. En index.html de su Host, haga lo siguiente:

  1. Reemplace el elemento DOM <header class="header"> con <frankenstein-header-wrapper> ;
  2. Agregue un nuevo script frankenstein.js . Este es el nuevo punto de entrada en Webpack que contiene todo lo relacionado con los envoltorios de Frankenstein.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

¡Eso es todo! Reinicie su servidor si es necesario y sea testigo de la magia del componente Alien integrado en Host.

Sin embargo, todavía parece que falta algo. El componente Alien en el contexto Host no tiene el mismo aspecto que en el contexto de la aplicación Alien independiente. Simplemente no tiene estilo.

Componente Alien React sin estilo después de integrarse en Host
Componente Alien React sin estilo después de integrarse en Host (vista previa grande)

¿Por que es esto entonces? ¿No deberían integrarse automáticamente los estilos del componente con el componente Alien en Host? Ojalá lo hicieran, pero como en demasiadas situaciones, depende. Estamos llegando a la parte desafiante de la migración de Frankenstein.

5.3. Información general sobre el estilo del componente alienígena

En primer lugar, la ironía es que no hay errores en la forma en que funcionan las cosas. Todo está como está diseñado para funcionar. Para explicar esto, mencionemos brevemente diferentes formas de diseñar componentes.

Estilos globales

Todos estamos familiarizados con estos: los estilos globales se pueden distribuir (y generalmente se distribuyen) sin ningún componente en particular y se aplican a toda la página. Los estilos globales afectan a todos los nodos DOM con selectores coincidentes.

Algunos ejemplos de estilos globales son las etiquetas <style> y <link rel="stylesheet"> que se encuentran en su index.html . Alternativamente, se puede importar una hoja de estilo global a algún módulo JS raíz para que todos los componentes también puedan acceder a ella.

El problema de diseñar aplicaciones de esta manera es obvio: mantener hojas de estilo monolíticas para aplicaciones grandes se vuelve muy difícil. Además, como vimos en el artículo anterior, los estilos globales pueden dividir fácilmente los componentes que se representan directamente en el árbol DOM principal como en React o Vue.

Estilos agrupados

Estos estilos suelen estar estrechamente relacionados con un componente en sí y rara vez se distribuyen sin el componente. Los estilos normalmente residen en el mismo archivo con el componente. Buenos ejemplos de este tipo de estilo son los componentes con estilo en React o CSS Modules y Scoped CSS en componentes de un solo archivo en Vue. Sin embargo, independientemente de la variedad de herramientas para escribir estilos agrupados, el principio subyacente en la mayoría de ellos es el mismo: las herramientas proporcionan un mecanismo de alcance para bloquear los estilos definidos en un componente para que los estilos no rompan otros componentes o global. estilos.

¿Por qué los estilos con alcance podrían ser frágiles?

En la Parte 1, cuando justificamos el uso de Shadow DOM en la migración de Frankenstein, abordamos brevemente el tema del alcance frente a la encapsulación y cómo la encapsulación de Shadow DOM es diferente de las herramientas de estilo de alcance. Sin embargo, no explicamos por qué las herramientas de alcance brindan un estilo tan frágil para nuestros componentes, y ahora, cuando nos enfrentamos al componente Alien sin estilo, se vuelve esencial para la comprensión.

Todas las herramientas de alcance para marcos modernos funcionan de manera similar:

  • Escribe estilos para su componente de alguna manera sin pensar mucho en el alcance o la encapsulación;
  • Ejecuta sus componentes con hojas de estilo importadas/incrustadas a través de algún sistema de agrupación, como Webpack o Rollup;
  • El paquete genera clases CSS únicas u otros atributos, creando e inyectando selectores individuales tanto para su HTML como para las hojas de estilo correspondientes;
  • El empaquetador crea una entrada de <style> en el <head> de su documento y coloca los estilos de sus componentes con selectores combinados únicos allí.

Eso es practicamente todo. Funciona y funciona bien en muchos casos. Excepto cuando no es así: cuando los estilos para todos los componentes viven en el ámbito de estilo global, se vuelve fácil romperlos, por ejemplo, usando una mayor especificidad. Esto explica la fragilidad potencial de las herramientas de alcance, pero ¿por qué nuestro componente Alien está completamente sin estilo?

Echemos un vistazo al Host actual usando DevTools. Al inspeccionar el envoltorio de Frankenstein recién agregado con el componente Alien React, por ejemplo, podemos ver algo como esto:

Envoltorio de Frankenstein con componente Alien en su interior. Tenga en cuenta las clases de CSS únicas en los nodos de Alien.
Envoltorio de Frankenstein con componente Alien en su interior. Tenga en cuenta las clases de CSS únicas en los nodos de Alien. (Vista previa grande)

Entonces, Webpack genera clases CSS únicas para nuestro componente. ¡Genial! ¿Dónde están los estilos entonces? Bueno, los estilos están precisamente donde están diseñados para estar: en el <head> del documento.

Mientras que el componente Alien está dentro del envoltorio de Frankenstein, sus estilos están en el encabezado del documento.
Mientras que el componente Alien está dentro del envoltorio de Frankenstein, sus estilos están en el <head> del documento. (Vista previa grande)

Entonces todo funciona como debería, y este es el principal problema. Dado que nuestro componente Alien reside en Shadow DOM, y como se explica en la Parte n.º 1, Shadow DOM proporciona una encapsulación completa de los componentes del resto de la página y los estilos globales, incluidas las hojas de estilo recién generadas para el componente que no puede cruzar el borde de la sombra y llegar al componente Alien. Por lo tanto, el componente Alien se deja sin estilo. Sin embargo, ahora, las tácticas para resolver el problema deberían ser claras: de alguna manera deberíamos colocar los estilos del componente en el mismo Shadow DOM donde reside nuestro componente (en lugar del <head> del documento).

5.4. Estilos de fijación para el componente alienígena

Hasta ahora, el proceso de migrar a cualquier framework era el mismo. Sin embargo, las cosas comienzan a divergir aquí: cada marco tiene sus recomendaciones sobre cómo diseñar los componentes y, por lo tanto, las formas de abordar el problema difieren. Aquí, discutimos los casos más comunes pero, si el marco con el que trabaja usa alguna forma única de diseñar componentes, debe tener en cuenta las tácticas básicas, como colocar los estilos del componente en Shadow DOM en lugar de <head> .

En este capítulo, cubrimos soluciones para:

  • Estilos agrupados con módulos CSS en Vue (las tácticas para Scoped CSS son las mismas);
  • Estilos agrupados con componentes con estilo en React;
  • Módulos CSS genéricos y estilos globales. Los combino porque los módulos CSS, en general, son muy similares a las hojas de estilo globales y cualquier componente puede importarlos, lo que desconecta los estilos de cualquier componente en particular.

Restricciones primero: cualquier cosa que hagamos para arreglar el estilo no debería romper el componente Alien en sí . De lo contrario, perdemos la independencia de nuestros sistemas Alien y Host. Por lo tanto, para abordar el problema del estilo, nos basaremos en la configuración del paquete o en el envoltorio de Frankenstein.

Estilos agrupados en Vue y Shadow DOM

Si está escribiendo una aplicación Vue, lo más probable es que esté utilizando componentes de un solo archivo. Si también usa Webpack, debe estar familiarizado con dos cargadores vue-loader y vue-style-loader . El primero le permite escribir esos componentes de un solo archivo, mientras que el segundo inyecta dinámicamente el CSS del componente en un documento como una etiqueta <style> . De forma predeterminada, vue-style-loader inyecta los estilos del componente en el <head> del documento. Sin embargo, ambos paquetes aceptan la opción shadowMode en la configuración, lo que nos permite cambiar fácilmente el comportamiento predeterminado e inyectar estilos (como lo indica el nombre de la opción) en Shadow DOM. Veámoslo en acción.

Configuración del paquete web

Como mínimo, el archivo de configuración de Webpack debe contener lo siguiente:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

En una aplicación real, su bloque test: /\.css$/ será más sofisticado (probablemente involucrando la regla oneOf ) para tener en cuenta las configuraciones de Host y Alien. Sin embargo, en este caso, nuestro jQuery está diseñado con <link rel="stylesheet"> simple en index.html , por lo que no creamos estilos para Host a través de Webpack, y es seguro atender solo a Alien.

Configuración de la envoltura

Además de la configuración de Webpack, también necesitamos actualizar nuestro envoltorio de Frankenstein, apuntando a Vue al Shadow DOM correcto. En nuestro Header-wrapper.js , la representación del componente Vue debe incluir la propiedad shadowRoot que conduce a shadowRoot de nuestro contenedor Frankenstein:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

Después de actualizar los archivos y reiniciar su servidor, debería obtener algo como esto en sus DevTools:

Estilos incluidos con el componente Alien Vue colocado dentro del envoltorio Frankenstein con todas las clases CSS únicas conservadas.
Estilos incluidos con el componente Alien Vue colocado dentro del envoltorio Frankenstein con todas las clases CSS únicas conservadas. (Vista previa grande)

Finalmente, los estilos para el componente Vue están dentro de nuestro Shadow DOM. Al mismo tiempo, su aplicación debería verse así:

El componente de encabezado comienza a parecerse más a lo que debería. Sin embargo, todavía falta algo.
El componente de encabezado comienza a parecerse más a lo que debería. Sin embargo, todavía falta algo. (Vista previa grande)

Empezamos a obtener algo parecido a nuestra aplicación Vue: los estilos incluidos con el componente se inyectan en el Shadow DOM del envoltorio, pero el componente aún no se ve como debería. La razón es que en la aplicación original de Vue, el componente tiene estilo no solo con los estilos incluidos, sino también parcialmente con estilos globales. Sin embargo, antes de arreglar los estilos globales, tenemos que hacer que nuestra integración de React esté en el mismo estado que la de Vue.

Estilos incluidos en React y Shadow DOM

Debido a que hay muchas maneras en que se puede diseñar un componente React, la solución particular para corregir un componente Alien en Frankenstein Migration depende de la forma en que diseñamos el componente en primer lugar. Veamos brevemente las alternativas más utilizadas.

componentes con estilo

styled-components es una de las formas más populares de aplicar estilo a los componentes de React. Para el componente Header React, los componentes con estilo son precisamente la forma en que lo diseñamos. Dado que este es un enfoque clásico de CSS en JS, no hay ningún archivo con una extensión dedicada al que podamos conectar nuestro paquete como lo hacemos con los archivos .css o .js , por ejemplo. Afortunadamente, los componentes con estilo permiten la inyección de estilos de componentes en un nodo personalizado (Shadow DOM en nuestro caso) en lugar del head del documento con la ayuda del componente de ayuda StyleSheetManager . Es un componente predefinido, instalado con el paquete styled-components que acepta la propiedad de target , definiendo "un nodo DOM alternativo para inyectar información de estilos". ¡Exactamente lo que necesitamos! Además, ni siquiera necesitamos cambiar la configuración de nuestro paquete web: todo depende de nuestro contenedor de Frankenstein.

Deberíamos actualizar nuestro Header-wrapper.js que contiene el componente React Alien con las siguientes líneas:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

Aquí, importamos el componente StyleSheetManager (de Alien, y no de Host) y envolvemos nuestro componente React con él. Al mismo tiempo, enviamos la propiedad de target apuntando a nuestro shadowRoot . Eso es todo. Si reinicia el servidor, debe ver algo como esto en sus DevTools:

Estilos incluidos con el componente React Alien colocado dentro del envoltorio Frankenstein con todas las clases CSS únicas conservadas.
Estilos incluidos con el componente React Alien colocado dentro del envoltorio Frankenstein con todas las clases CSS únicas conservadas. (Vista previa grande)

Ahora, los estilos de nuestro componente están en Shadow DOM en lugar de <head> . De esta forma, la representación de nuestra aplicación ahora se asemeja a lo que hemos visto anteriormente con la aplicación Vue.

Después de mover los estilos agrupados al envoltorio de Frankenstein, el componente Alien React comienza a verse mejor. Sin embargo, aún no hemos llegado.
Después de mover los estilos agrupados al envoltorio de Frankenstein, el componente Alien React comienza a verse mejor. Sin embargo, todavía no estamos allí. (Vista previa grande)

La misma historia: los componentes con estilo son responsables solo de la parte agrupada de los estilos del componente React , y los estilos globales administran los bits restantes. Volvemos a los estilos globales en un momento después de revisar un tipo más de componentes de estilo.

Módulos CSS

Si observa más de cerca el componente Vue que hemos corregido anteriormente, puede notar que los Módulos CSS son precisamente la forma en que diseñamos ese componente. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . It doesn't matter. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. Cómo hacemos esto?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Great news! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

Hojas de estilo globales y opción de insert del style-loader

Si revisa la documentación para esta opción, notará que esta opción toma un selector por configuración. Esto significa que si tiene varios componentes de Alien que requieren estilos globales incorporados en un envoltorio de Frankenstein, debe especificar style-loader para cada uno de los envoltorios de Frankenstein. En la práctica, esto significa que, probablemente, deba confiar en la regla oneOf en su bloque de configuración para servir a todos los contenedores.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

No muy flexible, estoy de acuerdo. Sin embargo, no es gran cosa siempre y cuando no tenga que migrar cientos de componentes. De lo contrario, podría dificultar el mantenimiento de la configuración de su Webpack. Sin embargo, el verdadero problema es que no podemos escribir un selector de CSS para Shadow DOM.

Al tratar de resolver esto, podemos notar que la opción de insert también puede tomar una función en lugar de un simple selector para especificar una lógica más avanzada para la inserción. ¡Con esto, podemos usar esta opción para insertar hojas de estilo directamente en Shadow DOM! En forma simplificada podría verse similar a esto:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

Tentador, ¿no? Sin embargo, esto no funcionará para nuestro escenario o funcionará lejos de ser óptimo. Nuestro <frankenstein-header-wrapper> está disponible en index.html (porque lo agregamos en el Paso 5.2). Pero cuando Webpack procesa todas las dependencias (incluidas las hojas de estilo) para un componente Alien o un envoltorio de Frankenstein, Shadow DOM aún no se inicializa en el envoltorio de Frankenstein: las importaciones se procesan antes. Por lo tanto, apuntar insert directamente a shadowRoot dará como resultado un error.

Solo hay un caso en el que podemos garantizar que Shadow DOM se inicialice antes de que Webpack procese nuestra dependencia de la hoja de estilo. Si el componente Alien no importa una hoja de estilo y depende del contenedor de Frankenstein para importarla, podemos emplear la importación dinámica e importar la hoja de estilo requerida después de configurar Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

Esto funcionará: dicha importación, combinada con la configuración de insert anterior, encontrará el Shadow DOM correcto e insertará la etiqueta <style> en él. Sin embargo, obtener y procesar la hoja de estilo llevará tiempo, lo que significa que sus usuarios con una conexión lenta o dispositivos lentos podrían enfrentar un momento del componente sin estilo antes de que su hoja de estilo ocupe su lugar dentro del Shadow DOM del envoltorio.

El componente Alien sin estilo se procesa antes de que la hoja de estilo global se importe y se agregue a Shadow DOM.
El componente Alien sin estilo se procesa antes de que la hoja de estilo global se importe y se agregue a Shadow DOM. (Vista previa grande)

Así que, en general, aunque insert acepta la función, desafortunadamente, no es suficiente para nosotros, y tenemos que recurrir a los selectores de CSS simples como frankenstein-header-wrapper . Sin embargo, esto no coloca hojas de estilo en Shadow DOM automáticamente, y las hojas de estilo residen en <frankenstein-header-wrapper> fuera de Shadow DOM.

style-loader coloca la hoja de estilo importada en el envoltorio de Frankenstein, pero fuera de Shadow DOM.
style-loader coloca la hoja de estilo importada en el envoltorio de Frankenstein, pero fuera de Shadow DOM. (Vista previa grande)

Necesitamos una pieza más del rompecabezas.

Configuración de contenedor para hojas de estilo globales y Shadow DOM

Afortunadamente, la solución es bastante sencilla en el lado del contenedor: cuando se inicializa Shadow DOM, debemos verificar si hay hojas de estilo pendientes en el contenedor actual y colocarlas en Shadow DOM.

El estado actual de la importación de la hoja de estilo global es el siguiente:

  • Importamos una hoja de estilo que debe agregarse a Shadow DOM. La hoja de estilo se puede importar en el propio componente Alien o, explícitamente, en el contenedor de Frankenstein. En el caso de la migración a React, por ejemplo, la importación se inicializa desde el contenedor. Sin embargo, en la migración a Vue, el componente similar importa la hoja de estilo requerida y no tenemos que importar nada en el contenedor.
  • Como se señaló anteriormente, cuando Webpack procesa las importaciones .css para el componente Alien, gracias a la opción de insert de style-loader , las hojas de estilo se inyectan en un contenedor de Frankenstein, pero fuera de Shadow DOM.

La inicialización simplificada de Shadow DOM en el envoltorio de Frankenstein, actualmente (antes de extraer cualquier hoja de estilo) debería verse similar a esto:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

Ahora, para evitar el parpadeo del componente sin estilo, lo que debemos hacer ahora es extraer todas las hojas de estilo requeridas después de la inicialización de Shadow DOM, pero antes de la representación del componente Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

Fue una explicación larga con muchos detalles, pero principalmente, todo lo que se necesita para incorporar hojas de estilo globales a Shadow DOM:

  • En la configuración de Webpack, agregue style-loader con la opción de insert que apunta al contenedor de Frankenstein requerido.
  • En el propio envoltorio, extraiga las hojas de estilo "pendientes" después de la inicialización de Shadow DOM, pero antes de la representación del componente Alien.

Después de implementar estos cambios, su componente debería tener todo lo que necesita. Lo único que podría desear (esto no es un requisito) agregar es un CSS personalizado para ajustar un componente de Alien en el entorno del Host. Incluso puede diseñar su componente Alien completamente diferente cuando se usa en Host. Va más allá del punto principal del artículo, pero mira el código final del contenedor, donde puedes encontrar ejemplos de cómo anular estilos simples en el nivel del contenedor.

  • Envoltura de Frankenstein para el componente React
  • Envoltura de Frankenstein para el componente Vue

También puede echar un vistazo a la configuración de Webpack en este paso de la migración:

  • Migración a React con componentes con estilo
  • Migración a React con Módulos CSS
  • Migración a Vue

Y finalmente, nuestros componentes se ven exactamente como pretendíamos que se vieran.

Resultado de la migración del componente Header escrito con Vue y React. La lista de tareas pendientes sigue siendo una aplicación jQuery.
Resultado de la migración del componente Header escrito con Vue y React. La lista de tareas pendientes sigue siendo una aplicación jQuery. (Vista previa grande)

5.5. Resumen de estilos de reparación para el componente Alien

Este es un gran momento para resumir lo que hemos aprendido en este capítulo hasta ahora. Podría parecer que tuvimos que hacer un gran trabajo para arreglar el estilo del componente Alien; sin embargo, todo se reduce a:

  • Reparar estilos agrupados implementados con componentes con estilo en React o módulos CSS y Scoped CSS en Vue es tan simple como un par de líneas en la configuración de contenedor Frankenstein o Webpack.
  • La corrección de estilos, implementada con módulos CSS, comienza con solo una línea en la configuración de css-loader . Después de eso, los módulos CSS se tratan como una hoja de estilo global.
  • La corrección de las hojas de estilo globales requiere configurar el paquete style-loader con la opción de insert en Webpack y actualizar el contenedor de Frankenstein para extraer las hojas de estilo en Shadow DOM en el momento adecuado del ciclo de vida del contenedor.

Después de todo, tenemos un componente Alien con el estilo adecuado migrado al Host. Sin embargo, solo hay una cosa que podría o no molestarlo según el marco al que migre.

Buenas noticias primero: si está migrando a Vue , la demostración debería funcionar bien y debería poder agregar nuevos elementos pendientes desde el componente Vue migrado. Sin embargo, si está migrando a React e intenta agregar un nuevo elemento pendiente, no tendrá éxito. Agregar nuevos elementos simplemente no funciona y no se agregan entradas a la lista. ¿Pero por qué? ¿Cuál es el problema? Sin prejuicios, pero React tiene sus propias opiniones sobre algunas cosas.

5.6. Eventos de React y JS en Shadow DOM

No importa lo que le diga la documentación de React, React no es muy amigable con los componentes web. La simplicidad del ejemplo en la documentación no resiste ninguna crítica, y cualquier cosa más complicada que representar un enlace en Web Component requiere investigación e investigación.

Como ha visto mientras arreglaba el estilo de nuestro componente Alien, a diferencia de Vue, donde las cosas se ajustan a los componentes web casi desde el primer momento, React no está listo para los componentes web. Por ahora, sabemos cómo hacer que los componentes de React al menos se vean bien dentro de los componentes web, pero también hay funciones y eventos de JavaScript que corregir.

Para resumir: Shadow DOM encapsula eventos y los reorienta, mientras que React no admite este comportamiento de Shadow DOM de forma nativa y, por lo tanto, no detecta eventos que provienen de Shadow DOM. Hay razones más profundas para este comportamiento, e incluso hay un problema abierto en el rastreador de errores de React si desea profundizar en más detalles y discusiones.

Afortunadamente, personas inteligentes prepararon una solución para nosotros. @josephnvu proporcionó la base para la solución y Lukas Bombach la convirtió en el módulo npm react-shadow-dom-retarget-events . Para que pueda instalar el paquete, siga las instrucciones en la página de paquetes, actualice el código de su envoltorio y su componente Alien comenzará a funcionar mágicamente:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

Si desea que tenga un mejor rendimiento, puede hacer una copia local del paquete (la licencia MIT lo permite) y limitar la cantidad de eventos para escuchar como se hace en el repositorio de demostración de Frankenstein. Para este ejemplo, sé qué eventos necesito para reorientar y especificar solo esos.

Con esto, finalmente (sé que fue un proceso largo) terminamos con la migración adecuada del primer componente Alien completamente funcional y con estilo. Consíguete un buen trago. ¡Te lo mereces!

6. Enjuague y repita para todos sus componentes

Después de migrar el primer componente, debemos repetir el proceso para todos nuestros componentes. Sin embargo, en el caso de Frankenstein Demo, solo queda uno: el responsable de mostrar la lista de tareas pendientes.

Nuevos envoltorios para nuevos componentes

Comencemos agregando un nuevo contenedor. Siguiendo la convención de nomenclatura, discutida anteriormente (ya que nuestro componente React se llama MainSection.js ), el contenedor correspondiente en la migración a React debe llamarse MainSection-wrapper.js . Al mismo tiempo, un componente similar en Vue se llama Listing.vue , por lo que el contenedor correspondiente en la migración a Vue debería llamarse Listing-wrapper.js . Sin embargo, sin importar la convención de nomenclatura, el envoltorio en sí será casi idéntico al que ya tenemos:

  • Envoltura para la lista de React
  • Envoltura para la lista de Vue

Solo hay una cosa interesante que presentamos en este segundo componente en la aplicación React. A veces, por esa u otra razón, es posible que desee utilizar algún complemento de jQuery en sus componentes. En el caso de nuestro componente React, introdujimos dos cosas:

  • Complemento de información sobre herramientas de Bootstrap que usa jQuery,
  • Un interruptor para clases CSS como .addClass() y .removeClass() .

    Nota : este uso de jQuery para agregar/eliminar clases es puramente ilustrativo. No use jQuery para este escenario en proyectos reales; en su lugar, confíe en JavaScript simple.

Por supuesto, puede parecer extraño introducir jQuery en un componente de Alien cuando migramos fuera de jQuery, pero su Host puede ser diferente del Host en este ejemplo: puede migrar fuera de AngularJS o cualquier otra cosa. Además, la funcionalidad jQuery en un componente y jQuery global no son necesariamente lo mismo.

Sin embargo, el problema es que incluso si confirma que el componente funciona bien en el contexto de su aplicación Alien, cuando lo coloca en Shadow DOM, sus complementos jQuery y otro código que se basa en jQuery simplemente no funcionarán.

jQuery en la sombra DOM

Echemos un vistazo a una inicialización general de un complemento jQuery aleatorio:

 $('.my-selector').fancyPlugin();

De esta manera, todos los elementos con .my-selector serán procesados ​​por fancyPlugin . Esta forma de inicialización asume que .my-selector está presente en el DOM global. Sin embargo, una vez que dicho elemento se coloca en Shadow DOM, al igual que con los estilos, los límites de sombra evitan que jQuery se cuele. Como resultado, jQuery no puede encontrar elementos dentro de Shadow DOM.

La solución es proporcionar un segundo parámetro opcional al selector que define el elemento raíz desde el que jQuery buscará. Y aquí es donde podemos proporcionar nuestro shadowRoot .

 $('.my-selector', this.shadowRoot).fancyPlugin();

De esta manera, los selectores de jQuery y, como resultado, los complementos funcionarán bien.

Sin embargo, tenga en cuenta que los componentes de Alien están destinados a ser utilizados tanto: en Alien sin shadow DOM, como en Host dentro de Shadow DOM. Por lo tanto, necesitamos una solución más unificada que no suponga la presencia de Shadow DOM de forma predeterminada.

Al analizar el componente MainSection en nuestra aplicación React, encontramos que establece la propiedad documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

Entonces, verificamos la propiedad root pasada, y si existe, esto es lo que usamos como documentRoot . De lo contrario, recurrimos a document .

Aquí está la inicialización del complemento de información sobre herramientas que usa esta propiedad:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

Como beneficio adicional, usamos la misma propiedad root para definir un contenedor para inyectar la información sobre herramientas en este caso.

Ahora, cuando el componente Alien esté listo para aceptar la propiedad root , actualizamos la representación del componente en el contenedor de Frankenstein correspondiente:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

¡Y eso es! El componente funciona tan bien en Shadow DOM como en el DOM global.

Configuración de Webpack para escenario de múltiples contenedores

La parte emocionante está sucediendo en la configuración de Webpack cuando se usan varios contenedores. Nada cambia para los estilos incluidos, como los módulos CSS en los componentes de Vue o los componentes con estilo en React. Sin embargo, los estilos globales deberían tener un pequeño giro ahora.

Recuerde, dijimos que style-loader (responsable de inyectar hojas de estilo globales en el Shadow DOM correcto) es inflexible ya que solo toma un selector a la vez para su opción de insert . Esto significa que debemos dividir la regla .css en Webpack para tener una subregla por contenedor usando la regla oneOf o similar, si está en un paquete que no sea Webpack.

Siempre es más fácil de explicar usando un ejemplo, así que esta vez hablemos del de la migración a Vue (el de la migración a React, sin embargo, es casi idéntico):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

He excluido css-loader porque su configuración es la misma en todos los casos. En su lugar, hablemos style-loader . En esta configuración, insertamos la etiqueta <style> en *-header-* o *-listing-* , según el nombre del archivo que solicita esa hoja de estilo (regla de issuer en Webpack). Pero debemos recordar que la hoja de estilo global requerida para renderizar un componente Alien puede importarse en dos lugares:

  • El propio componente Alien,
  • Un envoltorio de Frankenstein.

Y aquí, deberíamos apreciar la convención de nomenclatura para los contenedores, descrita anteriormente, cuando el nombre de un componente de Alien y el contenedor correspondiente coinciden. Si, por ejemplo, tenemos una hoja de estilo, importada en un componente de Vue llamado Header.vue , llega a corregir *-header-* wrapper. Al mismo tiempo, si, en cambio, importamos la hoja de estilo en el contenedor, dicha hoja de estilo sigue exactamente la misma regla si el contenedor se llama Header-wrapper.js sin ningún cambio en la configuración. Lo mismo para el componente Listing.vue y su contenedor correspondiente Listing-wrapper.js . Usando esta convención de nomenclatura, reducimos la configuración en nuestro paquete.

Después de migrar todos sus componentes, es hora del paso final de la migración.

7. Cambiar a extranjero

En algún momento, descubre que los componentes que identificó en el primer paso de la migración se reemplazan con envoltorios de Frankenstein. En realidad, no queda ninguna aplicación jQuery y lo que tiene es, esencialmente, la aplicación Alien que se une utilizando los medios de Host.

Por ejemplo, la parte de contenido de index.html en la aplicación jQuery, después de la migración de ambos microservicios, se parece a esto ahora:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

En este momento, no tiene sentido mantener nuestra aplicación jQuery: en su lugar, debemos cambiar a la aplicación Vue y olvidarnos de todos nuestros contenedores, Shadow DOM y elegantes configuraciones de Webpack. Para hacer esto, tenemos una solución elegante.

Hablemos de las solicitudes HTTP. Mencionaré la configuración de Apache aquí, pero esto es solo un detalle de implementación: hacer el cambio en Nginx o cualquier otra cosa debería ser tan trivial como en Apache.

Imagine que tiene su sitio servido desde la carpeta /var/www/html en su servidor. En este caso, su httpd.conf o httpd-vhost.conf debería tener una entrada que apunte a esa carpeta como:

 DocumentRoot "/var/www/html"

Para cambiar su aplicación después de la migración de Frankenstein de jQuery a React, todo lo que necesita hacer es actualizar la entrada DocumentRoot a algo como:

 DocumentRoot "/var/www/html/react/build"

Cree su aplicación Alien, reinicie su servidor y su aplicación se entregará directamente desde la carpeta de Alien: la aplicación React se entregará desde la carpeta react/ . Sin embargo, lo mismo es cierto para Vue, por supuesto, o cualquier otro marco que haya migrado también. Esta es la razón por la que es tan importante mantener el Anfitrión y el Extranjero completamente independientes y funcionales en cualquier momento porque su Alien se convierte en su Anfitrión en este paso.

Ahora puede eliminar de forma segura todo lo que haya alrededor de la carpeta de Alien, incluidos todos los DOM de sombra, los envoltorios de Frankenstein y cualquier otro artefacto relacionado con la migración. Fue un camino difícil en algunos momentos, pero ha migrado su sitio. ¡Felicidades!

Conclusión

Definitivamente pasamos por un terreno algo accidentado en este artículo. Sin embargo, después de que comenzamos con una aplicación jQuery, logramos migrarla tanto a Vue como a React. Hemos descubierto algunos problemas inesperados y no tan triviales en el camino: tuvimos que arreglar el estilo, tuvimos que arreglar la funcionalidad de JavaScript, introducir algunas configuraciones de paquetes y mucho más. Sin embargo, nos dio una mejor visión general de qué esperar en proyectos reales. Al final, obtuvimos una aplicación contemporánea sin partes restantes de la aplicación jQuery, aunque teníamos todos los derechos para ser escépticos sobre el resultado final mientras la migración estaba en curso.

Después del cambio a Alien, Frankenstein puede retirarse.
Después del cambio a Alien, Frankenstein puede retirarse. (Vista previa grande)

Frankenstein Migration no es una panacea ni debería ser un proceso aterrador. Es solo el algoritmo definido, aplicable a muchos proyectos, que ayuda a transformar los proyectos en algo nuevo y sólido de manera predecible.