Utilizzo di componenti Web con Next (o qualsiasi framework SSR)

Nel mio post precedente abbiamo esaminato Shoelace, che è una libreria di componenti con una suite completa di componenti UX belli, accessibili e, forse inaspettatamente, costruiti con Componenti Web. Ciò significa che possono essere utilizzati con qualsiasi framework JavaScript. Sebbene l'interoperabilità dei componenti Web di React sia, al momento, tutt'altro che ideale, ci sono soluzioni alternative.

Ma una grave lacuna dei componenti Web è la loro attuale mancanza di supporto per il rendering lato server (SSR). C'è qualcosa chiamato Declarative Shadow DOM (DSD) in lavorazione, ma l'attuale supporto è piuttosto minimo e in realtà richiede il buy-in dal tuo server web per emettere un markup speciale per il DSD. Al momento c'è del lavoro in corso Next.js che non vedo l'ora di vedere. Ma per questo post, vedremo come gestire i componenti Web da qualsiasi framework SSR, come Next.js, oggi.

Finiremo per fare una quantità non banale di lavoro manuale, e leggermente danneggiando le prestazioni di avvio della nostra pagina nel processo. Vedremo quindi come ridurre al minimo questi costi di prestazioni. Ma non commettere errori: questa soluzione non è priva di compromessi, quindi non aspettarti diversamente. Misurare e profilare sempre.

Il problema

Prima di immergerci, prendiamoci un momento e spieghiamo effettivamente il problema. Perché i componenti Web non funzionano bene con il rendering lato server?

Framework applicativi come Next.js prendono il codice React e lo eseguono attraverso un'API per "stringirlo essenzialmente", il che significa che trasforma i tuoi componenti in un semplice HTML. Quindi l'albero dei componenti React verrà visualizzato sul server che ospita l'app Web e l'HTML verrà inviato con il resto del documento HTML dell'app Web al browser dell'utente. Insieme a questo HTML ce ne sono alcuni tag che caricano React, insieme al codice per tutti i tuoi componenti React. Quando un browser li elabora tag, React renderà nuovamente l'albero dei componenti e abbinerà le cose con l'HTML di SSR che è stato inviato. A questo punto, tutti gli effetti inizieranno a essere eseguiti, i gestori di eventi si collegheranno e lo stato in realtà... conterrà lo stato. È a questo punto che l'app web diventa interattivo. Viene chiamato il processo di rielaborazione dell'albero dei componenti sul client e cablaggio di tutto idratazione.

Quindi, cosa ha a che fare questo con i componenti Web? Bene, quando esegui il rendering di qualcosa, dì lo stesso Shoelace componente che abbiamo visitato l'ultima volta:


   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.

…Reagisci (o onestamente in qualsiasi JavaScript) vedrà quei tag e li passerà semplicemente. React (o Svelte, o Solid) non sono responsabili della trasformazione di quei tag in schede ben formattate. Il codice è nascosto all'interno del codice che hai che definisce quei componenti Web. Nel nostro caso, quel codice è nella libreria Shoelace, ma il codice può essere ovunque. L'importante è quando il codice viene eseguito.

Normalmente, il codice che registra questi componenti Web verrà inserito nel codice normale dell'applicazione tramite un JavaScript import. Ciò significa che questo codice finirà nel tuo bundle JavaScript ed verrà eseguito durante l'idratazione, il che significa che, tra il tuo utente che vede per la prima volta l'HTML di SSR e l'idratazione, queste schede (o qualsiasi componente Web per quella materia) non visualizzeranno il contenuto corretto . Quindi, quando si verifica l'idratazione, viene visualizzato il contenuto corretto, che probabilmente fa sì che il contenuto attorno a questi componenti Web si sposti e si adatti al contenuto formattato correttamente. Questo è noto come a lampo di contenuti senza stile, o FOUC. In teoria, potresti inserire un markup tra tutti questi tag per abbinare l'output finito, ma questo è quasi impossibile in pratica, specialmente per una libreria di componenti di terze parti come Shoelace.

Spostamento del nostro codice di registrazione del componente Web

Quindi il problema è che il codice per fare in modo che i componenti Web eseguano ciò che devono fare non verrà effettivamente eseguito fino a quando non si verifica l'idratazione. Per questo post, esamineremo prima l'esecuzione di quel codice; subito, infatti. Esamineremo il raggruppamento personalizzato del codice del componente Web e l'aggiunta manuale di uno script direttamente al nostro documento quindi viene eseguito immediatamente e blocca il resto del documento finché non lo fa. Questa è normalmente una cosa terribile da fare. Il punto centrale del rendering lato server è non blocca l'elaborazione della nostra pagina fino a quando il nostro JavaScript non è stato elaborato. Ma una volta fatto, significa che, poiché il documento sta inizialmente visualizzando il nostro HTML dal server, i componenti Web verranno registrati ed emetteranno immediatamente e in modo sincrono il contenuto corretto.

