Brug af webkomponenter med Next (eller ethvert SSR-rammeværk)

I min forrige indlæg vi så på Shoelace, som er et komponentbibliotek med en komplet suite af UX-komponenter, der er smukke, tilgængelige og – måske uventet – bygget med Webkomponenter. Det betyder, at de kan bruges med enhver JavaScript-ramme. Mens Reacts webkomponent-interoperabilitet på nuværende tidspunkt er mindre end ideel, der er løsninger.

Men en alvorlig mangel ved Web Components er deres nuværende mangel på understøttelse af server-side rendering (SSR). Der er noget, der kaldes Declarative Shadow DOM (DSD) på vej, men den nuværende support til det er ret minimal, og det kræver faktisk buy-in fra din webserver for at udsende speciel markup til DSD'en. Der arbejdes pt Next.js som jeg glæder mig til at se. Men til dette indlæg vil vi se på, hvordan man administrerer webkomponenter fra enhver SSR-ramme, som Next.js, i dag.

Vi ender med at udføre en ikke-triviel mængde manuelt arbejde, og anelse skade vores sides opstartsydelse i processen. Vi vil derefter se på, hvordan man minimerer disse ydelsesomkostninger. Men tag ikke fejl: denne løsning er ikke uden afvejninger, så forvent ikke andet. Mål og profilér altid.

Problemet

Før vi dykker ind, lad os tage et øjeblik og faktisk forklare problemet. Hvorfor fungerer webkomponenter ikke godt sammen med gengivelse på serversiden?

Applikationsrammer som Next.js tager React-kode og kører den gennem en API for i det væsentlige at "strengificere" den, hvilket betyder, at den forvandler dine komponenter til almindelig HTML. Så React-komponenttræet gengives på serveren, der hoster webappen, og den HTML vil blive sendt ned sammen med resten af ​​webappens HTML-dokument til din brugers browser. Sammen med denne HTML er nogle tags, der indlæser React, sammen med koden for alle dine React-komponenter. Når en browser behandler disse tags, vil React gengive komponenttræet og matche tingene med den SSR'd HTML, der blev sendt ned. På dette tidspunkt vil alle effekter begynde at køre, hændelseshandlerne vil koble op, og tilstanden vil faktisk... indeholde tilstand. Det er på dette tidspunkt, at webappen bliver til interaktiv. Processen med at genbehandle dit komponenttræ på klienten og ledningsføring af alt kaldes hydrering.

Så hvad har dette at gøre med webkomponenter? Nå, når du gengiver noget, så sig det samme snørebånd komponent, vi besøgte sidste gang:


   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.

…Reager (eller ærligt enhver JavaScript-ramme) vil se disse tags og blot sende dem videre. React (eller Svelte eller Solid) er ikke ansvarlige for at omdanne disse tags til pænt formaterede faner. Koden til det er gemt væk inde i den kode du har, der definerer disse webkomponenter. I vores tilfælde er den kode i Shoelace-biblioteket, men koden kan være hvor som helst. Det der er vigtigt er når koden kører.

Normalt vil koden, der registrerer disse webkomponenter, blive trukket ind i din applikations normale kode via en JavaScript import. Det betyder, at denne kode vil ende i din JavaScript-pakke og udføres under hydrering, hvilket betyder, at mellem din bruger først ser SSR'en HTML og hydrering sker, vil disse faner (eller enhver webkomponent for den sags skyld) ikke gengive det korrekte indhold . Derefter, når der sker hydrering, vises det korrekte indhold, hvilket sandsynligvis får indholdet omkring disse webkomponenter til at flytte rundt og passe til det korrekt formaterede indhold. Dette er kendt som en glimt af ustylet indholdeller FOUC. I teorien kan du sætte opmærkning imellem alle disse tags for at matche det færdige output, men dette er næsten umuligt i praksis, især for et tredjeparts komponentbibliotek som Shoelace.

Flytning af vores webkomponent registreringskode

Så problemet er, at koden til at få webkomponenter til at gøre, hvad de skal gøre, faktisk ikke kører, før der opstår hydrering. Til dette indlæg vil vi se på at køre den kode før; umiddelbart, faktisk. Vi vil se på tilpasset bundtning af vores webkomponentkode og manuelt tilføje et script direkte til vores dokuments så det kører med det samme og blokerer resten af ​​dokumentet, indtil det gør det. Dette er normalt en forfærdelig ting at gøre. Hele pointen med server-side rendering er at ikke blokere vores side fra at blive behandlet, indtil vores JavaScript er behandlet. Men når det er gjort, betyder det, at da dokumentet i første omgang gengiver vores HTML fra serveren, vil webkomponenterne blive registreret og vil både øjeblikkeligt og synkront udsende det rigtige indhold.

