Використання веб-компонентів із Next (або будь-якою системою SSR)

У моєму попередній публікації ми розглянули Shoelace, бібліотеку компонентів із повним набором UX-компонентів, які красиві, доступні та — можливо, несподівано — створені за Веб-компоненти. Це означає, що їх можна використовувати з будь-якою структурою JavaScript. Хоча взаємодію веб-компонентів React наразі не ідеально, є обхідні шляхи.

Але одним серйозним недоліком веб-компонентів є поточна відсутність підтримки рендеринга на стороні сервера (SSR). У розробці є дещо під назвою Declarative Shadow DOM (DSD), але поточна підтримка цього є досить мінімальною, і це фактично вимагає участі вашого веб-сервера, щоб створювати спеціальну розмітку для DSD. Зараз ведеться робота Next.js що я з нетерпінням чекаю побачити. Але для цієї публікації ми розглянемо, як керувати веб-компонентами з будь-якої системи SSR, наприклад Next.js, сьогодні.

Ми завершимо виконання нетривіальної ручної роботи, і трохи погіршення продуктивності запуску нашої сторінки в процесі. Потім ми розглянемо, як мінімізувати ці витрати на продуктивність. Але не помиляйтеся: це рішення не позбавлене компромісів, тому не очікуйте іншого. Завжди вимірюйте та профілюйте.

Проблема

Перш ніж ми заглибимося, давайте трохи пояснимо проблему. Чому веб-компоненти погано працюють із відтворенням на стороні сервера?

Фреймворки додатків, такі як Next.js, беруть код React і запускають його через API, щоб по суті «строкувати» його, тобто він перетворює ваші компоненти на звичайний HTML. Таким чином, дерево компонентів React відображатиметься на сервері, на якому розміщено веб-програму, і цей HTML буде надіслано разом із рештою HTML-документа веб-програми у браузер вашого користувача. Разом із цим HTML є деякі теги, які завантажують React разом із кодом для всіх ваших компонентів React. Коли браузер обробляє їх тегів, React повторно відтворить дерево компонентів і зіставить речі з надісланим HTML-кодом SSR. У цей момент усі ефекти почнуть працювати, обробники подій підключаться, і стан фактично буде… містити стан. Саме на цьому етапі стає веб-програма інтерактивний. Викликається процес повторної обробки вашого дерева компонентів на клієнті та підключення всього гідратація.

Отже, яке відношення це має до веб-компонентів? Ну, коли ви щось рендерите, скажіть той же шнурок компонент, який ми відвідали Останній раз:


   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.

… Відреагуйте (або чесно будь-який JavaScript framework) побачить ці теги та просто передасть їх. React (чи Svelte, чи Solid) не відповідають за перетворення цих тегів на гарно відформатовані вкладки. Код для цього зберігається в будь-якому коді, який у вас є, який визначає ці веб-компоненти. У нашому випадку цей код знаходиться в бібліотеці Shoelace, але код може бути будь-де. Що важливо коли виконується код.

Зазвичай код, що реєструє ці веб-компоненти, буде втягнутий у звичайний код програми за допомогою JavaScript import. Це означає, що цей код потраплятиме у ваш пакет JavaScript і виконуватиметься під час гідратації, а це означає, що між тим, як ваш користувач уперше побачить HTML SSR, і тим часом, коли відбудеться гідратація, ці вкладки (або будь-який веб-компонент) не відображатимуть правильний вміст . Потім, коли відбувається зволоження, відображатиметься належний вміст, що, ймовірно, спричинить переміщення вмісту навколо цих веб-компонентів і підгонку до правильно відформатованого вмісту. Це відомо як a спалах нестилізованого вмісту, або FOUC. Теоретично ви могли б вставити розмітку між усім цим теги, щоб відповідати готовому результату, але це майже неможливо на практиці, особливо для бібліотеки сторонніх компонентів, як-от Shoelace.

Переміщення реєстраційного коду веб-компонента

Отже, проблема полягає в тому, що код, який змушує веб-компоненти робити те, що їм потрібно, насправді не запускається, доки не відбудеться гідратація. Для цієї публікації ми розглянемо запуск цього коду раніше; відразу, насправді. Ми розглянемо спеціальне групування коду нашого веб-компонента та ручне додавання сценарію безпосередньо до нашого документа тому він запускається негайно та блокує решту документа, доки це не станеться. Зазвичай це жахливий вчинок. Суть візуалізації на стороні сервера полягає в тому, щоб НЕ заблокувати обробку нашої сторінки, доки не буде оброблено наш JavaScript. Але як тільки це буде зроблено, це означає, що, оскільки документ спочатку відтворює наш HTML із сервера, веб-компоненти будуть зареєстровані та одразу й синхронно видаватимуть правильний вміст.

