Usando Web Components com Next (ou qualquer SSR Framework)

Na nossa num post anterior analisamos o Shoelace, que é uma biblioteca de componentes com um conjunto completo de componentes de UX que são bonitos, acessíveis e - talvez inesperadamente - construídos com Componentes da Web. Isso significa que eles podem ser usados ​​com qualquer framework JavaScript. Embora a interoperabilidade do Web Component do React seja, no momento, menos do que ideal, existem soluções alternativas.

Mas uma falha séria dos Web Components é sua atual falta de suporte para renderização do lado do servidor (SSR). Há algo chamado Declarative Shadow DOM (DSD) em andamento, mas o suporte atual para ele é bem mínimo e, na verdade, requer buy-in do seu servidor web para emitir marcação especial para o DSD. Atualmente, há trabalho sendo feito para Próximo.js que estou ansioso para ver. Mas para esta postagem, veremos como gerenciar Web Components de qualquer estrutura SSR, como Next.js, hoje.

Vamos acabar fazendo uma quantidade não trivial de trabalho manual, e levemente prejudicando o desempenho inicial da nossa página no processo. Em seguida, veremos como minimizar esses custos de desempenho. Mas não se engane: esta solução não é sem compensações, então não espere o contrário. Sempre medir e perfilar.

O problema

Antes de nos aprofundarmos, vamos parar um pouco e explicar o problema. Por que os Web Components não funcionam bem com a renderização do lado do servidor?

Estruturas de aplicativos como Next.js pegam o código React e o executam por meio de uma API para essencialmente “stringi-lo”, o que significa que transforma seus componentes em HTML simples. Portanto, a árvore de componentes do React será renderizada no servidor que hospeda o aplicativo da Web e esse HTML será enviado com o restante do documento HTML do aplicativo da Web para o navegador do usuário. Junto com este HTML estão alguns tags que carregam o React, junto com o código para todos os seus componentes do React. Quando um navegador processa esses tags, o React irá renderizar novamente a árvore de componentes e combinar as coisas com o HTML SSR que foi enviado. Neste ponto, todos os efeitos começarão a ser executados, os manipuladores de eventos serão conectados e o estado realmente... conterá o estado. É neste ponto que o aplicativo da web se torna interativo. O processo de reprocessar sua árvore de componentes no cliente e conectar tudo é chamado hidratação.

Então, o que isso tem a ver com Web Components? Bem, quando você renderiza algo, diga o mesmo cadarço componente que visitamos última vez:


   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.

…Reagir (ou honestamente qualquer framework JavaScript) verá essas tags e simplesmente as transmitirá. React (ou Svelte, ou Solid) não são responsáveis ​​por transformar essas tags em abas bem formatadas. O código para isso está escondido dentro de qualquer código que você tenha que defina esses Web Components. No nosso caso, esse código está na biblioteca Shoelace, mas o código pode estar em qualquer lugar. O importante é quando o código é executado.

Normalmente, o código que registra esses Web Components será inserido no código normal do seu aplicativo por meio de um JavaScript import. Isso significa que esse código será encerrado em seu pacote JavaScript e executado durante a hidratação, o que significa que, entre o usuário ver pela primeira vez o HTML SSR e a hidratação acontecendo, essas guias (ou qualquer componente da Web) não renderizarão o conteúdo correto . Então, quando a hidratação acontecer, o conteúdo adequado será exibido, provavelmente fazendo com que o conteúdo em torno desses Web Components se mova e se ajuste ao conteúdo formatado corretamente. Isso é conhecido como um flash de conteúdo sem estilo, ou FOUC. Em teoria, você poderia colocar marcação entre todos esses tags para corresponder à saída final, mas isso é praticamente impossível na prática, especialmente para uma biblioteca de componentes de terceiros como Shoelace.

Movendo nosso código de registro do Web Component

Portanto, o problema é que o código para fazer com que os Web Components façam o que eles precisam fazer não será executado até que ocorra a hidratação. Para este post, veremos como executar esse código mais cedo; imediatamente, na verdade. Veremos o agrupamento personalizado de nosso código de Web Component e a adição manual de um script diretamente ao arquivo do nosso documento. então ele é executado imediatamente e bloqueia o restante do documento até que seja executado. Isso normalmente é uma coisa terrível de se fazer. O objetivo da renderização do lado do servidor é não bloquear o processamento de nossa página até que nosso JavaScript seja processado. Mas uma vez feito isso, significa que, como o documento está inicialmente renderizando nosso HTML a partir do servidor, os Web Components serão registrados e emitirão imediatamente e de forma síncrona o conteúdo correto.

