将 Web 组件与 Next(或任何 SSR 框架)一起使用

在我 以前的帖子 我们查看了 Shoelace,它是一个组件库,其中包含一整套美观、易于访问的 UX 组件,而且——也许出乎意料地——用 Web组件. 这意味着它们可以与任何 JavaScript 框架一起使用。 尽管 React 的 Web 组件互操作性目前还不够理想, 有解决方法.

但是 Web 组件的一个严重缺点是它们目前缺乏对服务器端渲染 (SSR) 的支持。 有一种叫做声明性影子 DOM (DSD) 的东西正在开发中,但目前对它的支持非常少,而且它实际上需要从您的 Web 服务器购买才能为 DSD 发出特殊标记。 目前正在为 Next.js 我期待看到。 但在这篇文章中,我们将了解如何从任何 SSR 框架(例如 Next.js)管理 Web 组件, 今晚.

我们最终会做大量的手工工作,并且 在此过程中损害了我们页面的启动性能。 然后,我们将研究如何最小化这些性能成本。 但不要搞错:这个解决方案并非没有权衡,所以不要指望其他。 始终测量和配置文件。

该问题

在我们深入研究之前,让我们花点时间实际解释一下这个问题。 为什么 Web 组件不能很好地与服务器端渲染配合使用?

Next.js 之类的应用程序框架采用 React 代码并通过 API 运行它以从本质上“字符串化”它,这意味着它将您的组件转换为纯 HTML。 因此,React 组件树将在托管 Web 应用程序的服务器上呈现,并且该 HTML 将与 Web 应用程序的 HTML 文档的其余部分一起发送到用户的浏览器。 除了这个 HTML,还有一些 加载 React 的标签,以及所有 React 组件的代码。 当浏览器处理这些 标签,React 将重新渲染组件树,并与发送的 SSR 的 HTML 匹配。 此时,所有效果都将开始运行,事件处理程序将连接起来,状态实际上将……包含状态。 正是在这一点上,网络应用程序变成了 互动. 在客户端重新处理组件树并将所有内容连接起来的过程称为 水化.

那么,这与 Web Components 有什么关系呢? 好吧,当你渲染一些东西时,说同样的鞋带 我们访问的组件 上次:


   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 框架)将看到这些标签并简单地传递它们。 React(或 Svelte 或 Solid)不负责将这些标签转换为格式良好的选项卡。 其代码隐藏在定义这些 Web 组件的任何代码中。 在我们的例子中,该代码在 Shoelace 库中,但代码可以在任何地方。 重要的是 当代码运行时.

通常,注册这些 Web 组件的代码将通过 JavaScript 被拉入应用程序的正常代码中 import. 这意味着此代码将在您的 JavaScript 包中结束并在水合期间执行,这意味着在您的用户第一次看到 SSR 的 HTML 和水合发生之间,这些选项卡(或任何 Web 组件)将不会呈现正确的内容. 然后,当水合发生时,会显示正确的内容,可能会导致这些 Web 组件周围的内容四处移动并适合格式正确的内容。 这被称为 无样式内容的闪光,或 FOUC。 从理论上讲,您可以在所有这些之间添加标记 标记来匹配完成的输出,但这在实践中几乎是不可能的,尤其是对于像 Shoelace 这样的第三方组件库。

移动我们的 Web 组件注册码

所以问题是,让 Web 组件做他们需要做的事情的代码在水合发生之前不会真正运行。 在这篇文章中,我们将着眼于尽快运行该代码; 事实上,立即。 我们将查看自定义捆绑我们的 Web 组件代码,并手动将脚本直接添加到我们文档的 所以它会立即运行,并阻止文档的其余部分,直到它运行为止。 这通常是一件可怕的事情。 服务器端渲染的重点是 不能 阻止我们的页面处理,直到我们的 JavaScript 处理完毕。 但是一旦完成,这意味着,当文档最初从服务器呈现我们的 HTML 时,Web 组件将被注册,并将立即和同步地发出正确的内容。

在我们的例子中,我们是 只是 希望在阻塞脚本中运行我们的 Web 组件注册代码。 这段代码并不大,我们将通过添加一些缓存头来帮助后续访问来显着降低性能损失。 这不是一个完美的解决方案。 用户第一次浏览您的页面时总是会在加载该脚本文件时被阻止。 后续访问将很好地缓存,但这种权衡 也许不会 对你来说是可行的——电子商务,有人吗? 无论如何,分析、衡量并为您的应用做出正确的决定。 此外,未来 Next.js 完全有可能完全支持 DSD 和 Web 组件。

入门

我们将要查看的所有代码都在 这个GitHub仓库与 Vercel 一起部署在这里. Web 应用程序呈现一些 Shoelace 组件以及在水合时改变颜色和内容的文本。 您应该能够看到文本更改为“Hydrated”,鞋带组件已经正确渲染。

自定义捆绑 Web 组件代码

我们的第一步是创建一个 JavaScript 模块来导入我们所有的 Web 组件定义。 对于我正在使用的鞋带组件,我的代码如下所示:

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 包中并在 hydration 期间运行。 这将导致我们试图避免的 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`,
      },
    },
  },
});

这将使用我们的 Web 组件定义构建一个捆绑文件 shoelace-dir 文件夹。 让我们把它移到 public 文件夹,以便 Next.js 为其提供服务。 我们还应该跟踪文件的确切名称,并在文件末尾加上哈希。 这是一个移动文件并编写一个 JavaScript 模块的 Node 脚本,该模块导出一个带有捆绑文件名称的简单常量(这很快就会派上用场):

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 文件并拉入我们的 Web 组件包文件的名称:

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

然后我们手动渲染一个 标签在 . 这是我的全部 _document.js 文件看起来像:

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

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

那应该行得通! 我们的鞋带注册将加载到一个阻塞脚本中,并在我们的页面处理初始 HTML 时立即可用。

改善表现

我们可以保持原样,但让我们为 Shoelace 包添加缓存。 我们将通过在 Next.js 配置文件中添加以下条目来告诉 Next.js 使这些 Shoelace 包可缓存:

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

现在,在随后浏览我们的网站时,我们可以很好地看到 Shoelace 捆绑包缓存!

DevTools Sources 面板打开并显示加载的 Shoelace 包。
将 Web 组件与 Next(或任何 SSR 框架)一起使用

如果我们的鞋带包发生变化,文件名也会改变(通过 :hash 部分来自上面的源属性),浏览器会发现它没有缓存该文件,并且会简单地从网络请求它。

结束了

这可能看起来像是很多手工工作。 确实如此。 不幸的是,Web 组件没有为服务器端渲染提供更好的开箱即用支持。

但是我们不应该忘记它们提供的好处:能够使用不依赖于特定框架的优质 UX 组件非常好。 能够尝试全新的框架真是太好了,比如 固体,而无需查找(或一起破解)某种选项卡、模式、自动完成或任何组件。

时间戳记:

更多来自 CSS技巧