Uso de componentes web con Next (o cualquier marco SSR) | trucos CSS
En mi publicación anterior, analizamos Shoelace, que es una biblioteca de componentes con un conjunto completo de componentes UX que son hermosos, accesibles y, quizás inesperadamente, creados con componentes web. Esto significa que se pueden usar con cualquier marco de JavaScript. Si bien la interoperabilidad actual de los componentes web de React es menos que ideal, existen soluciones alternativas.
Pero uno de los principales inconvenientes de los componentes web es su actual falta de compatibilidad con la representación del lado del servidor (SSR). Hay algo llamado Declarative Shadow DOM (DSD) en desarrollo, pero el soporte actual es bastante mínimo y en realidad requiere la compra de su servidor web para transmitir un marcado especial para DSD. Siguiente.js que estoy deseando ver. Pero para esta publicación, veremos cómo administrar componentes web desde cualquier marco SSR como Next.js, Este Dia.
Terminaremos haciendo una cantidad no trivial de trabajo manual y ligeramente perjudica el rendimiento al iniciar nuestra página en el proceso. A continuación, veremos cómo minimizar estos costos de rendimiento. Pero no se equivoque: esta solución no está exenta de compromisos, así que no espere nada más. Mide y perfila siempre.
El problema
Antes de sumergirnos, tomemos un momento y expliquemos el problema. ¿Por qué los componentes web no funcionan bien con la representación del lado del servidor?
Los marcos de aplicaciones como Next.js toman el código React y lo ejecutan a través de una API para "formarlo", lo que significa que convierte sus componentes en HTML simple. Entonces, el árbol de componentes React se representará en el servidor que aloja la aplicación web, y este HTML enviarse junto con el resto del documento HTML de la aplicación web al navegador de su usuario. Junto con este HTML, hay algunos <script>
etiquetas que cargan React junto con el código de todos sus componentes de React cuando un navegador los procesa <script>
etiquetas, React volverá a representar el árbol de componentes y asignará las cosas al HTML enviado a SSR. En este punto, todos los efectos comenzarán a ejecutarse, los controladores de eventos se vincularán y el estado en realidad... contendrá el estado. En este punto, la aplicación web se convierte en interactivoSe invoca el proceso de volver a procesar el árbol de componentes de su cliente y conectar todo hidratación.
Entonces, ¿qué tiene esto que ver con los componentes web? Bueno, cuando te estés imaginando algo, di el mismo cordón <sl-tab-group>
componente que visitamos la última vez:
<sl-tab-group ref="{tabsRef}">
<sl-tab slot="nav" panel="general"> General </sl-tab>
<sl-tab slot="nav" panel="custom"> Custom </sl-tab>
<sl-tab slot="nav" panel="advanced"> Advanced </sl-tab>
<sl-tab slot="nav" panel="disabled" disabled> Disabled </sl-tab>
<sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
<sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
<sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
<sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>
… Reaccionar (o honestamente cualquier tipo Marco de JavaScript) verá estas etiquetas y simplemente las pasará. React (o Svelte o Solid) no es responsable de convertir estas etiquetas en secciones bien formateadas. El código para esto está oculto dentro de cualquier código que tenga que defina estos componentes web. En nuestro caso, este código está en la biblioteca de Shoelace, pero el código puede estar en cualquier lugar. Lo importante es cuando se ejecuta el código.
Por lo general, el código que registra estos componentes web se incorporará a su código de aplicación normal a través de JavaScript. import
Esto significa que este código terminará en su paquete de JavaScript y se ejecutará durante la hidratación, lo que significa que entre que el usuario vea por primera vez el HTML de SSR y se produzca la hidratación, estas pestañas (o cualquier componente web para el caso) no mostrarán el correcto Luego, cuando se produzca la hidratación, se mostrará el contenido correcto, lo que posiblemente provoque que el contenido alrededor de esos componentes web se mueva y se ajuste al contenido con el formato adecuado. Esto se conoce como destello de contenido sin estiloo FOUC. En teoría, podrías poner un marcado entre todos ellos. <sl-tab-xyz>
etiquetas para que coincidan con la salida final, pero esto es casi imposible en la práctica, especialmente para una biblioteca de componentes de terceros como Shoelace.
Mover el código de registro a nuestro componente web
Entonces, el problema es que el código que hace que los componentes web hagan lo que se supone que deben hacer, en realidad no funcionará hasta que se produzca la hidratación. Para esta publicación, veremos cómo ejecutar este código anteriormente; en realidad inmediatamente. Veremos la agrupación personalizada de nuestro código de componente web y la adición manual de secuencias de comandos directamente a nuestro documento <head>
por lo que comienza de inmediato y bloquea el resto del documento hasta que lo haga. Esto suele ser algo terrible. El objetivo de la representación del lado del servidor es sí no bloquear el procesamiento de nuestra página hasta que se procese nuestro JavaScript. Pero una vez hecho esto, significa que dado que el documento está procesando inicialmente nuestro HTML desde el servidor, los componentes web se registrarán y emitirán de forma inmediata y sincrónica el contenido correcto.
En nuestro caso, somos sólo estamos buscando ejecutar nuestro código de registro de componente web en un script de bloqueo. Este código no es enorme, e intentaremos reducir significativamente el impacto en el rendimiento agregando algunos encabezados de caché para ayudar con visitas de seguimiento. Esta no es la solución ideal. La primera vez que un usuario ve su página, siempre se bloqueará mientras se carga este archivo de script. Las visitas subsiguientes guardarán bien el caché, pero esta compensación tal vez no ser factible para usted: comercio electrónico, ¿alguien? De todos modos, perfile, mida y tome la decisión correcta para su aplicación. Además, es muy posible que Next.js sea totalmente compatible con DSD y componentes web en el futuro.
Nos estamos preparando para empezar
Todo el código que veremos está dentro este repositorio de GitHub y implementado aquí con VercelLa aplicación web muestra algunos componentes de Shoelace junto con texto que cambia de color y contenido cuando se hidrata. Debería poder ver el cambio de texto a "Hidratado" con los componentes Shoelace ahora renderizados correctamente.
Código de componente web de empaquetado opcional
Nuestro primer paso es crear un único módulo de JavaScript que importe todas nuestras definiciones de componentes web. Para los componentes de Shoelace que estoy usando, mi código se ve así:
import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";
import "@shoelace-style/shoelace/dist/components/tab/tab.js";
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
setDefaultAnimation("dialog.show", {
keyframes: [
{ opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
{ opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
],
options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
keyframes: [
{ opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
{ opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
],
options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
Carga las definiciones de <sl-tab-group>
y <sl-dialog>
componentes y reemplaza algunas animaciones predeterminadas para el cuadro de diálogo. Suficientemente simple. Pero lo interesante aquí es introducir este código en nuestra aplicación. Nosotros No puedo sólo import
este módulo. Si hacemos esto, se empaquetará en nuestros paquetes JavaScript normales y se ejecutará durante la hidratación. Esto conducirá al FOUC que estamos tratando de evitar.
Aunque Next.js tiene una serie de ganchos de paquete web para paquetes personalizados, usaré Vite en su lugar. Primero, instálelo con npm i vite
y luego crear un vite.config.js
El mío se ve así:
import { defineConfig } from "vite";
import path from "path";
export default defineConfig({
build: {
outDir: path.join(__dirname, "./shoelace-dir"),
lib: {
name: "shoelace",
entry: "./src/shoelace-bundle.js",
formats: ["umd"],
fileName: () => "shoelace-bundle.js",
},
rollupOptions: {
output: {
entryFileNames: `[name]-[hash].js`,
},
},
},
});
Esto creará un archivo por lotes con nuestras definiciones de componentes web en el shoelace-dir
Vamos a moverlo a public
carpeta para que Next.js la sirva. Y también necesitamos realizar un seguimiento del nombre exacto del archivo, con el hash al final. Aquí hay una secuencia de comandos de nodo que mueve el archivo y escribe un módulo de JavaScript que exporta una constante simple con el nombre del archivo por lotes (esto será útil pronto):
const fs = require("fs");
const path = require("path");
const shoelaceOutputPath = path.join(process.cwd(), "shoelace-dir");
const publicShoelacePath = path.join(process.cwd(), "public", "shoelace");
const files = fs.readdirSync(shoelaceOutputPath);
const shoelaceBundleFile = files.find(name => /^shoelace-bundle/.test(name));
fs.rmSync(publicShoelacePath, { force: true, recursive: true });
fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.join(shoelaceOutputPath, shoelaceBundleFile), path.join(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { force: true, recursive: true });
fs.writeFileSync(path.join(process.cwd(), "util", "shoelace-bundle-info.js"), `export const shoelacePath = "/shoelace/${shoelaceBundleFile}";`);
Aquí hay un script npm adjunto:
"bundle-shoelace": "vite build && node util/process-shoelace-bundle",
Esto debería funcionar. Sobre mí, util/shoelace-bundle-info.js
ahora existe y se ve así:
export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";
Cargando el guión
Vamos a Next.js _document.js
archivo y descargue el nombre de nuestro archivo de paquete de componentes web:
import { shoelacePath } from "../util/shoelace-bundle-info";
Luego trazamos manualmente un <script>
etiqueta en <head>
Eso es lo que es mi todo _document.js
el archivo se ve así:
import { Html, Head, Main, NextScript } from "next/document";
import { shoelacePath } from "../util/shoelace-bundle-info";
export default function Document() {
return (
<Html>
<Head>
<script src={shoelacePath}></script>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
¡Y eso debería funcionar! Nuestro registro de Sholace se cargará en un script de bloqueo y estará disponible inmediatamente cuando nuestra página procese el HTML inicial.
Mejorar la productividad
Podemos dejar las cosas como están, pero agreguemos almacenamiento en caché para nuestro paquete Shoelace. Le indicaremos a Next.js que haga que estos paquetes de Shoelace se puedan almacenar en caché agregando la siguiente entrada a nuestro archivo de configuración de Next.js:
async headers() {
return [
{
source: "/shoelace/shoelace-bundle-:hash.js",
headers: [
{
key: "Cache-Control",
value: "public,max-age=31536000,immutable",
},
],
},
];
}
Ahora, en posteriores navegaciones a nuestro sitio, ¡vemos que el paquete Shoelace se almacena bien en caché!
Si nuestro paquete Shoelace alguna vez cambia, el nombre del archivo cambiará (a través de :hash
parte de la propiedad de origen anterior), el navegador detectará que este archivo no está en caché y simplemente lo solicitará desde la web.
resumiendo
Esto puede haber parecido mucho trabajo manual, y lo fue. Es una pena que los componentes web no ofrezcan un mejor soporte listo para usar para la representación del lado del servidor.
Pero no debemos olvidar los beneficios que brindan: es bueno poder usar componentes UX de calidad que no están vinculados a un marco específico. También es bueno poder experimentar con nuevos marcos como Sólidosin tener que buscar (o piratear juntos) ninguna pestaña, modal, autocompletar o cualquier componente.
Deja una respuesta