У нашому випадку ми просто бажаючи запустити реєстраційний код веб-компонентів у сценарії блокування. Цей код невеликий, і ми спробуємо значно зменшити втрату продуктивності, додавши деякі заголовки кешу, щоб допомогти з наступними відвідуваннями. Це не ідеальне рішення. Перший раз, коли користувач переглядає вашу сторінку, він завжди блокуватиметься, поки завантажується файл сценарію. Наступні відвідування добре кешуватимуть, але це компроміс не може бути здійсненним для вас — електронна комерція, хтось? У будь-якому випадку, профілюйте, виміряйте та прийміть правильне рішення для своєї програми. Крім того, у майбутньому цілком можливо, що Next.js буде повністю підтримувати DSD і веб-компоненти.

Приступаючи до роботи

Весь код, який ми розглядатимемо, міститься це репо GitHub та розгорнуто тут разом із Vercel. Веб-програма відтворює деякі компоненти шнурка разом із текстом, який змінює колір і вміст після зволоження. Ви повинні побачити, як текст змінюється на «Hydrated», а компоненти Shoelace вже відображаються належним чином.

Спеціальний код веб-компонента комплектування

Наш перший крок — створити єдиний модуль JavaScript, який імпортує всі визначення веб-компонентів. Для компонентів Shoelace, які я використовую, мій код виглядає так:

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

Він завантажує визначення для та компоненти та замінює деякі типові анімації для діалогового вікна. Досить просто. Але найцікавішим моментом є розміщення цього коду в нашій програмі. ми не може просто import цей модуль. Якби ми зробили це, його було б об’єднано в наші звичайні пакети JavaScript і запущено під час гідратації. Це призведе до FOUC, якого ми намагаємося уникнути.

Незважаючи на те, що у Next.js є кілька хуків webpack для користувацьких пакетів, я буду використовувати Віте замість цього. Спочатку встановіть його за допомогою npm i vite а потім створіть vite.config.js файл. Мій виглядає так:

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

Це створить файл комплекту з нашими визначеннями веб-компонентів у shoelace-dir папку. Давайте перенесемо це до public папку, щоб Next.js обслуговував її. І ми також повинні відстежувати точну назву файлу з хешем у кінці. Ось скрипт Node, який переміщує файл і записує модуль JavaScript, який експортує просту константу з іменем файлу комплекту (незабаром це стане в нагоді):

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

Ось супутній сценарій npm:

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

Це має спрацювати. Для мене, util/shoelace-bundle-info.js зараз існує і виглядає так:

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

Завантаження сценарію

Перейдемо до Next.js _document.js файл і введіть назву нашого пакетного файлу веб-компонентів:

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

Потім ми вручну візуалізуємо a тег у . Ось що все моє _document.js файл виглядає так:

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

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

І це має спрацювати! Наша реєстрація Shoelace завантажиться у сценарій блокування та стане доступною одразу, коли наша сторінка обробить початковий HTML.

Підвищення продуктивності

Ми могли б залишити все як є, але давайте додамо кешування для нашого пакету Shoelace. Ми скажемо Next.js зробити ці набори Shoelace доступними для кешування, додавши наступний запис до нашого конфігураційного файлу Next.js:

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

Тепер, під час наступних переглядів нашого сайту, ми бачимо, що комплект Shoelace добре кешується!

Відкрита панель джерел DevTools із завантаженим пакетом шнурків.
Використання веб-компонентів із Next (або будь-якою системою SSR)

Якщо наш комплект Shoelace колись зміниться, зміниться і назва файлу (через :hash з вихідної властивості вище), браузер виявить, що цей файл не кешовано, і просто запитає його свіжим із мережі.

Підводячи підсумок

Це могло здатися великою кількістю ручної роботи; і це було. На жаль, веб-компоненти не пропонують кращої готової підтримки для відтворення на стороні сервера.

Але ми не повинні забувати про переваги, які вони надають: приємно мати можливість використовувати якісні UX-компоненти, які не прив’язані до конкретного фреймворку. Дуже приємно мати можливість експериментувати з абсолютно новими фреймворками, наприклад Solid, без необхідності шукати (або зламати разом) якусь вкладку, модальний, автозавершений чи інший компонент.

Часова мітка:

Більше від CSS-хитрощі