I vores tilfælde er vi lige søger at køre vores webkomponent registreringskode i et blokerende script. Denne kode er ikke enorm, og vi vil se på at reducere ydeevnen markant ved at tilføje nogle cache-headere for at hjælpe med efterfølgende besøg. Dette er ikke en perfekt løsning. Første gang en bruger gennemser din side, vil den altid blokere, mens scriptfilen indlæses. Efterfølgende besøg vil cache pænt, men denne afvejning måske ikke være muligt for dig - e-handel, nogen? Uanset hvad, profilér, mål og tag den rigtige beslutning for din app. Desuden er det i fremtiden fuldt ud muligt, at Next.js fuldt ud vil understøtte DSD og webkomponenter.

Kom godt i gang

Al den kode, vi skal se på, er i denne GitHub-repo , indsat her med Vercel. Webappen gengiver nogle snørebåndskomponenter sammen med tekst, der ændrer farve og indhold ved hydrering. Du burde kunne se teksten ændre sig til "Hydrated", hvor snørebåndskomponenterne allerede gengives korrekt.

Brugerdefineret bundling webkomponentkode

Vores første skridt er at oprette et enkelt JavaScript-modul, der importerer alle vores webkomponentdefinitioner. For de snørebåndskomponenter, jeg bruger, ser min kode sådan ud:

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

Den indlæser definitionerne for , komponenter og tilsidesætter nogle standardanimationer for dialogen. Simpelt nok. Men det interessante her er at få denne kode ind i vores applikation. Vi kan ikke ganske enkelt import dette modul. Hvis vi gjorde det, ville det blive samlet i vores normale JavaScript-bundter og køre under hydrering. Dette ville forårsage den FOUC, vi forsøger at undgå.

Mens Next.js har en række webpack-kroge til at tilpasse ting, vil jeg bruge Lives i stedet. Først skal du installere det med npm i vite og derefter oprette en vite.config.js fil. Min ser sådan ud:

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

Dette vil bygge en bundlefil med vores webkomponentdefinitioner i shoelace-dir folder. Lad os flytte det over til public mappe, så Next.js vil tjene den. Og vi bør også holde styr på det nøjagtige navn på filen, med hashen i slutningen af ​​den. Her er et Node-script, der flytter filen og skriver et JavaScript-modul, der eksporterer en simpel konstant med navnet på bundle-filen (dette vil være praktisk om kort tid):

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

Her er et ledsagende npm-script:

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

Det burde virke. For mig, util/shoelace-bundle-info.js eksisterer nu og ser sådan ud:

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

Indlæser scriptet

Lad os gå ind i Next.js _document.js fil og træk navnet på vores webkomponent-pakkefil:

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

Derefter gengiver vi manuelt en tag i . Her er hvad hele min _document.js fil ser sådan ud:

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

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

Og det burde virke! Vores snørebåndsregistrering indlæses i et blokerende script og er tilgængelig med det samme, da vores side behandler den indledende HTML.

Forbedring af ydeevne

Vi kunne lade tingene være, som de er, men lad os tilføje caching til vores snørebåndsbundt. Vi vil fortælle Next.js at gøre disse Shoelace-bundter cachebare ved at tilføje følgende indgang til vores Next.js-konfigurationsfil:

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

Nu, på efterfølgende gennemgange af vores side, ser vi Shoelace-bundtet cache pænt!

DevTools-panelet Kilder åbnes og viser det indlæste snørebåndsbundt.
Brug af webkomponenter med Next (eller ethvert SSR-rammeværk)

Hvis vores snørebåndspakke nogensinde ændres, ændres filnavnet (via :hash del fra kildeegenskaben ovenfor), vil browseren opdage, at den ikke har den fil cachelagret, og vil blot anmode om den frisk fra netværket.

Indpakning op

Dette kan have virket som en masse manuelt arbejde; og det var det. Det er uheldigt, at webkomponenter ikke tilbyder bedre out-of-the-box support til server-side rendering.

Men vi bør ikke glemme de fordele, de giver: det er rart at kunne bruge kvalitets UX-komponenter, der ikke er bundet til en bestemt ramme. Det er aldo rart at kunne eksperimentere med helt nye rammer, som f.eks Solid, uden at skulle finde (eller hacke sammen) en slags fane, modal, autofuldførelse eller hvilken som helst komponent.

Tidsstempel:

Mere fra CSS-tricks