Logotipo de Zephyrnet

Uso de componentes web con Next (o cualquier marco SSR)

Fecha:

En mi Publicación anterior miramos 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 del componente web de React es, en la actualidad, menos que ideal, hay soluciones.

Pero una deficiencia grave 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 proceso, pero el soporte actual es bastante mínimo, y en realidad requiere la compra de su servidor web para emitir un marcado especial para el DSD. Actualmente se está trabajando para Next.js que estoy deseando ver. Pero para esta publicación, veremos cómo administrar los componentes web desde cualquier marco SSR, como Next.js, hoy.

Terminaremos haciendo una cantidad no trivial de trabajo manual, y ligeramente dañando el rendimiento de inicio de nuestra página en el proceso. Luego veremos cómo minimizar estos costos de rendimiento. Pero no se equivoque: esta solución no está exenta de compensaciones, así que no espere lo contrario. 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 esencialmente "formarlo", lo que significa que convierte sus componentes en HTML simple. Entonces, el árbol de componentes de React se representará en el servidor que aloja la aplicación web, y ese HTML se enviará con el resto del documento HTML de la aplicación web al navegador de su usuario. Junto con este HTML hay algunos etiquetas que cargan React, junto con el código para todos sus componentes de React. Cuando un navegador procesa estos tags, React volverá a representar el árbol de componentes y hará coincidir las cosas con el HTML de SSR que se envió. En este punto, todos los efectos comenzarán a ejecutarse, los controladores de eventos se conectarán y el estado en realidad... contendrá el estado. Es en este punto que la aplicación web se vuelve interactivo. El proceso de volver a procesar su árbol de componentes en el cliente y conectar todo se llama hidratación.

Entonces, ¿qué tiene esto que ver con los componentes web? Bueno, cuando renderices algo, di lo mismo Shoelace componente que visitamos último tiempo:


   General 
   Custom 
   Advanced 
   Disabled 

  This is the general tab panel.
  This is the custom tab panel.
  This is the advanced tab panel.
  This is a disabled tab panel.

…Reaccionar (o honestamente cualquier marco de JavaScript) verá esas etiquetas y simplemente las pasará. React (o Svelte o Solid) no son responsables de convertir esas etiquetas en pestañas bien formateadas. El código para eso está escondido dentro de cualquier código que tenga que defina esos componentes web. En nuestro caso, ese 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.

Normalmente, el código que registra estos componentes web se incorporará al código normal de su aplicación a través de un código JavaScript. import. Eso 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) no mostrarán el contenido correcto. . Luego, cuando se produzca la hidratación, se mostrará el contenido adecuado, lo que probablemente hará que el contenido alrededor de estos 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ía colocar marcas entre todos esos 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.

Moviendo nuestro código de registro de Componente Web

Entonces, el problema es que el código para hacer que los componentes web hagan lo que deben hacer no se ejecutará hasta que se produzca la hidratación. Para esta publicación, veremos cómo ejecutar ese código antes; inmediatamente, de hecho. Veremos la agrupación personalizada de nuestro código de componente web y la adición manual de un script directamente a nuestro documento. por lo que se ejecuta inmediatamente y bloquea el resto del documento hasta que lo haga. Esto es normalmente algo terrible de hacer. El objetivo de la representación del lado del servidor es no bloquee el procesamiento de nuestra página hasta que nuestro JavaScript se haya procesado. Pero una vez hecho esto, significa que, como el documento está renderizando 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, estamos solo buscando ejecutar nuestro código de registro de componente web en un script de bloqueo. Este código no es enorme y buscaremos reducir significativamente el impacto en el rendimiento agregando algunos encabezados de caché para ayudar con las visitas posteriores. Esta no es una solución perfecta. La primera vez que un usuario navega por su página siempre se bloqueará mientras se carga ese archivo de script. Las visitas posteriores se almacenarán en caché muy bien, 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, en el futuro es muy posible que Next.js sea totalmente compatible con DSD y Web Components.

Cómo comenzar

Todo el código que veremos está en este repositorio de GitHub y implementado aquí con Vercel. La aplicación web representa algunos componentes de Shoelace junto con texto que cambia de color y contenido al hidratarse. Debería poder ver el cambio de texto a "Hidratado", con los componentes de Shoelace ya representados correctamente.

Código de componente web de empaquetado personalizado

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 para el y componentes y anula 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 puede simplemente import este módulo. Si hiciéramos eso, se incluiría en nuestros paquetes de JavaScript normales y se ejecutaría durante la hidratación. Esto causaría el FOUC que estamos tratando de evitar.

Si bien Next.js tiene una serie de ganchos de paquete web para empaquetar cosas personalizadas, usaré tornillo en cambio. Primero, instálelo con npm i vite y luego crea un vite.config.js expediente. 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 de paquete con nuestras definiciones de componentes web en el shoelace-dir carpeta. Pasemos a la public carpeta para que Next.js la sirva. Y también debemos hacer un seguimiento del nombre exacto del archivo, con el hash al final. Aquí hay una secuencia de comandos de Node que mueve el archivo y escribe un módulo de JavaScript que exporta una constante simple con el nombre del archivo del paquete (esto será útil en breve):

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 complementario:

"bundle-shoelace": "vite build && node util/process-shoelace-bundle",

Eso debería funcionar. Para mi, util/shoelace-bundle-info.js ahora existe, y se ve así:

export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";

Cargando el guión

Vayamos a Next.js _document.js e ingrese el nombre de nuestro archivo de paquete de componentes web:

import { shoelacePath } from "../util/shoelace-bundle-info";

Luego renderizamos manualmente un etiqueta en el . Aquí está todo mi _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 (
    
      
        
      
      
        
); }

¡Y eso debería funcionar! Nuestro registro de Shoelace se cargará en un script de bloqueo y estará disponible inmediatamente cuando nuestra página procese el HTML inicial.

Mejorando el desempeño

Podríamos dejar las cosas como están, pero agreguemos almacenamiento en caché para nuestro paquete Shoelace. Le diremos 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 las siguientes búsquedas en nuestro sitio, ¡vemos que el paquete Shoelace se almacena en caché muy bien!

El panel Fuentes de DevTools se abre y muestra el paquete Shoelace cargado.

Si nuestro paquete Shoelace cambia alguna vez, el nombre del archivo cambiará (a través de la :hash parte de la propiedad de origen anterior), el navegador encontrará que no tiene ese archivo almacenado en caché y simplemente lo solicitará nuevo de la red.

Terminando

Esto puede haber parecido mucho trabajo manual; y fue. Es desafortunado 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 agradable poder experimentar con nuevos marcos, como Sólido, sin necesidad de encontrar (o piratear juntos) algún tipo de pestaña, modal, autocompletar o cualquier componente.

punto_img

Información más reciente

punto_img