No nosso caso, estamos apenas por procurando executar nosso código de registro do Web Component em um script de bloqueio. Esse código não é enorme e tentaremos diminuir significativamente o impacto no desempenho adicionando alguns cabeçalhos de cache para ajudar nas visitas subsequentes. Esta não é uma solução perfeita. A primeira vez que um usuário navegar em sua página sempre será bloqueada enquanto o arquivo de script é carregado. As visitas subsequentes serão armazenadas em cache bem, mas essa compensação pode não ser viável para você — e-commerce, alguém? De qualquer forma, perfile, meça e tome a decisão certa para seu aplicativo. Além disso, no futuro é perfeitamente possível que o Next.js suporte totalmente DSD e Web Components.

Iniciar

Todo o código que veremos está em este repositório GitHub e implantado aqui com Vercel. O aplicativo da web renderiza alguns componentes de cadarço junto com o texto que muda de cor e conteúdo após a hidratação. Você deve conseguir ver o texto mudar para “Hidratado”, com os componentes Cadarço já renderizando corretamente.

Código de Componente Web de pacote personalizado

Nosso primeiro passo é criar um único módulo JavaScript que importe todas as nossas definições de Web Components. Para os componentes Shoelace que estou usando, meu código fica assim:

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

Ele carrega as definições para o e componentes e substitui algumas animações padrão para a caixa de diálogo. Simples o suficiente. Mas a parte interessante aqui é colocar esse código em nosso aplicativo. Nós não podes simplesmente import este módulo. Se fizéssemos isso, ele seria empacotado em nossos pacotes JavaScript normais e executado durante a hidratação. Isso causaria o FOUC que estamos tentando evitar.

Embora o Next.js tenha vários ganchos do webpack para personalizar coisas do pacote, usarei parafuso em vez de. Primeiro, instale-o com npm i vite e, em seguida, crie um vite.config.js Arquivo. O meu está assim:

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

Isso criará um arquivo de pacote com nossas definições de Web Component no shoelace-dir pasta. Vamos movê-lo para o public pasta para que Next.js a sirva. E também devemos acompanhar o nome exato do arquivo, com o hash no final. Aqui está um script Node que move o arquivo e escreve um módulo JavaScript que exporta uma constante simples com o nome do arquivo do pacote (isso será útil em breve):

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

Aqui está um script npm complementar:

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

Isso deve funcionar. Para mim, util/shoelace-bundle-info.js agora existe, e se parece com isso:

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

Carregando o roteiro

Vamos para o Next.js _document.js e puxe o nome do nosso arquivo de pacote do Web Component:

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

Em seguida, renderizamos manualmente um tag no . Aqui está o que todo o meu _document.js arquivo se parece com:

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

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

E isso deve funcionar! Nosso registro de cadarço será carregado em um script de bloqueio e estará disponível imediatamente à medida que nossa página processa o HTML inicial.

Melhorando a performance

Poderíamos deixar as coisas como estão, mas vamos adicionar cache para nosso pacote Shoelace. Diremos ao Next.js para tornar esses pacotes Shoelace armazenáveis ​​em cache adicionando a seguinte entrada ao nosso arquivo de configuração Next.js:

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

Agora, em navegações subsequentes em nosso site, vemos o pacote Shoelace armazenando em cache muito bem!

Painel DevTools Sources aberto e mostrando o pacote Shoelace carregado.
Usando Web Components com Next (ou qualquer SSR Framework)

Se o nosso pacote Shoelace mudar, o nome do arquivo mudará (através do :hash parte da propriedade source acima), o navegador descobrirá que não tem esse arquivo armazenado em cache e simplesmente o solicitará da rede.

Resumindo

Isso pode ter parecido muito trabalho manual; e foi. É lamentável que os Web Components não ofereçam melhor suporte pronto para uso para renderização no lado do servidor.

Mas não devemos esquecer os benefícios que eles proporcionam: é bom poder usar componentes de UX de qualidade que não estão vinculados a uma estrutura específica. Também é bom poder experimentar novos frameworks, como Sólido, sem precisar encontrar (ou hackear) algum tipo de guia, modal, autocomplete ou qualquer outro componente.

Carimbo de hora:

Mais de Truques CSS