Verwenden von Webkomponenten mit Next (oder einem beliebigen SSR-Framework)

In meinem previous post Wir haben uns Shoelace angesehen, eine Komponentenbibliothek mit einer vollständigen Suite von UX-Komponenten, die schön, zugänglich und – vielleicht unerwartet – mit ihnen gebaut sind Webkomponenten. Das bedeutet, dass sie mit jedem JavaScript-Framework verwendet werden können. Während die Interoperabilität der Webkomponenten von React derzeit alles andere als ideal ist, es gibt workarounds.

Ein schwerwiegender Mangel von Webkomponenten ist jedoch der derzeitige Mangel an Unterstützung für serverseitiges Rendering (SSR). Es gibt etwas namens Declarative Shadow DOM (DSD) in Arbeit, aber die derzeitige Unterstützung dafür ist ziemlich gering, und es erfordert tatsächlich eine Einzahlung von Ihrem Webserver, um ein spezielles Markup für das DSD auszugeben. Es wird derzeit daran gearbeitet Next.js auf das ich mich freue. Aber in diesem Beitrag sehen wir uns an, wie Webkomponenten von jedem SSR-Framework wie Next.js verwaltet werden. heute.

Wir werden am Ende eine nicht triviale Menge an manueller Arbeit erledigen, und leicht dabei die Startleistung unserer Seite beeinträchtigen. Anschließend sehen wir uns an, wie diese Leistungskosten minimiert werden können. Aber machen Sie keinen Fehler: Diese Lösung ist nicht ohne Kompromisse, also erwarten Sie nichts anderes. Immer messen und profilieren.

Das Problem

Bevor wir eintauchen, nehmen wir uns einen Moment Zeit und erklären das Problem tatsächlich. Warum funktionieren Webkomponenten nicht gut mit serverseitigem Rendering?

Anwendungsframeworks wie Next.js nehmen React-Code und führen ihn über eine API aus, um ihn im Wesentlichen zu „stringifizieren“, was bedeutet, dass Ihre Komponenten in einfaches HTML umgewandelt werden. Der React-Komponentenbaum wird also auf dem Server gerendert, der die Web-App hostet, und dieser HTML-Code wird mit dem Rest des HTML-Dokuments der Web-App an den Browser Ihres Benutzers gesendet. Zusammen mit diesem HTML sind einige Tags, die React laden, zusammen mit dem Code für alle Ihre React-Komponenten. Wenn ein Browser diese verarbeitet -Tags rendert React den Komponentenbaum neu und gleicht die Dinge mit dem SSR-HTML ab, das gesendet wurde. An diesem Punkt beginnen alle Effekte zu laufen, die Event-Handler werden verbunden und der Zustand wird tatsächlich … den Zustand enthalten. An diesem Punkt wird die Web-App interaktive. Der Prozess der erneuten Verarbeitung Ihres Komponentenbaums auf dem Client und der gesamten Verkabelung wird aufgerufen Hydratation.

Was hat das also mit Webkomponenten zu tun? Nun, wenn Sie etwas rendern, sagen Sie gleich Schnürsenkel Komponente, die wir besucht haben letzten Mal:


   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.

…reagieren (oder ehrlich jedem JavaScript-Framework) sieht diese Tags und gibt sie einfach weiter. React (oder Svelte oder Solid) sind nicht dafür verantwortlich, diese Tags in schön formatierte Registerkarten umzuwandeln. Der Code dafür ist in Ihrem Code versteckt, der diese Webkomponenten definiert. In unserem Fall befindet sich dieser Code in der Shoelace-Bibliothek, aber der Code kann überall sein. Wichtig ist wenn der Code ausgeführt wird.

Normalerweise wird der Code, der diese Webkomponenten registriert, über ein JavaScript in den normalen Code Ihrer Anwendung gezogen import. Das bedeutet, dass dieser Code in Ihrem JavaScript-Bundle landet und während der Hydratation ausgeführt wird, was bedeutet, dass diese Registerkarten (oder andere Webkomponenten für diese Angelegenheit) nicht den richtigen Inhalt darstellen, wenn Ihr Benutzer zum ersten Mal das SSR-HTML sieht und die Hydratation stattfindet . Wenn dann Hydratation stattfindet, wird der richtige Inhalt angezeigt, was wahrscheinlich dazu führt, dass sich der Inhalt um diese Webkomponenten herum bewegt und an den richtig formatierten Inhalt passt. Dies ist als bekannt Flash von ungestylten Inhalten, oder FOUC. Theoretisch könnten Sie Markup zwischen all diesen einfügen -Tags, um mit der fertigen Ausgabe übereinzustimmen, aber das ist in der Praxis so gut wie unmöglich, insbesondere für eine Komponentenbibliothek eines Drittanbieters wie Shoelace.

Verschieben unseres Registrierungscodes für die Webkomponente

Das Problem ist also, dass der Code, der Webkomponenten dazu bringt, das zu tun, was sie tun müssen, nicht wirklich ausgeführt wird, bis die Hydratation eintritt. In diesem Beitrag werden wir uns mit der Ausführung dieses Codes früher befassen. sofort, in der Tat. Wir betrachten das benutzerdefinierte Bündeln unseres Webkomponentencodes und das manuelle Hinzufügen eines Skripts direkt zu unserem Dokument Daher wird es sofort ausgeführt und blockiert den Rest des Dokuments, bis dies der Fall ist. Das ist normalerweise eine schreckliche Sache. Der springende Punkt beim serverseitigen Rendern ist nicht die Verarbeitung unserer Seite blockieren, bis unser JavaScript verarbeitet wurde. Aber sobald dies erledigt ist, bedeutet dies, dass die Webkomponenten registriert werden und sowohl sofort als auch synchron den richtigen Inhalt ausgeben, da das Dokument zunächst unseren HTML-Code vom Server rendert.

