Bruke webkomponenter med Next (eller et hvilket som helst SSR-rammeverk)

I min Forrige innlegg vi så på Shoelace, som er et komponentbibliotek med en full pakke med UX-komponenter som er vakre, tilgjengelige og – kanskje uventet – bygget med Nettkomponenter. Dette betyr at de kan brukes med et hvilket som helst JavaScript-rammeverk. Mens Reacts webkomponent-interoperabilitet for tiden er mindre enn ideell, det finnes løsninger.

Men en alvorlig mangel ved Web Components er deres nåværende mangel på støtte for server-side rendering (SSR). Det er noe som kalles Declarative Shadow DOM (DSD) på gang, men nåværende støtte for det er ganske minimalt, og det krever faktisk innkjøp fra webserveren din for å sende ut spesiell markering for DSD. Det jobbes for tiden for Next.js som jeg gleder meg til å se. Men for dette innlegget skal vi se på hvordan du administrerer webkomponenter fra ethvert SSR-rammeverk, som Next.js, i dag.

Vi vil ende opp med å gjøre en ikke-triviell mengde manuelt arbeid, og litt skade siden vårs oppstartsytelse i prosessen. Vi skal deretter se på hvordan vi kan minimere disse ytelseskostnadene. Men gjør ingen feil: denne løsningen er ikke uten avveininger, så ikke forvent noe annet. Mål og profiler alltid.

Problemet

Før vi dykker inn, la oss ta et øyeblikk og faktisk forklare problemet. Hvorfor fungerer ikke nettkomponenter bra med gjengivelse på serversiden?

Applikasjonsrammeverk som Next.js tar React-kode og kjører den gjennom en API for å "strengifisere" den, noe som betyr at den gjør komponentene dine til vanlig HTML. Så React-komponenttreet vil gjengis på serveren som er vert for nettappen, og den HTML-koden vil bli sendt ned sammen med resten av webappens HTML-dokument til brukerens nettleser. Sammen med denne HTML er noen tagger som laster inn React, sammen med koden for alle dine React-komponenter. Når en nettleser behandler disse -tagger, vil React gjengi komponenttreet på nytt og matche ting med SSR-koden som ble sendt ned. På dette tidspunktet vil alle effektene begynne å kjøre, hendelsesbehandlerne vil koble opp, og staten vil faktisk ... inneholde tilstand. Det er på dette tidspunktet webappen blir interaktiv. Prosessen med å behandle komponenttreet på nytt på klienten og koble opp alt kalles hydrering.

Så, hva har dette med webkomponenter å gjøre? Vel, når du gjengir noe, si det samme skolissene komponenten vi besøkte siste 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 ærlig talt noen JavaScript-rammeverket) vil se disse kodene og bare sende dem videre. React (eller Svelte eller Solid) er ikke ansvarlige for å gjøre disse kodene til pent formaterte faner. Koden for det er gjemt i den koden du har som definerer disse webkomponentene. I vårt tilfelle er den koden i Shoelace-biblioteket, men koden kan være hvor som helst. Det som er viktig er når koden kjører.

Normalt vil koden som registrerer disse nettkomponentene trekkes inn i programmets vanlige kode via JavaScript import. Det betyr at denne koden vil havne i JavaScript-pakken din og kjøres under hydrering, noe som betyr at mellom at brukeren først ser SSR'd HTML og hydrering skjer, vil disse fanene (eller en hvilken som helst nettkomponent for den saks skyld) ikke gjengi riktig innhold . Deretter, når hydrering skjer, vil det riktige innholdet vises, noe som sannsynligvis får innholdet rundt disse nettkomponentene til å bevege seg rundt og passe til riktig formatert innhold. Dette er kjent som en glimt av ustilt innhold, eller FOUC. I teorien kan du feste markup mellom alle disse koder for å matche den ferdige produksjonen, men dette er nesten umulig i praksis, spesielt for et tredjeparts komponentbibliotek som Shoelace.

Flytting av registreringskoden for nettkomponenter

Så problemet er at koden for å få webkomponenter til å gjøre det de trenger å gjøre, faktisk ikke kjøres før hydrering skjer. For dette innlegget skal vi se på å kjøre den koden tidligere; umiddelbart, faktisk. Vi skal se på tilpasset bunting av webkomponentkoden, og manuelt legge til et skript direkte i dokumentets så det kjører umiddelbart, og blokkerer resten av dokumentet til det gjør det. Dette er vanligvis en forferdelig ting å gjøre. Hele poenget med gjengivelse på serversiden er å ikke blokker siden vår fra å behandles til JavaScript er behandlet. Men når det er gjort, betyr det at, ettersom dokumentet i utgangspunktet gjengir HTML-koden vår fra serveren, vil webkomponentene registreres og vil både umiddelbart og synkront sende ut riktig innhold.

