Utilisation de composants Web avec Next (ou n'importe quel framework SSR)

Dans mon post précédent nous avons examiné Shoelace, qui est une bibliothèque de composants avec une suite complète de composants UX beaux, accessibles et, peut-être de manière inattendue, construits avec Composants Web. Cela signifie qu'ils peuvent être utilisés avec n'importe quel framework JavaScript. Bien que l'interopérabilité des composants Web de React soit, à l'heure actuelle, loin d'être idéale, il existe des solutions de contournement.

Mais une grave lacune des composants Web est leur manque actuel de prise en charge du rendu côté serveur (SSR). Il y a quelque chose appelé Declarative Shadow DOM (DSD) en préparation, mais sa prise en charge actuelle est assez minime et nécessite en fait l'adhésion de votre serveur Web pour émettre un balisage spécial pour le DSD. Il y a actuellement des travaux en cours pour Next.js que j'ai hâte de voir. Mais pour cet article, nous verrons comment gérer les composants Web à partir de n'importe quel framework SSR, comme Next.js, aujourd'hui.

Nous finirons par faire une quantité non négligeable de travail manuel, et légèrement nuire aux performances de démarrage de notre page dans le processus. Nous verrons ensuite comment minimiser ces coûts de performances. Mais ne vous y trompez pas : cette solution n'est pas sans compromis, alors ne vous attendez pas à autre chose. Toujours mesurer et profiler.

Le problème

Avant de plonger, prenons un moment et expliquons réellement le problème. Pourquoi les composants Web ne fonctionnent-ils pas bien avec le rendu côté serveur ?

Les frameworks d'application comme Next.js prennent le code React et l'exécutent via une API pour le "chaîner" essentiellement, ce qui signifie qu'il transforme vos composants en HTML brut. Ainsi, l'arborescence des composants React sera rendue sur le serveur hébergeant l'application Web, et ce code HTML sera envoyé avec le reste du document HTML de l'application Web au navigateur de votre utilisateur. Avec ce code HTML, il y a quelques balises qui chargent React, ainsi que le code de tous vos composants React. Lorsqu'un navigateur traite ces tags, React restituera l'arborescence des composants et fera correspondre les éléments avec le code HTML SSR envoyé. À ce stade, tous les effets commenceront à s'exécuter, les gestionnaires d'événements seront connectés et l'état contiendra en fait… l'état. C'est à ce stade que l'application Web devient Interactif. Le processus de retraitement de votre arborescence de composants sur le client et de tout câbler s'appelle hydratation.

Alors, qu'est-ce que cela a à voir avec les composants Web ? Eh bien, quand vous rendez quelque chose, dites le même Lacet composant que nous avons visité dernière fois:


   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.

… Réagissez (ou honnêtement tous Framework JavaScript) verra ces balises et les transmettra simplement. React (ou Svelte ou Solid) n'est pas responsable de la transformation de ces balises en onglets bien formatés. Le code pour cela est caché à l'intérieur de tout code que vous avez qui définit ces composants Web. Dans notre cas, ce code se trouve dans la bibliothèque Shoelace, mais le code peut être n'importe où. Ce qui est important c'est quand le code s'exécute.

Normalement, le code enregistrant ces composants Web sera inséré dans le code normal de votre application via un JavaScript import. Cela signifie que ce code se retrouvera dans votre bundle JavaScript et s'exécutera pendant l'hydratation, ce qui signifie que, entre la première fois que votre utilisateur verra le HTML SSR et l'hydratation se produire, ces onglets (ou tout composant Web d'ailleurs) ne rendront pas le contenu correct . Ensuite, lorsque l'hydratation se produit, le contenu approprié s'affichera, ce qui entraînera probablement le déplacement du contenu autour de ces composants Web et l'adaptera au contenu correctement formaté. Ceci est connu comme un flash de contenu sans style, ou FOUC. En théorie, vous pourriez coller un balisage entre tous ces balises pour correspondre à la sortie finale, mais cela est pratiquement impossible dans la pratique, en particulier pour une bibliothèque de composants tiers comme Shoelace.

Déplacement de notre code d'enregistrement de composant Web

Le problème est donc que le code permettant aux composants Web de faire ce qu'ils doivent faire ne s'exécutera pas tant que l'hydratation n'aura pas eu lieu. Pour cet article, nous verrons comment exécuter ce code plus tôt ; immédiatement, en fait. Nous examinerons le regroupement personnalisé de notre code de composant Web et l'ajout manuel d'un script directement à notre document. il s'exécute donc immédiatement et bloque le reste du document jusqu'à ce qu'il le fasse. C'est normalement une chose terrible à faire. L'intérêt du rendu côté serveur est de ne sauraient bloquer le traitement de notre page jusqu'à ce que notre JavaScript soit traité. Mais une fois cela fait, cela signifie que, comme le document rend initialement notre code HTML à partir du serveur, les composants Web seront enregistrés et émettront immédiatement et de manière synchrone le bon contenu.

