Utilizarea componentelor web cu Next (sau orice cadru SSR)

În mea anterior mesaj ne-am uitat la Shoelace, care este o bibliotecă de componente cu o suită completă de componente UX care sunt frumoase, accesibile și – poate în mod neașteptat – construite cu Componente web. Aceasta înseamnă că pot fi utilizate cu orice cadru JavaScript. În timp ce interoperabilitatea componentelor web a React este, în prezent, mai puțin decât ideală, există soluții alternative.

Dar un dezavantaj serios al componentelor web este lipsa lor actuală de suport pentru randarea pe server (SSR). Există ceva numit Declarative Shadow DOM (DSD) în lucru, dar suportul actual pentru acesta este destul de minim și, de fapt, necesită acceptare de la serverul dvs. web pentru a emite un marcaj special pentru DSD. În prezent se lucrează pentru Next.js pe care abia astept sa-l vad. Dar pentru această postare, vom analiza cum să gestionăm componentele web din orice cadru SSR, cum ar fi Next.js, astăzi.

Vom ajunge să facem o cantitate nebanală de muncă manuală și puțin afectarea performanței de pornire a paginii noastre în acest proces. Apoi vom analiza cum să minimizăm aceste costuri de performanță. Dar nu vă înșelați: această soluție nu este lipsită de compromisuri, așa că nu vă așteptați la altceva. Măsurați și profilați întotdeauna.

Problema

Înainte de a ne scufunda, să luăm un moment și să explicăm problema. De ce Componentele Web nu funcționează bine cu randarea pe server?

Cadrele de aplicație precum Next.js preiau codul React și îl rulează printr-un API pentru a-l „stringe”, în esență, ceea ce înseamnă că vă transformă componentele în HTML simplu. Deci, arborele componente React va fi redat pe serverul care găzduiește aplicația web, iar acel HTML va fi trimis împreună cu restul documentului HTML al aplicației web către browserul utilizatorului. Alături de acest HTML sunt câteva etichete care încarcă React, împreună cu codul pentru toate componentele dvs. React. Când un browser le procesează etichete, React va reda arborele de componente și va potrivi lucrurile cu HTML-ul SSR care a fost trimis. În acest moment, toate efectele vor începe să ruleze, gestionatorii de evenimente se vor conecta și starea va conține de fapt... starea. În acest moment devine aplicația web interactiv. Procesul de reprocesare a arborelui de componente pe client și de cablare a totul este numit hidratare.

Deci, ce legătură are asta cu componentele web? Ei bine, când redați ceva, spuneți același șiret componenta pe care am vizitat-o ultima dată:


   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.

… Reacționează (sau sincer Orice JavaScript) va vedea acele etichete și le va transmite pur și simplu. React (sau Svelte, sau Solid) nu sunt responsabile pentru transformarea acelor etichete în file frumos formatate. Codul pentru acesta este ascuns în orice cod pe care îl aveți și care definește acele componente web. În cazul nostru, acel cod se află în biblioteca Shoelace, dar codul poate fi oriunde. Ceea ce este important este când rulează codul.

În mod normal, codul care înregistrează aceste componente web va fi introdus în codul normal al aplicației dvs. prin intermediul unui JavaScript import. Aceasta înseamnă că acest cod se va încheia în pachetul dvs. JavaScript și se va executa în timpul hidratării, ceea ce înseamnă că, între utilizatorul dvs. care vede pentru prima dată HTML-ul SSR și se produce hidratarea, aceste file (sau orice componentă web, de altfel) nu vor reda conținutul corect. . Apoi, atunci când are loc hidratarea, se va afișa conținutul adecvat, ceea ce poate face ca conținutul din jurul acestor componente web să se miște și să se potrivească conținutului formatat corespunzător. Aceasta este cunoscută ca a fulger de conținut fără stil, sau FOUC. În teorie, ai putea pune un marcaj între toate acestea etichete pentru a se potrivi cu rezultatul final, dar acest lucru este aproape imposibil în practică, mai ales pentru o bibliotecă de componente terță parte precum Shoelace.

Mutarea codului nostru de înregistrare a componentei web

Deci problema este că codul pentru a face componentele web să facă ceea ce trebuie să facă nu va rula de fapt până când nu apare hidratarea. Pentru această postare, ne vom uita la rularea codului respectiv mai devreme; imediat, de fapt. Ne vom uita la gruparea personalizată a codului nostru de componentă web și la adăugarea manuală a unui script direct în documentul nostru. așa că rulează imediat și blochează restul documentului până când o face. Acesta este în mod normal un lucru groaznic de făcut. Scopul redării pe partea serverului este să nu blocați pagina noastră de la procesarea până când JavaScript este procesat. Dar odată terminat, înseamnă că, deoarece documentul redă inițial HTML-ul nostru de pe server, Componentele Web vor fi înregistrate și vor emite atât imediat cât și sincron conținutul potrivit.