In unserem Fall sind wir nur Wir möchten unseren Registrierungscode für Webkomponenten in einem blockierenden Skript ausführen. Dieser Code ist nicht riesig, und wir werden versuchen, den Leistungseinbruch erheblich zu verringern, indem wir einige Cache-Header hinzufügen, um bei späteren Besuchen zu helfen. Dies ist keine perfekte Lösung. Das erste Mal, wenn ein Benutzer Ihre Seite durchsucht, wird immer blockiert, während diese Skriptdatei geladen wird. Nachfolgende Besuche werden gut zwischengespeichert, aber dieser Kompromiss möglicherweise nicht für Sie machbar sein – E-Commerce, irgendjemand? Erstellen Sie auf jeden Fall ein Profil, messen Sie und treffen Sie die richtige Entscheidung für Ihre App. Außerdem ist es in Zukunft durchaus möglich, dass Next.js DSD und Webkomponenten vollständig unterstützt.

Erste Schritte

Der gesamte Code, den wir uns ansehen werden, ist drin dieses GitHub Repo und hier mit Vercel eingesetzt. Die Web-App rendert einige Shoelace-Komponenten zusammen mit Text, der Farbe und Inhalt bei Hydratation ändert. Sie sollten sehen können, dass sich der Text in „Hydrated“ ändert, wobei die Shoelace-Komponenten bereits richtig gerendert werden.

Code für benutzerdefinierte Bündelungs-Webkomponenten

Unser erster Schritt besteht darin, ein einzelnes JavaScript-Modul zu erstellen, das alle unsere Webkomponentendefinitionen importiert. Für die von mir verwendeten Shoelace-Komponenten sieht mein Code folgendermaßen aus:

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

Es lädt die Definitionen für die und Komponenten und überschreibt einige Standardanimationen für das Dialogfeld. Einfach genug. Aber das Interessante daran ist, diesen Code in unsere Anwendung zu bekommen. Wir kann keine einfach import dieses Modul. Wenn wir das tun würden, würde es in unsere normalen JavaScript-Bundles gebündelt und während der Hydration ausgeführt. Dies würde zu dem FOUC führen, den wir zu vermeiden versuchen.

Während Next.js eine Reihe von Webpack-Hooks zum benutzerdefinierten Bündeln von Dingen hat, werde ich sie verwenden Schraube stattdessen. Installieren Sie es zuerst mit npm i vite und dann erstellen Sie eine vite.config.js Datei. Meine sieht so aus:

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

Dadurch wird eine Bundle-Datei mit unseren Webkomponenten-Definitionen in der erstellt shoelace-dir Mappe. Verschieben wir es auf die public Ordner, damit Next.js ihn bereitstellt. Und wir sollten uns auch den genauen Namen der Datei mit dem Hash am Ende merken. Hier ist ein Node-Skript, das die Datei verschiebt und ein JavaScript-Modul schreibt, das eine einfache Konstante mit dem Namen der Bundle-Datei exportiert (dies wird sich in Kürze als nützlich erweisen):

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 ist ein begleitendes npm-Skript:

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

Das sollte funktionieren. Für mich, util/shoelace-bundle-info.js existiert jetzt und sieht so aus:

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

Laden des Skripts

Gehen wir in die Next.js _document.js Datei und ziehen Sie den Namen unserer Webkomponenten-Bundle-Datei hinein:

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

Dann rendern wir manuell a Tag in der . Hier ist, was mein gesamtes _document.js Datei sieht aus wie:

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

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

Und das sollte funktionieren! Unsere Shoelace-Registrierung wird in einem Sperrskript geladen und ist sofort verfügbar, wenn unsere Seite den ursprünglichen HTML-Code verarbeitet.

Verbessernde Leistung

Wir könnten die Dinge so lassen, wie sie sind, aber fügen wir Caching für unser Schnürsenkel-Bundle hinzu. Wir weisen Next.js an, diese Schnürsenkel-Bundles cachebar zu machen, indem wir unserer Next.js-Konfigurationsdatei den folgenden Eintrag hinzufügen:

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

Bei späteren Besuchen unserer Website sehen wir nun, dass das Schnürsenkel-Bundle gut zwischengespeichert wird!

DevTools-Quellenbereich geöffnet und zeigt das geladene Schnürsenkel-Bundle an.
Verwenden von Webkomponenten mit Next (oder einem beliebigen SSR-Framework)

Wenn sich unser Schnürsenkel-Bundle jemals ändert, ändert sich der Dateiname (über die :hash Teil aus der Source-Eigenschaft oben), wird der Browser feststellen, dass er diese Datei nicht zwischengespeichert hat, und wird sie einfach neu vom Netzwerk anfordern.

Wrapping up

Dies mag wie eine Menge Handarbeit erschienen sein; und es war. Es ist bedauerlich, dass Webkomponenten keine bessere standardmäßige Unterstützung für das serverseitige Rendern bieten.

Aber wir sollten die Vorteile nicht vergessen, die sie bieten: Es ist schön, hochwertige UX-Komponenten verwenden zu können, die nicht an ein bestimmtes Framework gebunden sind. Es ist auch schön, mit brandneuen Frameworks experimentieren zu können, wie z Solid, ohne eine Art Registerkarte, Modal, Autovervollständigung oder andere Komponenten finden (oder zusammenhacken) zu müssen.

Zeitstempel:

Mehr von CSS-Tricks