Webcomponenten gebruiken met Next (of een ander SSR-framework)

Naar mijn vorige post we hebben gekeken naar Shoelace, een componentenbibliotheek met een volledige reeks UX-componenten die mooi, toegankelijk en - misschien onverwacht - gebouwd met Webcomponenten. Dit betekent dat ze kunnen worden gebruikt met elk JavaScript-framework. Hoewel de interoperabiliteit van de webcomponenten van React op dit moment niet ideaal is, er zijn oplossingen.

Maar een ernstige tekortkoming van Web Components is hun huidige gebrek aan ondersteuning voor server-side rendering (SSR). Er is iets in de maak dat de Declarative Shadow DOM (DSD) wordt genoemd, maar de huidige ondersteuning ervoor is vrij minimaal, en het vereist in feite buy-in van je webserver om speciale opmaak voor de DSD uit te zenden. Er wordt momenteel gewerkt voor Next.js die ik graag zie. Maar voor dit bericht zullen we bekijken hoe u webcomponenten kunt beheren vanuit elk SSR-framework, zoals Next.js, vandaag.

We zullen uiteindelijk een niet-triviale hoeveelheid handwerk doen, en licht de opstartprestaties van onze pagina in het proces schaden. Vervolgens bekijken we hoe we deze prestatiekosten kunnen minimaliseren. Maar vergis u niet: deze oplossing is niet zonder compromissen, dus verwacht niet anders. Altijd meten en profileren.

Het probleem

Voordat we erin duiken, laten we even de tijd nemen om het probleem daadwerkelijk uit te leggen. Waarom werken webcomponenten niet goed met server-side rendering?

Toepassingsframeworks zoals Next.js nemen React-code en voeren het door een API om het in wezen te "stringificeren", wat betekent dat het uw componenten in gewone HTML verandert. Dus de React-componentenstructuur wordt weergegeven op de server die de webapp host en die HTML wordt samen met de rest van het HTML-document van de webapp naar de browser van uw gebruiker gestuurd. Samen met deze HTML zijn enkele tags die React laden, samen met de code voor al je React-componenten. Wanneer een browser deze verwerkt tags, zal React de componentenboom opnieuw renderen en de dingen matchen met de SSR'd HTML die naar beneden is gestuurd. Op dit punt zullen alle effecten beginnen te werken, de gebeurtenis-handlers zullen worden aangesloten en de status zal feitelijk ... de status bevatten. Op dit punt wordt de web-app interactieve. Het proces van het opnieuw verwerken van uw componentenboom op de client en het aansluiten van alles wordt genoemd hydratatie.

Dus, wat heeft dit te maken met Web Components? Nou, als je iets rendert, zeg dan dezelfde schoenveter onderdeel dat we bezochten laatste keer:


   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.

โ€ฆReageer (of eerlijk gezegd) elke JavaScript-framework) zullen die tags zien en ze gewoon doorgeven. React (of Svelte of Solid) is niet verantwoordelijk voor het omzetten van die tags in mooi opgemaakte tabbladen. De code daarvoor is weggestopt in de code die je hebt die die webcomponenten definieert. In ons geval staat die code in de Shoelace-bibliotheek, maar de code kan overal staan. Wat belangrijk is, is: wanneer de code wordt uitgevoerd.

Normaal gesproken wordt de code die deze webcomponenten registreert, via een JavaScript in de normale code van uw toepassing getrokken import. Dat betekent dat deze code in uw JavaScript-bundel terechtkomt en wordt uitgevoerd tijdens hydratatie, wat betekent dat, tussen het moment dat uw gebruiker de SSR'd HTML ziet en hydratatie plaatsvindt, deze tabbladen (of welke webcomponent dan ook) niet de juiste inhoud weergeven . Wanneer hydratatie plaatsvindt, wordt de juiste inhoud weergegeven, waardoor de inhoud rond deze webcomponenten waarschijnlijk zal bewegen en in de correct opgemaakte inhoud past. Dit staat bekend als a flits van niet-gestileerde inhoud, of FOUC. In theorie zou je tussen al die markeringen kunnen plakken tags die overeenkomen met de voltooide uitvoer, maar dit is in de praktijk vrijwel onmogelijk, vooral voor een componentenbibliotheek van derden zoals Shoelace.

Onze registratiecode voor webcomponenten verplaatsen

Het probleem is dus dat de code om Web Components te laten doen wat ze moeten doen, pas echt wordt uitgevoerd als er hydratatie optreedt. Voor dit bericht zullen we kijken naar het eerder uitvoeren van die code; onmiddellijk, eigenlijk. We zullen kijken naar het op maat bundelen van onze Web Component-code en het handmatig toevoegen van een script rechtstreeks aan onze documenten dus het wordt onmiddellijk uitgevoerd en blokkeert de rest van het document totdat het werkt. Dit is normaal gesproken verschrikkelijk om te doen. Het hele punt van server-side rendering is om: niet blokkeer onze pagina voor verwerking totdat onze JavaScript is verwerkt. Maar als het eenmaal klaar is, betekent dit dat, aangezien het document onze HTML in eerste instantie vanaf de server weergeeft, de webcomponenten worden geregistreerd en zowel onmiddellijk als synchroon de juiste inhoud uitzenden.

