Używanie komponentów internetowych z Next (lub dowolnym frameworkiem SSR)

W moim poprzedni post przyjrzeliśmy się Shoelace, która jest biblioteką komponentów z pełnym zestawem komponentów UX, które są piękne, dostępne i – być może nieoczekiwanie – zbudowane z Komponenty sieciowe. Oznacza to, że mogą być używane z dowolnym frameworkiem JavaScript. Chociaż interoperacyjność Web Components React jest obecnie mniej niż idealna, istnieją obejścia.

Jednak jedną z poważnych wad Web Components jest ich obecny brak obsługi renderowania po stronie serwera (SSR). Pracuje nad czymś, co nazywa się Declarative Shadow DOM (DSD), ale obecne wsparcie dla tego jest dość minimalne i faktycznie wymaga wpisowego z twojego serwera sieciowego, aby wyemitować specjalne znaczniki dla DSD. Obecnie trwają prace nad Next.js którego nie mogę się doczekać. Ale w tym poście przyjrzymy się, jak zarządzać komponentami sieciowymi z dowolnego frameworka SSR, takiego jak Next.js, już dziś.

Skończymy wykonując nietrywialną ilość ręcznej pracy i nieco pogorszenia wydajności uruchamiania naszej strony w tym procesie. Następnie przyjrzymy się, jak zminimalizować te koszty wydajności. Ale nie popełnij błędu: to rozwiązanie nie jest pozbawione kompromisów, więc nie oczekuj inaczej. Zawsze mierz i profiluj.

Problem

Zanim zanurkujemy, poświęćmy chwilę i faktycznie wyjaśnijmy problem. Dlaczego składniki sieci Web nie działają dobrze z renderowaniem po stronie serwera?

Struktury aplikacji, takie jak Next.js, pobierają kod React i uruchamiają go przez API, aby zasadniczo go „stringizować”, co oznacza, że ​​zamienia twoje komponenty w zwykły HTML. Tak więc drzewo komponentów React będzie renderowane na serwerze hostującym aplikację internetową, a kod HTML zostanie wysłany wraz z resztą dokumentu HTML aplikacji internetowej do przeglądarki użytkownika. Wraz z tym kodem HTML są niektóre tagi, które ładują Reacta, wraz z kodem dla wszystkich komponentów Reacta. Gdy przeglądarka je przetwarza tagi, React ponownie wyrenderuje drzewo komponentów i dopasuje je do wysłanego kodu HTML z SSR. W tym momencie wszystkie efekty zaczną działać, procedury obsługi zdarzeń zostaną połączone, a stan faktycznie… będzie zawierał stan. W tym momencie aplikacja internetowa staje się interaktywne. Nazywa się proces ponownego przetwarzania drzewa komponentów na kliencie i okablowania wszystkiego uwodnienie.

Więc co to ma wspólnego z komponentami sieciowymi? Cóż, kiedy coś renderujesz, powiedz to samo sznurowadło komponent, który odwiedziliśmy ostatni raz:


   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.

…Reaguj (lub szczerze każdy JavaScript) zobaczy te tagi i po prostu przekaże je dalej. React (lub Svelte lub Solid) nie są odpowiedzialne za przekształcenie tych tagów w ładnie sformatowane zakładki. Kod do tego jest schowany wewnątrz dowolnego kodu, który definiuje te składniki sieci Web. W naszym przypadku ten kod znajduje się w bibliotece Shoelace, ale kod może być wszędzie. Ważne jest kiedy kod działa.

Zwykle kod rejestrujący te składniki sieci Web zostanie wciągnięty do normalnego kodu aplikacji za pomocą JavaScript import. Oznacza to, że ten kod zostanie umieszczony w pakiecie JavaScript i będzie wykonywany podczas nawadniania, co oznacza, że ​​pomiędzy pierwszym zauważeniem przez użytkownika kodu HTML SSR a nawodnieniem te karty (lub dowolny składnik sieciowy) nie będą renderować prawidłowej zawartości . Następnie, gdy nastąpi nawodnienie, zostanie wyświetlona właściwa zawartość, prawdopodobnie powodując, że zawartość wokół tych składników sieci Web będzie się przemieszczać i dopasowywać do odpowiednio sformatowanej zawartości. Jest to znane jako flash niestylowanej treścilub FOUC. Teoretycznie możesz umieścić znaczniki między tymi wszystkimi znaczniki pasujące do gotowego wyniku, ale w praktyce jest to prawie niemożliwe, zwłaszcza w przypadku biblioteki komponentów innej firmy, takiej jak Shoelace.

Przenoszenie kodu rejestracyjnego naszego komponentu internetowego

Problem polega więc na tym, że kod, który sprawi, że Web Components zrobią to, czego potrzebują, nie uruchomi się, dopóki nie nastąpi uwodnienie. W tym poście przyjrzymy się uruchomieniu tego kodu wcześniej; w rzeczywistości natychmiast. Przyjrzymy się niestandardowemu pakowaniu kodu naszego komponentu sieci Web i ręcznym dodawaniu skryptu bezpośrednio do naszego dokumentu więc działa natychmiast i blokuje resztę dokumentu, dopóki to nie nastąpi. Zwykle jest to straszne. Cały sens renderowania po stronie serwera polega na: nie zablokować przetwarzanie naszej strony do czasu przetworzenia naszego kodu JavaScript. Ale gdy to zrobisz, oznacza to, że ponieważ dokument początkowo renderuje nasz kod HTML z serwera, komponenty sieciowe zostaną zarejestrowane i będą natychmiast i synchronicznie emitować odpowiednią zawartość.