Nel nostro caso, lo siamo ad appena cercando di eseguire il nostro codice di registrazione del componente Web in uno script di blocco. Questo codice non è enorme e cercheremo di ridurre significativamente il calo delle prestazioni aggiungendo alcune intestazioni della cache per aiutare con le visite successive. Questa non è una soluzione perfetta. La prima volta che un utente esplora la tua pagina si bloccherà sempre durante il caricamento del file di script. Le visite successive verranno memorizzate bene nella cache, ma questo compromesso potrebbe non essere fattibile per te - e-commerce, chiunque? In ogni caso, profila, misura e prendi la decisione giusta per la tua app. Inoltre, in futuro è del tutto possibile che Next.js supporterà completamente DSD e Web Components.

Per iniziare

Tutto il codice che esamineremo è contenuto questo repository GitHub ed schierato qui con Vercel. L'app Web esegue il rendering di alcuni componenti di Shoelace insieme al testo che cambia colore e contenuto durante l'idratazione. Dovresti essere in grado di vedere il testo cambiare in "Idratato", con i componenti di Shoelace già visualizzati correttamente.

Codice del componente Web di raggruppamento personalizzato

Il nostro primo passo è creare un singolo modulo JavaScript che importi tutte le nostre definizioni di componenti Web. Per i componenti di Shoelace che sto usando, il mio codice è simile al seguente:

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)" },
});

Carica le definizioni per il ed componenti e sovrascrive alcune animazioni predefinite per la finestra di dialogo. Abbastanza semplice. Ma il pezzo interessante qui è inserire questo codice nella nostra applicazione. Noi non può semplicemente import questo modulo. Se lo facessimo, verrebbe raggruppato nei nostri normali bundle JavaScript e funzionerebbe durante l'idratazione. Ciò causerebbe il FOUC che stiamo cercando di evitare.

Mentre Next.js ha una serie di hook webpack per raggruppare cose personalizzate, userò rapidamente invece. Innanzitutto, installalo con npm i vite e quindi crea un file vite.config.js file. Il mio si presenta così:

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`,
      },
    },
  },
});

Questo creerà un file bundle con le nostre definizioni dei componenti Web in shoelace-dir cartella. Passiamo al public cartella in modo che Next.js lo serva. E dovremmo anche tenere traccia del nome esatto del file, con l'hash alla fine. Ecco uno script Node che sposta il file e scrive un modulo JavaScript che esporta una semplice costante con il nome del file bundle (questo tornerà utile a 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}";`);

Ecco uno script npm complementare:

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

Dovrebbe funzionare. Per me, util/shoelace-bundle-info.js ora esiste e si presenta così:

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

Caricamento dello script

Entriamo nel Next.js _document.js file e inserisci il nome del nostro file bundle del componente Web:

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

Quindi eseguiamo manualmente il rendering di a tag nel file . Ecco qual è il mio intero _document.js il file ha questo aspetto:

import { Html, Head, Main, NextScript } from "next/document";
import { shoelacePath } from "../util/shoelace-bundle-info";

export default function Document() {
  return (
    
      
        
      
      
        
); }

E dovrebbe funzionare! La nostra registrazione di Shoelace verrà caricata in uno script di blocco e sarà immediatamente disponibile quando la nostra pagina elabora l'HTML iniziale.

Migliorare le prestazioni

Potremmo lasciare le cose come stanno, ma aggiungiamo la memorizzazione nella cache per il nostro bundle Shoelace. Diremo a Next.js di rendere questi bundle di Shoelace memorizzabili nella cache aggiungendo la seguente voce al nostro file di configurazione Next.js:

async headers() {
  return [
    {
      source: "/shoelace/shoelace-bundle-:hash.js",
      headers: [
        {
          key: "Cache-Control",
          value: "public,max-age=31536000,immutable",
        },
      ],
    },
  ];
}

Ora, nelle successive visite al nostro sito, vediamo che il bundle Shoelace viene memorizzato bene nella cache!

Il pannello Sorgenti di DevTools si apre e mostra il pacchetto di lacci delle scarpe caricato.
Utilizzo di componenti Web con Next (o qualsiasi framework SSR)

Se il nostro pacchetto Shoelace cambia, il nome del file cambierà (tramite il file :hash parte dalla proprietà sorgente sopra), il browser scoprirà che non ha quel file memorizzato nella cache e lo richiederà semplicemente fresco dalla rete.

Concludendo

Questo potrebbe essere sembrato un sacco di lavoro manuale; ed esso era. È un peccato che i componenti Web non offrano un miglior supporto pronto all'uso per il rendering lato server.

Ma non dobbiamo dimenticare i vantaggi che forniscono: è bello poter utilizzare componenti UX di qualità che non sono legati a un framework specifico. È bello poter sperimentare framework nuovi di zecca, come Tinte Unite, senza dover trovare (o hackerare insieme) una sorta di scheda, modale, completamento automatico o qualsiasi altro componente.

Timestamp:

Di più da Trucchi CSS