În cazul nostru, suntem doar caută să rulăm codul nostru de înregistrare pentru componentele web într-un script de blocare. Acest cod nu este imens și vom căuta să reducem semnificativ performanța prin adăugarea unor anteturi cache pentru a ajuta la vizitele ulterioare. Aceasta nu este o soluție perfectă. Prima dată când un utilizator navighează pagina dvs. se va bloca întotdeauna în timp ce acel fișier script este încărcat. Vizitele ulterioare vor fi memorate în cache, dar acest compromis nu se va putea să fie fezabil pentru tine — e-commerce, cineva? Oricum, profilați, măsurați și luați decizia corectă pentru aplicația dvs. În plus, în viitor este posibil ca Next.js să suporte pe deplin DSD și componente web.

Noțiuni de bază

Tot codul pe care îl vom analiza este în acest depozit GitHub și dislocat aici cu Vercel. Aplicația web redă unele componente șireturi împreună cu text care își schimbă culoarea și conținutul la hidratare. Ar trebui să puteți vedea că textul se schimbă în „Hidratat”, cu componentele șireturi deja redate corect.

Cod personalizat pentru componentă web

Primul nostru pas este să creăm un singur modul JavaScript care să importe toate definițiile componentelor noastre web. Pentru componentele șireturi pe care le folosesc, codul meu arată astfel:

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

Încarcă definițiile pentru și componente și suprascrie unele animații implicite pentru dialog. Destul de simplu. Dar piesa interesantă aici este introducerea acestui cod în aplicația noastră. Noi nu poti pur şi simplu import acest modul. Dacă am face asta, ar fi inclus în pachetele noastre JavaScript obișnuite și ar rula în timpul hidratării. Acest lucru ar provoca FOUC pe care încercăm să-l evităm.

În timp ce Next.js are o serie de cârlige webpack pentru a grupa lucruri personalizate, voi folosi Vieți in schimb. Mai întâi, instalează-l cu npm i vite și apoi creați un vite.config.js fişier. Al meu arata asa:

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

Acest lucru va crea un fișier pachet cu definițiile componentelor noastre web în shoelace-dir pliant. Să-l mutăm la public folder astfel încât Next.js să-l servească. Și ar trebui să urmărim, de asemenea, numele exact al fișierului, cu hash la sfârșitul acestuia. Iată un script Node care mută fișierul și scrie un modul JavaScript care exportă o constantă simplă cu numele fișierului pachet (acest lucru va fi util în scurt timp):

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

Iată un script npm însoțitor:

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

Asta ar trebui să funcționeze. Pentru mine, util/shoelace-bundle-info.js acum există și arată astfel:

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

Se încarcă scriptul

Să mergem la Next.js _document.js fișier și trageți numele fișierului nostru pachet de componente web:

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

Apoi redăm manual a eticheta în . Iată ce este întregul meu _document.js fisierul arata ca:

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

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

Și asta ar trebui să funcționeze! Înregistrarea noastră se va încărca într-un script de blocare și va fi disponibilă imediat pe măsură ce pagina noastră procesează HTML-ul inițial.

Îmbunătățirea performanței

Am putea lăsa lucrurile așa cum sunt, dar să adăugăm cache pentru pachetul nostru de șireturi. Îi vom spune Next.js să facă aceste pachete de șireturi să poată fi stocate în cache, adăugând următoarea intrare în fișierul nostru de configurare Next.js:

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

Acum, la navigarea ulterioară pe site-ul nostru, vedem pachetul de șireturi de pantofi păstrându-se frumos în cache!

Panoul Surse DevTools este deschis și afișează pachetul de șireturi încărcat.
Utilizarea componentelor web cu Next (sau orice cadru SSR)

Dacă pachetul nostru de șireturi se schimbă vreodată, numele fișierului se va schimba (prin intermediul :hash parte din proprietatea sursă de mai sus), browserul va constata că nu are acel fișier în cache și pur și simplu îl va solicita proaspăt din rețea.

La finalul

Acest lucru poate părea o mulțime de muncă manuală; si a fost. Este regretabil că Componentele Web nu oferă un suport mai bun pentru redarea pe server.

Dar nu ar trebui să uităm de beneficiile pe care le oferă: este plăcut să poți folosi componente UX de calitate care nu sunt legate de un cadru specific. Este foarte frumos să poți experimenta cadre noi, cum ar fi Solid, fără a fi nevoie să găsiți (sau să piratați împreună) un fel de filă, modal, completare automată sau orice componentă.

Timestamp-ul:

Mai mult de la CSS Trucuri