W naszym przypadku jesteśmy właśnie chce uruchomić nasz kod rejestracyjny składnika sieci Web w skrypcie blokującym. Ten kod nie jest duży i postaramy się znacznie zmniejszyć spadek wydajności, dodając kilka nagłówków pamięci podręcznej, aby pomóc w kolejnych wizytach. To nie jest idealne rozwiązanie. Gdy użytkownik po raz pierwszy przegląda twoją stronę, zawsze zostanie zablokowany podczas ładowania pliku skryptu. Kolejne wizyty będą ładnie buforowane, ale to kompromis może nie być wykonalne dla Ciebie — e-commerce, ktoś? W każdym razie profiluj, mierz i podejmij właściwą decyzję dla swojej aplikacji. Poza tym jest całkiem możliwe, że w przyszłości Next.js będzie w pełni wspierał DSD i Web Components.

Pierwsze kroki

Cały kod, na który będziemy patrzeć, jest w to repozytorium GitHub i wdrożone tutaj z Vercel. Aplikacja internetowa renderuje niektóre składniki Shoelace wraz z tekstem, który zmienia kolor i zawartość po nawodnieniu. Powinieneś być w stanie zobaczyć, jak tekst zmienia się na „Nawodniony”, a komponenty Sznurowadła są już poprawnie renderowane.

Niestandardowy kod pakietu Web Component

Naszym pierwszym krokiem jest stworzenie pojedynczego modułu JavaScript, który importuje wszystkie nasze definicje komponentów sieciowych. W przypadku komponentów Shoelace, których używam, mój kod wygląda tak:

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

Ładuje definicje dla i komponentów i nadpisuje niektóre domyślne animacje okna dialogowego. Wystarczająco proste. Ale interesującym elementem jest wprowadzenie tego kodu do naszej aplikacji. My nie może po prostu import ten moduł. Gdybyśmy to zrobili, zostałby dołączony do naszych normalnych pakietów JavaScript i działałby podczas nawadniania. To spowodowałoby FOUC, którego próbujemy uniknąć.

Podczas gdy Next.js ma wiele punktów zaczepienia webpacków do niestandardowych pakietów, użyję Zyje zamiast. Najpierw zainstaluj go za pomocą npm i vite a następnie utwórz plik vite.config.js plik. Mój wygląda tak:

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

Spowoduje to zbudowanie pliku pakietu z definicjami naszych komponentów sieciowych w shoelace-dir teczka. Przenieśmy to do public folder, aby Next.js go obsługiwał. Powinniśmy również śledzić dokładną nazwę pliku, z hashem na końcu. Oto skrypt Node, który przenosi plik i zapisuje moduł JavaScript, który eksportuje prostą stałą z nazwą pliku pakietu (przyda się to wkrótce):

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

Oto towarzyszący skrypt npm:

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

To powinno działać. Dla mnie, util/shoelace-bundle-info.js teraz istnieje i wygląda tak:

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

Ładowanie skryptu

Przejdźmy do Next.js _document.js i pobierz nazwę naszego pliku pakietu Web Components:

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

Następnie ręcznie renderujemy a tag w . Oto co moja cała _document.js plik wygląda następująco:

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

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

I to powinno działać! Nasza rejestracja Shoelace zostanie załadowana w skrypcie blokującym i będzie dostępna natychmiast, gdy nasza strona przetworzy początkowy kod HTML.

Poprawa wydajności

Moglibyśmy zostawić rzeczy tak, jak są, ale dodajmy pamięć podręczną dla naszego pakietu sznurowadeł. Powiemy Next.js, aby te pakiety Shoelace można było przechowywać w pamięci podręcznej, dodając następujący wpis do naszego pliku konfiguracyjnego Next.js:

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

Teraz, podczas kolejnych przeglądów naszej witryny, widzimy, że pakiet Shoelace ładnie się buforuje!

Otworzy się panel DevTools Sources z załadowanym pakietem Shoelace.
Używanie komponentów internetowych z Next (lub dowolnym frameworkiem SSR)

Jeśli nasz pakiet Shoelace kiedykolwiek się zmieni, zmieni się nazwa pliku (poprzez :hash części z powyższej właściwości źródłowej), przeglądarka stwierdzi, że nie ma tego pliku w pamięci podręcznej i po prostu zażąda go odświeżenia z sieci.

Zamykając

Mogło się wydawać, że to dużo ręcznej pracy; i to było. To niefortunne, że komponenty sieciowe nie oferują lepszej, gotowej do użycia obsługi renderowania po stronie serwera.

Ale nie powinniśmy zapominać o korzyściach, jakie zapewniają: fajnie jest móc używać wysokiej jakości komponentów UX, które nie są powiązane z konkretnym frameworkiem. Fajnie jest móc eksperymentować z zupełnie nowymi frameworkami, takimi jak Solidne, bez konieczności znajdowania (lub hakowania) jakiejś karty, modalnej, autouzupełniania lub jakiegokolwiek innego komponentu.

Znak czasu:

Więcej z Sztuczki CSS