I vårt tilfelle er vi det bare ønsker å kjøre webkomponentregistreringskoden vår i et blokkeringsskript. Denne koden er ikke stor, og vi vil se på å redusere ytelsen betraktelig ved å legge til noen cache-overskrifter for å hjelpe med påfølgende besøk. Dette er ikke en perfekt løsning. Den første gangen en bruker surfer på siden din vil alltid blokkere mens den skriptfilen er lastet. Påfølgende besøk vil cache fint, men denne avveiningen kanskje ikke være mulig for deg – e-handel, noen? Uansett, profiler, mål og ta den riktige avgjørelsen for appen din. Dessuten, i fremtiden er det fullt mulig at Next.js vil støtte DSD og webkomponenter fullt ut.

Komme i gang

All koden vi skal se på er i denne GitHub-repoen og utplassert her med Vercel. Nettappen gjengir noen skolisserkomponenter sammen med tekst som endrer farge og innhold ved hydrering. Du skal kunne se teksten endres til "Hydrated", med skolissene-komponentene som allerede er riktig gjengitt.

Tilpasset bunting av webkomponentkode

Vårt første trinn er å lage en enkelt JavaScript-modul som importerer alle webkomponentdefinisjonene våre. For skolissene jeg bruker, ser koden min slik ut:

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 laster inn definisjonene for og komponenter, og overstyrer noen standardanimasjoner for dialogen. Enkelt nok. Men det interessante her er å få denne koden inn i applikasjonen vår. Vi kan ikke ganske enkelt import denne modulen. Hvis vi gjorde det, ville det bli samlet inn i våre vanlige JavaScript-pakker og kjørt under hydrering. Dette vil føre til FOUC vi prøver å unngå.

Mens Next.js har en rekke webpack-kroker for å tilpasse ting, skal jeg bruke Bor i stedet. Først, installer den med npm i vite og deretter lage en vite.config.js fil. Min ser slik ut:

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 pakkefil med våre webkomponentdefinisjoner i shoelace-dir mappe. La oss flytte det over til public mappen slik at Next.js vil tjene den. Og vi bør også holde styr på det nøyaktige navnet på filen, med hashen på slutten av den. Her er et nodeskript som flytter filen og skriver en JavaScript-modul som eksporterer en enkel konstant med navnet på pakkefilen (dette vil komme til nytte 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 ledsager-npm-skript:

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

Det burde fungere. For meg, util/shoelace-bundle-info.js eksisterer nå, og ser slik ut:

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

Laster skriptet

La oss gå inn på Next.js _document.js fil og trekk inn navnet på nettkomponentpakkefilen vår:

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

Deretter gjengir vi manuelt en tag i . Her er det hele mitt _document.js filen ser ut som:

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

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

Og det burde funke! Shoelace-registreringen vår vil lastes inn i et blokkeringsskript og være tilgjengelig umiddelbart ettersom siden vår behandler den første HTML-en.

Forbedre ytelsen

Vi kan la ting være som de er, men la oss legge til caching for skolissene våre. Vi vil fortelle Next.js å ​​gjøre disse Shoelace-buntene bufringsbare ved å legge til følgende oppføring i vår Next.js-konfigurasjonsfil:

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

Nå, på etterfølgende søk til nettstedet vårt, ser vi Shoelace-bunten cache pent!

DevTools Sources-panelet åpnes og viser den innlastede Shoelace-pakken.
Bruke webkomponenter med Next (eller et hvilket som helst SSR-rammeverk)

Hvis skolissene våre endres, vil filnavnet endres (via :hash del fra kildeegenskapen ovenfor), vil nettleseren oppdage at den ikke har den filen bufret, og vil ganske enkelt be om den fersk fra nettverket.

Innpakning opp

Dette kan ha virket som mye manuelt arbeid; og det var. Det er uheldig at nettkomponenter ikke tilbyr bedre ut-av-boksen-støtte for gjengivelse på serversiden.

Men vi bør ikke glemme fordelene de gir: det er fint å kunne bruke kvalitets UX-komponenter som ikke er knyttet til et spesifikt rammeverk. Det er fint å kunne eksperimentere med helt nye rammer, som solid, uten å måtte finne (eller hacke sammen) en slags fane, modal, autofullføring eller hvilken som helst komponent.

Tidstempel:

Mer fra CSS triks