Creación de componentes web interoperables que incluso funcionan con React PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.

Creación de componentes web interoperables que incluso funcionan con React

Aquellos de nosotros que hemos sido desarrolladores web durante más de unos pocos años, probablemente hemos escrito código utilizando más de un marco de JavaScript. Con todas las opciones disponibles (React, Svelte, Vue, Angular, Solid), es casi inevitable. Una de las cosas más frustrantes con las que tenemos que lidiar cuando trabajamos en varios marcos es volver a crear todos esos componentes de interfaz de usuario de bajo nivel: botones, pestañas, menús desplegables, etc. Lo que es particularmente frustrante es que normalmente los tendremos definidos en un marco. , digamos React, pero luego necesitamos reescribirlos si queremos construir algo en Svelte. O Vue. O Sólido. Y así.

¿No sería mejor si pudiéramos definir estos componentes de interfaz de usuario de bajo nivel una vez, de forma independiente del marco, y luego reutilizarlos entre marcos? ¡Por supuesto que sí! Y podemos; los componentes web son el camino. Esta publicación le mostrará cómo.

A partir de ahora, falta un poco la historia de SSR para los componentes web. El Shadow DOM declarativo (DSD) es la forma en que un componente web se representa en el lado del servidor, pero, al momento de escribir este artículo, no está integrado con sus marcos de aplicaciones favoritos como Next, Remix o SvelteKit. Si ese es un requisito para usted, asegúrese de verificar el estado más reciente de DSD. Pero de lo contrario, si SSR no es algo que está usando, siga leyendo.

Primero, algo de contexto

Los componentes web son esencialmente elementos HTML que usted mismo define, como <yummy-pizza> o lo que sea, desde cero. Están cubiertos aquí en CSS-Tricks (incluyendo una extensa serie de Caleb Williams y uno de John Rhea) pero repasaremos brevemente el proceso. Esencialmente, usted define una clase de JavaScript, la hereda de HTMLElement, y luego defina las propiedades, atributos y estilos que tenga el componente web y, por supuesto, el marcado que finalmente mostrará a sus usuarios.

Ser capaz de definir elementos HTML personalizados que no están vinculados a ningún componente en particular es emocionante. Pero esta libertad es también una limitación. Existir independientemente de cualquier marco de JavaScript significa que realmente no puede interactuar con esos marcos de JavaScript. Piense en un componente de React que obtenga algunos datos y luego represente algunos otros Componente React, pasando los datos. Esto realmente no funcionaría como un componente web, ya que un componente web no sabe cómo representar un componente React.

Los componentes web sobresalen particularmente como componentes de la hoja. componentes de la hoja son lo último que se representa en un árbol de componentes. Estos son los componentes que reciben algunos apoyos y hacen algunos UI. Estos son no los componentes que se encuentran en el medio de su árbol de componentes, pasando datos, estableciendo contexto, etc., solo piezas puras de UI se verá igual, sin importar qué marco de JavaScript esté impulsando el resto de la aplicación.

El componente web que estamos construyendo

En lugar de construir algo aburrido (y común), como un botón, construyamos algo un poco diferente. En mi loading analizamos el uso de vistas previas de imágenes borrosas para evitar el reflujo de contenido y proporcionar una interfaz de usuario decente para los usuarios mientras se cargan nuestras imágenes. Observamos la codificación base64 de versiones borrosas y degradadas de nuestras imágenes, y las mostramos en nuestra interfaz de usuario mientras se cargaba la imagen real. También buscamos generar vistas previas increíblemente compactas y borrosas usando una herramienta llamada desenfoque.

Esa publicación le mostró cómo generar esas vistas previas y usarlas en un proyecto de React. Esta publicación le mostrará cómo usar esas vistas previas de un componente web para que puedan ser utilizadas por cualquier Marco de JavaScript.

Pero necesitamos caminar antes de poder correr, así que primero veremos algo trivial y tonto para ver exactamente cómo funcionan los componentes web.