In ons geval zijn we voor slechts onze Web Component-registratiecode in een blokkerend script willen uitvoeren. Deze code is niet enorm en we zullen proberen de prestatiehit aanzienlijk te verminderen door enkele cacheheaders toe te voegen om te helpen bij volgende bezoeken. Dit is geen perfecte oplossing. De eerste keer dat een gebruiker door uw pagina bladert, wordt altijd geblokkeerd terwijl dat scriptbestand wordt geladen. Volgende bezoeken zullen mooi in de cache worden opgeslagen, maar deze afweging misschien niet haalbaar zijn voor u - e-commerce, iemand? Hoe dan ook, profileer, meet en neem de juiste beslissing voor uw app. Bovendien is het in de toekomst heel goed mogelijk dat Next.js DSD en Web Components volledig zal ondersteunen.

Aan de slag

Alle code die we zullen bekijken is binnen deze GitHub-opslagplaats en hier ingezet met Vercel. De web-app geeft sommige schoenvetercomponenten weer samen met tekst die van kleur en inhoud verandert bij hydratatie. U zou de tekst moeten kunnen zien veranderen in 'Gehydrateerd', waarbij de onderdelen van de schoenveter al correct worden weergegeven.

Aangepaste bundeling Web Component-code

Onze eerste stap is het maken van een enkele JavaScript-module die al onze Web Component-definities importeert. Voor de schoenvetercomponenten die ik gebruik, ziet mijn code er als volgt uit:

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

Het laadt de definities voor de en componenten en overschrijft enkele standaardanimaties voor het dialoogvenster. Simpel genoeg. Maar het interessante hier is om deze code in onze applicatie te krijgen. Wij kan niet eenvoudigweg import deze module. Als we dat zouden doen, zou het in onze normale JavaScript-bundels worden gebundeld en tijdens hydratatie worden uitgevoerd. Dit zou de FOUC veroorzaken die we proberen te vermijden.

Hoewel Next.js een aantal webpack-haken heeft om dingen op maat te bundelen, gebruik ik schroef in plaats van. Installeer het eerst met npm i vite en maak vervolgens een vite.config.js het dossier. De mijne ziet er zo uit:

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

Dit zal een bundelbestand bouwen met onze Web Component-definities in de shoelace-dir map. Laten we het verplaatsen naar de public map zodat Next.js het zal dienen. En we moeten ook de exacte naam van het bestand bijhouden, met de hash aan het einde ervan. Hier is een Node-script dat het bestand verplaatst en een JavaScript-module schrijft die een eenvoudige constante exporteert met de naam van het bundelbestand (dit zal binnenkort van pas komen):

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

Hier is een begeleidend npm-script:

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

Dat moet werken. Voor mij, util/shoelace-bundle-info.js bestaat nu en ziet er als volgt uit:

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

Het script laden

Laten we ingaan op de Next.js _document.js bestand en trek de naam van ons Web Component-bundelbestand in:

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

Dan renderen we handmatig a tag in de . Dit is wat mijn hele _document.js bestand ziet eruit als:

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

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

En dat moet lukken! Onze schoenveterregistratie wordt geladen in een blokkeerscript en is onmiddellijk beschikbaar wanneer onze pagina de initiรซle HTML verwerkt.

Prestaties verbeteren

We kunnen de dingen laten zoals ze zijn, maar laten we caching toevoegen voor onze Shoelace-bundel. We zullen Next.js vertellen om deze schoenveterbundels cachebaar te maken door het volgende item toe te voegen aan ons Next.js-configuratiebestand:

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

Nu, bij de volgende bezoeken aan onze site, zien we dat de Shoelace-bundel goed in de cache wordt opgeslagen!

DevTools Sources-paneel wordt geopend en toont de geladen schoenveterbundel.
Webcomponenten gebruiken met Next (of een ander SSR-framework)

Als onze Schoenveterbundel ooit verandert, verandert de bestandsnaam (via de :hash gedeelte van de broneigenschap hierboven), zal de browser ontdekken dat het bestand dat bestand niet in de cache heeft staan, en het gewoon vers van het netwerk opvragen.

Afsluiten

Dit leek misschien veel handwerk; en het was. Het is jammer dat Web Components geen betere kant-en-klare ondersteuning biedt voor server-side rendering.

Maar we mogen de voordelen die ze bieden niet vergeten: het is fijn om hoogwaardige UX-componenten te kunnen gebruiken die niet gebonden zijn aan een specifiek framework. Het is ook leuk om te kunnen experimenteren met gloednieuwe frameworks, zoals Solid, zonder dat je een soort tabblad, modaal, automatisch aanvullen of welk onderdeel dan ook hoeft te vinden (of samen te hacken).

Tijdstempel:

Meer van CSS-trucs