Dans notre cas, nous sommes juste cherchant à exécuter notre code d'enregistrement de composant Web dans un script de blocage. Ce code n'est pas énorme et nous chercherons à réduire considérablement les performances en ajoutant des en-têtes de cache pour faciliter les visites ultérieures. Ce n'est pas une solution parfaite. La première fois qu'un utilisateur navigue sur votre page sera toujours bloqué pendant le chargement de ce fichier de script. Les visites suivantes se cacheront bien, mais ce compromis pourrait ne pas être faisable pour vous - e-commerce, quelqu'un? Quoi qu'il en soit, profilez, mesurez et prenez la bonne décision pour votre application. En outre, à l'avenir, il est tout à fait possible que Next.js prenne entièrement en charge les composants DSD et Web.

Commencer

Tout le code que nous allons examiner est dans ce dépôt GitHub ainsi que les déployé ici avec Vercel. L'application Web rend certains composants Shoelace avec du texte qui change de couleur et de contenu lors de l'hydratation. Vous devriez pouvoir voir le texte passer à "Hydraté", les composants Shoelace étant déjà rendus correctement.

Code de composant Web de regroupement personnalisé

Notre première étape consiste à créer un seul module JavaScript qui importe toutes nos définitions de composants Web. Pour les composants Shoelace que j'utilise, mon code ressemble à ceci :

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

Il charge les définitions des ainsi que les composants et remplace certaines animations par défaut pour la boîte de dialogue. Assez simple. Mais ce qui est intéressant ici, c'est d'intégrer ce code dans notre application. Nous ne peut pas simplement import ce module. Si nous faisions cela, il serait regroupé dans nos bundles JavaScript normaux et fonctionnerait pendant l'hydratation. Cela provoquerait le FOUC que nous essayons d'éviter.

Bien que Next.js ait un certain nombre de crochets Webpack pour personnaliser les choses, j'utiliserai Vite plutôt. Tout d'abord, installez-le avec npm i vite puis créez un vite.config.js déposer. Le mien ressemble à ça :

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

Cela créera un fichier bundle avec nos définitions de composants Web dans le shoelace-dir dossier. Déplaçons-le vers le public dossier afin que Next.js le serve. Et nous devrions également garder une trace du nom exact du fichier, avec le hachage à la fin. Voici un script Node qui déplace le fichier et écrit un module JavaScript qui exporte une constante simple avec le nom du fichier bundle (cela sera utile sous peu) :

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

Voici un script npm compagnon :

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

Cela devrait fonctionner. Pour moi, util/shoelace-bundle-info.js existe maintenant et ressemble à ceci :

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

Chargement du script

Passons au Next.js _document.js fichier et tirez le nom de notre fichier de bundle de composants Web :

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

Ensuite, nous rendons manuellement un tag dans le . Voici ce que mon ensemble _document.js le fichier ressemble à:

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

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

Et ça devrait marcher ! Notre inscription Shoelace se chargera dans un script de blocage et sera disponible immédiatement lorsque notre page traitera le code HTML initial.

Améliorer les performances

Nous pourrions laisser les choses telles qu'elles sont, mais ajoutons la mise en cache pour notre bundle Shoelace. Nous dirons à Next.js de rendre ces bundles Shoelace cachables en ajoutant l'entrée suivante à notre fichier de configuration Next.js :

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

Maintenant, lors de navigations ultérieures sur notre site, nous voyons le bundle Shoelace bien se mettre en cache !

Le panneau DevTools Sources est ouvert et affiche le bundle Shoelace chargé.
Utilisation de composants Web avec Next (ou n'importe quel framework SSR)

Si notre bundle Shoelace change, le nom du fichier changera (via le :hash partie de la propriété source ci-dessus), le navigateur constatera qu'il n'a pas ce fichier en cache et le demandera simplement à partir du réseau.

Emballage en place

Cela peut avoir semblé être beaucoup de travail manuel; et c'était. Il est regrettable que les composants Web n'offrent pas une meilleure prise en charge prête à l'emploi pour le rendu côté serveur.

Mais il ne faut pas oublier les avantages qu'ils procurent : c'est bien de pouvoir utiliser des composants UX de qualité qui ne sont pas liés à un framework spécifique. C'est également agréable de pouvoir expérimenter de nouveaux frameworks, comme Solide, sans avoir besoin de trouver (ou de pirater ensemble) une sorte d'onglet, modal, de saisie semi-automatique ou autre composant.

Horodatage:

Plus de Astuces CSS