Todo en esta publicación construirá componentes web estándar sin ninguna herramienta. Eso significa que el código tendrá un poco de repetitivo, pero debería ser relativamente fácil de seguir. Herramientas como cama or plantilla están diseñados para crear componentes web y se pueden usar para eliminar gran parte de este modelo. ¡Os animo a echarles un vistazo! Pero para esta publicación, preferiré un poco más de repetitivo a cambio de no tener que introducir y enseñar otra dependencia.

Un componente de contador simple

Construyamos el clásico "Hola mundo" de los componentes de JavaScript: un contador. Representaremos un valor y un botón que incrementa ese valor. Simple y aburrido, pero nos permitirá ver el componente web más simple posible.

Para construir un componente web, el primer paso es crear una clase de JavaScript, que hereda de HTMLElement:

class Counter extends HTMLElement {}

El último paso es registrar el componente web, pero solo si aún no lo hemos registrado:

if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter);
}

Y, por supuesto, renderízalo:

<counter-wc></counter-wc>

Y todo lo demás es que hagamos que el componente web haga lo que queramos. Un método de ciclo de vida común es connectedCallback, que se activa cuando nuestro componente web se agrega al DOM. Podríamos usar ese método para representar cualquier contenido que deseemos. Recuerde, esta es una clase JS que hereda de HTMLElement, lo que significa nuestro this El valor es el elemento del componente web en sí mismo, con todos los métodos normales de manipulación de DOM que ya conoce y ama.

En su forma más simple, podríamos hacer esto:

class Counter extends HTMLElement { connectedCallback() { this.innerHTML = "<div style='color: green'>Hey</div>"; }
} if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter);
}

…que funcionará bien.

La palabra "hey" en verde.
Creación de componentes web interoperables que incluso funcionan con React

Agregar contenido real

Agreguemos contenido útil e interactivo. Necesitamos una <span> para mantener el valor del número actual y un <button> para incrementar el contador. Por ahora, crearemos este contenido en nuestro constructor y lo agregaremos cuando el componente web esté realmente en el DOM:

constructor() { super(); const container = document.createElement('div'); this.valSpan = document.createElement('span'); const increment = document.createElement('button'); increment.innerText = 'Increment'; increment.addEventListener('click', () => { this.#value = this.#currentValue + 1; }); container.appendChild(this.valSpan); container.appendChild(document.createElement('br')); container.appendChild(increment); this.container = container;
} connectedCallback() { this.appendChild(this.container); this.update();
}

Si está realmente asqueado por la creación manual de DOM, recuerde que puede configurar innerHTML, o incluso cree un elemento de plantilla una vez como una propiedad estática de su clase de componente web, clónelo e inserte el contenido para nuevas instancias de componente web. Probablemente hay algunas otras opciones en las que no estoy pensando, o siempre puede usar un marco de componentes web como cama or plantilla. Pero para esta publicación, continuaremos manteniéndolo simple.

Continuando, necesitamos una propiedad de clase de JavaScript configurable llamada value

#currentValue = 0; set #value(val) { this.#currentValue = val; this.update();
}

Es solo una propiedad de clase estándar con un setter, junto con una segunda propiedad para mantener el valor. Un giro divertido es que estoy usando la sintaxis de propiedad de clase privada de JavaScript para estos valores. Eso significa que nadie fuera de nuestro componente web puede tocar estos valores. Este es JavaScript estándar eso es compatible con todos los navegadores modernos, así que no tengas miedo de usarlo.

O siéntase libre de llamarlo _value si tu prefieres. Y, por último, nuestro update método:

update() { this.valSpan.innerText = this.#currentValue;
}

¡Funciona!

El componente web contador.
Creación de componentes web interoperables que incluso funcionan con React

Obviamente, este no es un código que le gustaría mantener a escala. Aquí hay un completo ejemplo de trabajo si desea una mirada más cercana. Como he dicho, herramientas como Lit y ​​Stencil están diseñadas para simplificar esto.

Añadiendo algo más de funcionalidad

Esta publicación no es una inmersión profunda en los componentes web. No cubriremos todas las API y ciclos de vida; ni siquiera cubriremos raíces de sombra o ranuras. Hay un sinfín de contenidos sobre esos temas. Mi objetivo aquí es proporcionar una introducción lo suficientemente decente para despertar cierto interés, junto con una guía útil sobre usando componentes web con los populares marcos de JavaScript que ya conoce y ama.

Con ese fin, mejoremos un poco nuestro componente web de contador. Hagamos que acepte un color atributo, para controlar el color del valor que se muestra. Y también hagamos que acepte un increment propiedad, por lo que los consumidores de este componente web pueden incrementarlo en 2, 3, 4 a la vez. Y para impulsar estos cambios de estado, usemos nuestro nuevo contador en una zona de pruebas Svelte: llegaremos a Reaccionar en un momento.

Comenzaremos con el mismo componente web que antes y agregaremos un atributo de color. Para configurar nuestro componente web para aceptar y responder a un atributo, agregamos un atributo estático observedAttributes propiedad que devuelve los atributos que escucha nuestro componente web.

static observedAttributes = ["color"];

Con eso en su lugar, podemos agregar un attributeChangedCallback método de ciclo de vida, que se ejecutará cada vez que cualquiera de los atributos enumerados en observedAttributes están configurados o actualizados.

attributeChangedCallback(name, oldValue, newValue) { if (name === "color") { this.update(); }
}

Ahora actualizamos nuestro update método para usarlo realmente:

update() { this.valSpan.innerText = this._currentValue; this.valSpan.style.color = this.getAttribute("color") || "black";
}

Por último, agreguemos nuestro increment propiedad:

increment = 1;

Sencillo y humilde.

Usando el componente de contador en Svelte

Usemos lo que acabamos de hacer. Entraremos en nuestro componente de aplicación Svelte y agregaremos algo como esto:

<script> let color = "red";
</script> <style> main { text-align: center; }
</style> <main> <select bind:value={color}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> <counter-wc color={color}></counter-wc>
</main>

¡Y funciona! Nuestro contador se renderiza, incrementa y el menú desplegable actualiza el color. Como puede ver, representamos el atributo de color en nuestra plantilla Svelte y, cuando cambia el valor, Svelte se encarga del trabajo preliminar de llamar setAttribute en nuestra instancia de componente web subyacente. No hay nada especial aquí: esto es lo mismo que ya hace con los atributos de cualquier Elemento HTML.

Las cosas se ponen un poco interesantes con el increment apuntalar. Esto es no un atributo en nuestro componente web; es un accesorio en la clase del componente web. Eso significa que debe configurarse en la instancia del componente web. Tenga paciencia conmigo, ya que las cosas terminarán mucho más simples en un momento.

Primero, agregaremos algunas variables a nuestro componente Svelte:

let increment = 1;
let wcInstance;

Nuestra potencia de un componente de contador le permitirá incrementar en 1 o en 2:

<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>

Pero, En teoria, necesitamos obtener la instancia real de nuestro componente web. Esto es lo mismo que siempre hacemos cada vez que agregamos un ref con Reaccionar. Con Svelte, es un simple bind:this directiva:

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Ahora, en nuestra plantilla Svelte, escuchamos los cambios en la variable de incremento de nuestro componente y establecemos la propiedad del componente web subyacente.

$: { if (wcInstance) { wcInstance.increment = increment; }
}

Puedes probarlo en esta demostración en vivo.

Obviamente, no queremos hacer esto para cada componente web o accesorio que necesitemos administrar. ¿No sería bueno si pudiéramos establecer increment directamente en nuestro componente web, en el marcado, como lo hacemos normalmente para accesorios de componentes, y tenerlo, ya sabes, solo trabajo? En otras palabras, sería bueno si pudiéramos eliminar todos los usos de wcInstance y use este código más simple en su lugar:

<counter-wc increment={increment} color={color}></counter-wc>

Resulta que podemos. Este código funciona; Svelte se encarga de todo ese trabajo preliminar por nosotros. Compruébalo en esta demostración. Este es un comportamiento estándar para casi todos los marcos de JavaScript.

Entonces, ¿por qué te mostré la forma manual de configurar la propiedad del componente web? Dos razones: es útil entender cómo funcionan estas cosas y, hace un momento, dije que esto funciona para "prácticamente" todos los marcos de JavaScript. Pero hay un marco que, de manera enloquecedora, no admite la configuración de accesorios de componentes web como acabamos de ver.

React es una bestia diferente

Creación de componentes web interoperables que incluso funcionan con React PlatoBlockchain Data Intelligence. Búsqueda vertical. Ai.
Creación de componentes web interoperables que incluso funcionan con React

Reaccionar. El marco de JavaScript más popular del planeta no admite la interoperabilidad básica con componentes web. Este es un problema bien conocido que es exclusivo de React. Curiosamente, esto en realidad está solucionado en la rama experimental de React, pero por alguna razón no se fusionó con la versión 18. Dicho esto, aún podemos seguir el progreso de la misma. Y puedes probar esto tú mismo con un demo en vivo.

La solución, por supuesto, es utilizar un ref, tome la instancia del componente web y configure manualmente increment cuando ese valor cambia. Se parece a esto:

import React, { useState, useRef, useEffect } from 'react';
import './counter-wc'; export default function App() { const [increment, setIncrement] = useState(1); const [color, setColor] = useState('red'); const wcRef = useRef(null); useEffect(() => { wcRef.current.increment = increment; }, [increment]); return ( <div> <div className="increment-container"> <button onClick={() => setIncrement(1)}>Increment by 1</button> <button onClick={() => setIncrement(2)}>Increment by 2</button> </div> <select value={color} onChange={(e) => setColor(e.target.value)}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc> </div> );
}

Como comentamos, codificar esto manualmente para cada propiedad de componente web simplemente no es escalable. Pero no todo está perdido porque tenemos un par de opciones.

Opción 1: usar atributos en todas partes

Tenemos atributos. Si hizo clic en la demostración de React anterior, el increment prop no funcionaba, pero el color cambió correctamente. ¿No podemos codificar todo con atributos? Tristemente no. Los valores de atributo solo pueden ser cadenas. Eso es lo suficientemente bueno aquí, y podríamos llegar un poco lejos con este enfoque. números como increment se puede convertir a y desde cadenas. Podríamos incluso JSON stringificar/analizar objetos. Pero eventualmente tendremos que pasar una función a un componente web, y en ese momento nos quedaremos sin opciones.

Opción 2: Envuélvelo

Hay un viejo dicho que dice que puedes resolver cualquier problema en informática agregando un nivel de direccionamiento indirecto (excepto el problema de demasiados niveles de direccionamiento indirecto). El código para configurar estos accesorios es bastante predecible y simple. ¿Y si lo escondemos en una biblioteca? La gente inteligente detrás de Lit tener una solución. Esta biblioteca crea un nuevo componente React para usted después de que le proporcione un componente web y enumere las propiedades que necesita. Si bien inteligente, no soy un fan de este enfoque.

En lugar de tener un mapeo uno a uno de los componentes web a los componentes React creados manualmente, lo que prefiero es simplemente una Componente de reacción que le pasamos a nuestro componente web. nombre de etiqueta a (counter-wc en nuestro caso), junto con todos los atributos y propiedades, y para que este componente represente nuestro componente web, agregue el ref, luego descubra qué es un accesorio y qué es un atributo. Esa es la solución ideal en mi opinión. No conozco una biblioteca que haga esto, pero debería ser fácil de crear. ¡Démosle una oportunidad!

Este es el personal estamos buscando:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag es el nombre de la etiqueta del componente web; el resto son las propiedades y atributos que queremos transmitir.

Así es como se ve mi implementación:

import React, { createElement, useRef, useLayoutEffect, memo } from 'react'; const _WcWrapper = (props) => { const { wcTag, children, ...restProps } = props; const wcRef = useRef(null); useLayoutEffect(() => { const wc = wcRef.current; for (const [key, value] of Object.entries(restProps)) { if (key in wc) { if (wc[key] !== value) { wc[key] = value; } } else { if (wc.getAttribute(key) !== value) { wc.setAttribute(key, value); } } } }); return createElement(wcTag, { ref: wcRef });
}; export const WcWrapper = memo(_WcWrapper);

La línea más interesante está al final:

return createElement(wcTag, { ref: wcRef });

Así es como creamos un elemento en React con un nombre dinámico. De hecho, esto es en lo que React normalmente transpila JSX. Todos nuestros divs se convierten a createElement("div") llamadas Normalmente no necesitamos llamar a esta API directamente, pero está ahí cuando la necesitamos.

Más allá de eso, queremos ejecutar un efecto de diseño y recorrer cada accesorio que hemos pasado a nuestro componente. Los repasamos todos y verificamos si se trata de una propiedad con un in check que comprueba el objeto de la instancia del componente web, así como su cadena de prototipos, que detectará cualquier getter/setter que termine en el prototipo de la clase. Si no existe tal propiedad, se supone que es un atributo. En cualquier caso, solo lo establecemos si el valor realmente ha cambiado.

Si se pregunta por qué usamos useLayoutEffect en lugar de useEffect, es porque queremos ejecutar inmediatamente estas actualizaciones antes de que se represente nuestro contenido. Además, tenga en cuenta que no tenemos una matriz de dependencia para nuestro useLayoutEffect; esto significa que queremos ejecutar esta actualización en cada renderizado. Esto puede ser arriesgado ya que React tiende a volver a renderizar bastante. Mejoro esto envolviendo todo en React.memo. Esta es esencialmente la versión moderna de React.PureComponent, lo que significa que el componente solo se volverá a renderizar si alguno de sus accesorios reales ha cambiado, y verifica si eso sucedió a través de una simple verificación de igualdad.

El único riesgo aquí es que si está pasando un accesorio de objeto que está mutando directamente sin reasignarlo, entonces no verá las actualizaciones. Pero esto está muy desaconsejado, especialmente en la comunidad de React, así que no me preocuparía por eso.

Antes de continuar, me gustaría mencionar una última cosa. Es posible que no esté satisfecho con el aspecto del uso. Nuevamente, este componente se usa así:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Específicamente, es posible que no le guste pasar el nombre de la etiqueta del componente web al <WcWrapper> componente y prefieren en su lugar el @lit-labs/react paquete anterior, que crea un nuevo componente React individual para cada componente web. Eso es totalmente justo y te animo a que uses lo que te resulte más cómodo. Pero para mí, una ventaja de este enfoque es que es fácil borrar. Si por algún milagro React fusiona el manejo adecuado de componentes web de su rama experimental en main mañana, podrá cambiar el código anterior de este:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

…a esto:

<counter-wc ref={wcRef} increment={increment} color={color} />

Probablemente incluso podrías escribir un solo codemod para hacer eso en todas partes, y luego eliminar <WcWrapper> en total. En realidad, elimine eso: una búsqueda global y reemplazar con un RegEx probablemente funcionaría.

La implementación

Lo sé, parece que tomó un viaje para llegar aquí. Si recuerda, nuestro objetivo original era tomar el código de vista previa de la imagen que vimos en mi loadingy muévalo a un componente web para que pueda usarse en cualquier marco de JavaScript. La falta de interoperabilidad adecuada de React agregó muchos detalles a la mezcla. Pero ahora que tenemos un manejo decente sobre cómo crear un componente web y usarlo, la implementación será casi anticlimática.

Dejaré caer todo el componente web aquí y mencionaré algunas de las partes interesantes. Si desea verlo en acción, aquí hay un demostración de trabajo. Cambiará entre mis tres libros favoritos sobre mis tres lenguajes de programación favoritos. La URL de cada libro será única cada vez, por lo que puede ver la vista previa, aunque es probable que desee acelerar las cosas en la pestaña Red de DevTools para ver realmente cómo suceden las cosas.

Ver código completo
class BookCover extends HTMLElement { static observedAttributes = ['url']; attributeChangedCallback(name, oldValue, newValue) { if (name === 'url') { this.createMainImage(newValue); } } set preview(val) { this.previewEl = this.createPreview(val); this.render(); } createPreview(val) { if (typeof val === 'string') { return base64Preview(val); } else { return blurHashPreview(val); } } createMainImage(url) { this.loaded = false; const img = document.createElement('img'); img.alt = 'Book cover'; img.addEventListener('load', () =&gt; { if (img === this.imageEl) { this.loaded = true; this.render(); } }); img.src = url; this.imageEl = img; } connectedCallback() { this.render(); } render() { const elementMaybe = this.loaded ? this.imageEl : this.previewEl; syncSingleChild(this, elementMaybe); }
}

Primero, registramos el atributo que nos interesa y reaccionamos cuando cambia:

static observedAttributes = ['url']; attributeChangedCallback(name, oldValue, newValue) { if (name === 'url') { this.createMainImage(newValue); }
}

Esto hace que se cree nuestro componente de imagen, que se mostrará solo cuando se cargue:

createMainImage(url) { this.loaded = false; const img = document.createElement('img'); img.alt = 'Book cover'; img.addEventListener('load', () => { if (img === this.imageEl) { this.loaded = true; this.render(); } }); img.src = url; this.imageEl = img;
}

Luego tenemos nuestra propiedad de vista previa, que puede ser nuestra cadena de vista previa base64 o nuestra blurhash paquete:

set preview(val) { this.previewEl = this.createPreview(val); this.render();
} createPreview(val) { if (typeof val === 'string') { return base64Preview(val); } else { return blurHashPreview(val); }
}

Esto difiere a cualquier función auxiliar que necesitemos:

function base64Preview(val) { const img = document.createElement('img'); img.src = val; return img;
} function blurHashPreview(preview) { const canvasEl = document.createElement('canvas'); const { w: width, h: height } = preview; canvasEl.width = width; canvasEl.height = height; const pixels = decode(preview.blurhash, width, height); const ctx = canvasEl.getContext('2d'); const imageData = ctx.createImageData(width, height); imageData.data.set(pixels); ctx.putImageData(imageData, 0, 0); return canvasEl;
}

Y, por último, nuestro render método:

connectedCallback() { this.render();
} render() { const elementMaybe = this.loaded ? this.imageEl : this.previewEl; syncSingleChild(this, elementMaybe);
}

Y algunos métodos de ayuda para unir todo:

export function syncSingleChild(container, child) { const currentChild = container.firstElementChild; if (currentChild !== child) { clearContainer(container); if (child) { container.appendChild(child); } }
} export function clearContainer(el) { let child; while ((child = el.firstElementChild)) { el.removeChild(child); }
}

Es un poco más repetitivo de lo que necesitaríamos si construimos esto en un marco, pero la ventaja es que podemos reutilizarlo en cualquier marco que queramos, aunque React necesitará un contenedor por ahora, como discutimos. .

Retazos

Ya he mencionado el envoltorio React de Lit. Pero si te encuentras usando Stencil, en realidad es compatible con un tubería de salida separada solo para React. Y la buena gente de Microsoft también ha creó algo similar al envoltorio de Lit, adjunto a la biblioteca de componentes web rápidos.

Como mencioné, todos los marcos que no se llamen React se encargarán de configurar las propiedades del componente web por usted. Solo tenga en cuenta que algunos tienen algunos sabores especiales de sintaxis. Por ejemplo, con Solid.js, <your-wc value={12}> siempre asume que value es una propiedad, que puede anular con un attr prefijo, como <your-wc attr:value={12}>.

Terminando

Los componentes web son una parte interesante, a menudo infrautilizada, del panorama del desarrollo web. Pueden ayudar a reducir su dependencia de cualquier marco de JavaScript único al administrar su interfaz de usuario o componentes de "hoja". Si bien crearlos como componentes web, a diferencia de los componentes Svelte o React, no será tan ergonómico, la ventaja es que serán ampliamente reutilizables.


Creación de componentes web interoperables que incluso funcionan con React publicado originalmente el Trucos CSS. Debieras obtener el boletín.

Sello de tiempo:

Mas de Trucos CSS