服务器端渲染一个使用Web组件的博客
#javascript #html #css #Web组件

本博客文章支持YouTube Rivestream计划于周三4/19,美国东部标准时间下午12点 /下午9点。您可以观看直播here on Youtube

介绍

服务器端渲染网站从未如此简单。几年前,它采用了诸如PHP和Java之类的服务器端技术,除了JavaScript外,还要求Web开发人员学习另一种语言。随着node.js的引入,Web开发人员在服务器上获得了JavaScript运行时,以及诸如Express之类的工具,可以处理HTTP请求并将HTML返回到客户端。用于服务器端的元框架渲染弹起,支持流行的库和框架,例如React,Angular,Vue或Svelte。

meta-frameworks必须考虑反应一流的公民,因为虚拟DOM,DOM周围的抽象使库可以“差异”更改并更新视图。虚拟DOM承诺效率,但是我们得到的是一种工具链,使Web开发更难支持虚拟DOM。必须创建和维护工具,以支持渲染虚拟DOM服务器端,然后“补充”视图客户端。如此多的工具的令人讨厌的副作用是Web开发人员必须维护许多依赖关系,哪些地区开发人员来自开发功能和实施错误修复。

水合成为近年来几位网络开发人员词汇的一部分。水合是使用客户端JavaScript将状态和交互性添加到服务器渲染的HTML的过程。当我们今天说“服务器端渲染”时,Web开发人员的许多概念普及了。

如果我告诉您有一种方法可以用较少的工具来渲染HTML?这会诱人吗?

如果您喜欢本博客文章中正在阅读的内容,请考虑购买我的书Fullstack Web Components的副本,可在newline.co上找到。这篇文章之后的详细信息。

在本指南中,我将演示如何使用Express Mifdreware使用Google开发的开源软件包来使用Express Mifdreware进行服务器端渲染自主元素。 @lit-labs/ssr是由维护LIT的团队在积极开发下的图书馆包,该团队是一个流行的图书馆,这是一个流行的开发自定义元素的图书馆。 @lit-labs/ssr是Lit Labs实验套餐家族的一部分。即使该软件包处于“实验性”状态,核心产品也非常稳定,可与“ Vanilla”自定义元素一起使用。

您可以通过绑定包装导出的render函数以表达中间件来呈现 @lit-labs/ssr今天的自定义元素。 @lit-labs/ssr支持渲染自定义元素,尽管LitElement基于Web标准,但首先是从Litelement延伸的自定义元素,但该类是从HTMLElement延伸的。 @lit-labs/ssr依赖另一个名为声明的影子dom的网络标准。

声明性的影子DOM

Declarative Shadow DOM是Web规范,使服务器端渲染自定义元素成为可能。在遵循此标准之前,开发自定义元素的唯一方法是急需的。您必须定义一个class,并且在constructor内部构建了阴影根。在下面的示例中,我们有一个名为AppCard的自定义元素,该元素迫切地构建了阴影根。阴影根的好处是我们获得封装。在阴影根的上下文中定义的CSS和HTML无法泄漏到页面的其余部分。

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = document.createElement('template');
      template.innerHTML = `
      <style>
      ${styles}
      </style>
      <header>
        <slot name="header"></slot>
      </header>
      <section>
        <slot name="content"></slot>
      </section>
      <footer>
        <slot name="footer"></slot>
      </footer>
      `;
      shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

customElements.define('app-card', AppCard);

声明的影子DOM允许您声明为组件定义相同的模板。这是用声明的影子dom定义的AppCard的同一阴影根的示例。

<app-card>
  <template shadowrootmode="open"> 
    <style>
      ${styles}
    </style>
    <header>
      <slot name="header"></slot>
    </header>
    <section>
      <slot name="content"></slot>
    </section>
    <footer>
      <slot name="footer"></slot>
    </footer> 
  </template>
  <img slot="header" src="${thumbnail}" alt="${alt}" />
  <h2 slot="header">${headline}</h2>
  <p slot="content">${content}</p>
  <a href="/post/${link}" slot="footer">Read Post</a>
</app-card>

声明的影子DOM将shadowrootmode属性引入了HTML模板。在此示例<app-card>中,该属性被检测到我的HTML解析器并应用为父元素的阴影根。上面的示例使用模板插槽将内容动态注入自定义元素模板。使用声明的影子DOM,在模板外定义的任何HTML都被视为可以通过插槽投射到“ Shadow dom”的“光DOM”。 ${}语法的用法仅用于示例。这不是某种数据结合技术。由于现在声明地定义了模板,因此您可以将定义减少为字符串。 ES2015模板字符串非常适合此目的。在演示中,我们将使用模板字符串使用声明的影子DOM标准来声明每个组件的模板。

但是等等?如果将组件的模板简化为字符串,您如何将交互性注入组件客户端?您仍然必须在客户端急需定义组件,但是由于Shadow Dom已经实例化,因为浏览器已经解析了声明的Shadow dom模板,因此您不再需要实例化模板。如果阴影根尚不存在,您仍然可以急需实例化阴影DOM。

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      // instantiate the template imperatively
      // if a shadow root doesn't exist 
    }
    ...

可选的是,当检测到阴影根时,您可以将组件客户端的水合不同(因为组件是服务器端渲染的)。

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (this.shadowRoot) {
      // bind event listeners here
      // or handle other client-side interactivity
    }
    ...

自定义元素必须注册,无论其模板是在毫不止在地或声明的。

customElements.define('app-card', AppCard);

人群中的建筑师可能会注意到这种方法一个缺陷。相同的模板不需要两次定义吗?一次用于声明的影子dom,第二次在自定义元素的constructor中进行声明。当然,但是我们可以通过使用ES2015模板字符串来减轻这种情况。通过通过构图实现模板,我们可以将模板通常急切地定义为另一个定义的模板。我们将确保在此研讨会中开发的每个组件中重复使用部分模板。

您将建造什么

在此研讨会中,您将服务器端渲染以显示博客所需的四个自定义元素:

  • AppCard显示一张卡
  • AppHeader显示网站标头
  • MainView显示网站标头和几张卡
  • PostView显示网站标题和博客文章

The main view of the blog displays a header and cards

验收标准

博客应该有两条路线。一个显示最新帖子列表,另一个显示单个帖子的内容。

  • 当用户访问http://localhost:4444时,用户应查看网站标头和几张卡(MainView)。

  • 用户访问http://localhost:4444/post/:slug时,用户应查看网站标题和博客文章内容(PostView)。该路由包括一个变量“ slug”,该变量由博客文章动态支持。

项目结构

工作空间是一个由4个项目目录组成的monorepo:

  • 客户端:自定义元素渲染客户端和服务器端
  • 服务器:处理服务器端渲染的服务器
  • 垫片:node.js中找不到的浏览器规格的自定义垫片,由lit>提供
  • 提供
  • 样式:博客网站的全球样式

Lerna和NX处理该项目,而Nodemon处理更改和重建项目。

该项目主要用打字稿编码。

对于研讨会,您将主要专注于在/packages/server/src/middleware/ssr.ts上找到的单个文件。该文件包含处理服务器端渲染的中间件。您还将编辑在软件包/客户端/SRC/中找到的自定义元素。每个文件都包含一些样板以开始您的启动。

建筑学

在此研讨会中您将开发出异步渲染声明的阴影DOM模板的方法。每个视图都映射到Route。在此文件中的启动代码中定义了验收标准中列出的两个Route

export const routes: Routes = [
  {
    path: "/",
    component: "main",
    tag: "main-view",
    template: mainTemplate,
  },
  {
    pathMatch: /\/post\/([a-zA-Z0-9-]*)/,
    component: "post",
    tag: "post-view",
    template: postTemplate,
  },
];

我们需要对路线的静态定义。将定义放入从文件导出的Array中是一种意见。一些元框架用在构建或运行时解析的目录的名称使此定义相混淆。 在中间件中,我们将引用此Array,以检查用户在路径上是否存在路由的路线。上方的路由使用标识符:path定义。 path: "/",与根相匹配,即http://localhost:4444。第二个示例改用pathMatch。用于显示每个帖子的路由是动态的,应该通过Slug显示博客文章。每个路由也对应于template,我们将在每个“视图”文件中定义为声明的Shadow dom Template字符串,或更准确地,返回模板字符串的function。模板的示例如下。

export const mainTemplate = () => `<style>
    ${styles}
  </style>
  <div class="container">
  <!-- put content here -->
  </div>`;

在研讨会期间,我们将显示一条静态路线,但是在使每个视图都取决于API请求之后不久。从API端点返回的JSON将用作视图的模型。我们将从每个视图文件中导出一个命名function,以获取数据并返回模型。一个例子是下面。

function fetchModel(): Promise<DataModel> {
  return Promise.all([
    fetch("http://localhost:4444/api/meta"),
    fetch("http://localhost:4444/api/posts"),
  ])
    .then((responses) => Promise.all(responses.map((res) => res.json())))
    .then((jsonResponses) => {
      const meta = jsonResponses[0];
      const posts = jsonResponses[1].posts;
      return {
        meta,
        posts,
      };
    });
}

在上面的示例中,对fetch的两个电话请求该网站的元数据,以及来自两个不同API端点的最近博客文章的Array。响应映射到显示视图所需的模型。

下面的每个API端点的描述,但首先让我们看看信息流。

Markdown-> JSON W/嵌入式Markdown-> HTML->声明的影子DOM模板

博客文章以降价格式存储在目录套件/服务器/数据/帖子/中。两个API端点(/API/帖子和/api/post/:slug)从每个文件中获取标记,并以Post格式返回该标记。 Post的类型定义是按下来的:

export type Post = {
  content: string;
  slug: string;
  title: string;
  thumbnail: string;
  author: string;
  excerpt: string;
};

另一个端点处理整个页面的元数据。该端点返回的数据的界面已简化为研讨会,可以扩展出来。

export type Meta = {
  author: string;
  title: string;
};

在研讨会期间,您将依靠三个本地API端点来获取网站的元数据以及与每个博客文章关联的数据。每个端点的描述如下。

http://localhost:4444/api/meta返回以JSON格式显示站点标题所需的元数据。

http://localhost:4444/api/post/:slug返回了一篇博客文章,该文章是“ slug”,一个由json格式划定的字符串。

http://localhost:4444/api/posts返回以JSON格式的一系列博客文章。

您将在研讨会期间向这些端点提出请求,并使用JSON响应来填充每个声明的Shadow dom模板的内容。在每个需要数据的文件中,MetaPost架构的类型定义已在packages/server/src/db/index.ts中提供。这些类型的定义被导入相关文件以简化开发。

除了提供的三个本地端点外,您还将向GitHub API提出请求,以解析从随附的博客文章文件中返回的降价。此API是必要的,因为它提供了在Markdown文件中找到的解析代码段的最简单方法并将其转换为HTML。

入门

您需要一个GitHub帐户。您可以使用github登录stackblitz,然后您需要github来生成令牌。

如果您还没有GitHub帐户,signup for Github here

如果您想关注本教程,则叉StackBlitzGithub repo

使用StackBlitz时,开发环境将立即安装依赖项并加载嵌入式浏览器。 VS代码可在浏览器中用于开发。 Stackblitz在Google Chrome中效果最好。

如果使用github,则将存储库分叉并在本地克隆回购。运行npm installnpm run dev。开始在您选择的IDE中进行编码。

关于令牌的重要说明

在研讨会期间,我们将使用称为Octokit的Github API从Markdown中为每个博客文章生成客户端HTML。如果您使用的是Stackblitz,则为研讨会提供API令牌,但将在不久后撤销。如果您将存储库或令牌被吊销,请登录github和generate a new token on Github在研讨会中使用。

,但要像我一样,永远不要在代码中静态存储令牌。为研讨会注入令牌的唯一原因是因为这是开始使用octokit提出请求的最简单方法。

有关支持的重要说明

Stackblitz在Google Chrome中效果最好。如果您与Stackblitz一起关注,请确保您正在使用Google Chrome。

本研讨会中包含的示例将在除Firefox以外的每个主流浏览器中使用。尽管Mozilla表示支持实施声明性的影子DOM,但浏览器vender尚未在稳定版本中提供支持。可以使用小马来解决Firefox缺乏支持。对声明的影子DOM的支持可能会改变Firefox。我有信心,标准将在不久的将来成为跨浏览器兼容,因为Mozilla最近在changed it's position上对声明的影子Dom。我已经使用the ponyfill超过一年了,没有任何缺乏标准支持的浏览器问题。

用 @lit-labs/ssr渲染自定义元素的服务器端

让我们开始编码,好吗?要执行的第一个任务是声明在packages/client/client/src/view/main/index.ts中找到的名为MainView的组件的声明影子dom模板。此视图最终将替换当前在Path http://localhost:4444/的浏览器中显示“ Web组件博客入门”的样板。

在Mainview中支持声明的影子DOM

打开IDE中的packages/client/src/view/main/index.ts,并在MainView classconstructor中找到当前定义的阴影dom模板。我们将将此模板声明为ES2015模板字符串,由function返回,名为shadowTemplate。将当前的模板切入并粘贴到由shadowTemplate返回的模板字符串中。

const shadowTemplate = () => html`<style>
    ${styles}
  </style>
  <div class="post-container">
    <app-header></app-header>
  </div>`;

为了方便起见,html被导入到该文件中,您可以用来标记模板字面的标签。如果您选择了GitHub并在本地克隆了存储库,则可以启用this VSCode extension,该this VSCode extension在标记的模板文字中提供HTML和CSS的语法突出显示。这确实是html的唯一目的,尽管您可以利用此功能并以不同的方式解析模板字符串。

更新命令逻辑以调用shadowTemplate,该逻辑应与以前一样返回相同的String

template.innerHTML = shadowTemplate();

为了重复使用模板,我们可以将其传递给我们声明的下一个function,名为template。格式是相同的,尽管这次的模板通常声明为与组件(<main-view>)关联的HTML标签和与属性shadowrootmode设置为open相关的HTML标签。注入shadowTemplate()我们早些时候在<template>标签内定义。

const template = () => html`<main-view>
  <template shadowrootmode="open">
    ${shadowTemplate()}
  </template>
  </main-view>`;

恭喜!您刚刚宣布了第一个声明的影子DOM模板。这种方法的一些好处是,我们可以重复使用毫无疑问声明的典型阴影根模板,并且由于我们使用了function,因此可以将可以传递给ES2015模板字符串的注入参数。当我们想用API端点返回的数据填充模板时,这将派上用场。

最后,请确保从文件中导出template。有关为什么我们以后我们导出MainView classtemplate function的更多信息。

export { MainView, template };

这是一个不错的开始,但是如果我们将代码保留为IS,我们会遇到问题。虽然可以重复使用模板部分,但上面的示例是夸张的。您很少能像我们那样将整个阴影根注入声明的阴影DOM模板中,尤其是当有儿童自定义元素也需要使用声明性的Shadow dom渲染服务器端时。在上面的示例中,当前无法在服务器上渲染<app-header>,因为我们没有用声明的Shadow dom声明该组件的模板。让我们现在就这样做。

支持标题中的声明性影子DOM

要声明一个新的模板以渲染AppHeader组件,打开软件包/client/src/component/header/header.ts。

就像在上一个示例中一样,剪切并粘贴了在AppHeader constructor中迫切定义的模板中,将返回相同字符串的新的function中的新的function。这次,将单个参数注入到将Object与两个属性进行解构的函数中:stylestitle并将其传递到模板。

const shadowTemplate = ({ styles, title }) => html`
  <style>
    ${styles}
  </style>
  <h1>${title}</h1>
`;

使用第一个参数调用shadowTemplate,设置为文件中当前找到的两个属性:stylestitle

template.innerHTML = shadowTemplate({ styles, title });

对于低级组件,以这种方式定义参数以后填充模板是非常可以的。稍后我们需要将API端点的响应绘制到template function期望的内容时,它将提供一些灵活性。要声明声明的影子DOM模板,请定义一个名为template的新的function,这次用<app-header>标签和HTML模板封装了对shadowTemplate的调用。

const template = ({ styles, title }) => html`<app-header>
  <template shadowrootmode="open">
  ${shadowTemplate({ styles, title })}
  </template>
</app-header>`;

我们可以在这种情况下声明使用的阴影根模板,因为AppHeader是一个叶子节点,也就是说,它没有直接的后代,也需要使用声明的影子dom。

最后,导出可以在MainView中重复使用的所有必要部分。

export { styles, title, template, AppHeader };

使用声明性标头更新Mainview

打开包/client/src/view/main/index.ts,并从header.js导入启用template的零件,这一次将其重命名为appHeaderTemplate。请注意,如何使用styles as appHeaderStyles与此本地styles在packages/client/src/view/main/index.ts中不冲突。

import {
  styles as appHeaderStyles,
  template as appHeaderTemplate,
  title,
  AppHeader,
} from '../../component/header/Header.js';

更新template function,用appHeaderTemplate替换shadowTemplate呼叫,确保通过stylestitle

const template = () => html`<main-view>
  <template shadowrootmode="open">
    ${appHeaderTemplate({ styles: appHeaderStyles, title})}
  </template>
  </main-view>`;

敏锐的观察者可能会在这里注意到机会。我们不必始终使用Header.ts中规定的stylestitle,而是使用对appHeaderTemplate的调用来覆盖造型或动态设置title的一种手段。我们将在本研讨会的后面部分做后者。

支持声明中的声明性阴影DOM

在编码服务器端所需的明确中间件之前,渲染了我们迄今为止声明的这些声明的影子DOM模板,我们需要做一些家政服务。包装/client/src/view/post/index.ts中定义的第二个视图组件还需要一个名为templatefunction。此组件负责显示一个博客文章。

注意形成的模式?当服务器端渲染组件时,模式非常有用。如果每个组件可靠地导出相同的命名template,我们可以确保中间件可靠地解释每个声明的阴影dom模板。

打开软件包/client/src/view/post/index.ts。剪切并复制毫无疑问地声明的模板中的function,就像您在最后两个组件中一样。

const shadowTemplate = () => html`<style>
    ${styles}
  </style>
  <div class="post-container">
    <app-header></app-header>
    <div class="post-content">
      <h2>Author: </h2>
      <footer>
        <a href="/">👈 Back</a>
      </footer>
    </div>
  </div>`;

PostViewconstructor中致电shadowTemplate()

template.innerHTML = shadowTemplate();

声明一个名为template的新的function,并确保用声明的影子DOM封装shadowTemplate

const template = () => html`<post-view>
  <template shadowrootmode="open">
    ${shadowTemplate()}
  </template>
</post-view>`;

从包装/客户端/src/view/post/index.ts。
导出template

export { PostView, template };

我们稍后将返回此文件,但是现在应该足以显示一些文本和一个将用户导航回MainView的链接。到中间件...

快速中间件

@lit-labs/ssr是一个由Google Lit发行的NPM软件包,用于服务器端渲染点亮的模板和组件。该软件包用于Node.js的上下文中,可以与Express Mifdreware一起使用。 Express是一个流行的HTTP服务器,用于Node.js,主要基于中间件。快速中间件是拦截HTTP请求并允许您处理HTTP响应的功能。我们将编码将请求处理给http://localhost:4444http://localhost:4444/post/的快速中间件,并将其称为function作为ssr。该功能中的算法是您将要处理的。

首先,在包装/服务器/src/index.ts中知道您要使用的中间件已导入并设置在两个路由上。

import ssr from "./middleware/ssr.js";
...
app.get("/", ssr);
app.get("/post/:slug", ssr);

当用户访问http://localhost:4444/http://localhost:4444/post/时,中间件会被激活。邮政视图的第二个路线中的符号:slug表示中间件应该期望一个名为“ slug”的路径段,该路径现在将出现在Request params上,传递到Middleware function中。

如果您从未编码Node.js,请不要担心。我们将像在Pervious部分一样逐步进行逐步编码,以编码将为每个视图的声明的影子DOM模板服务的快速中间件。

打开包/服务器/src/middleware/ssr.ts开始编码快递中间件。浏览文件以使自己熟悉可用的import并声明为const

请注意,如何将不同的文件注入renderApp中声明的HTML。这里与readFileSync一起使用相对路径,以找到名为style.css的全局样式文件的路径,然后缩小样式(以提高性能)。可以通过使用env变量动态设置minifyCSS来关闭降低,该变量用于确定代码是否在“开发”或“生产”环境中运行。

const stylePath = (filename) =>
  resolve(`${process.cwd()}../../style/${filename}`);
const readStyleFile = (filename) =>
  readFileSync(stylePath(filename)).toString();

const styles = await minify(readStyleFile('style.css'), {
  minifyCSS: env === "production",
  removeComments: true,
  collapseWhitespace: true,
});

在上面的示例中,博客的全局样式是从monorepo中的文件中读取的,然后根据环境缩小。

中间件function在文件的底部找到。目前,中间件调用renderApp(),并通过调用res.status(200)来响应HTTP请求,该请求是“成功”的HTTP响应代码,然后是.send(),然后用ES2015模板字符串从renderApp返回。


export default async (req, res) => {
  const ssrResult = renderApp();
  res.status(200).send(ssrResult);
};

选择下图中显示的样板,然后删除<script>

Image description

在模板中插入一个新字符串,以显示浏览器窗口中的更改。在下面的示例中,我们将“ Hello World”注入模板。这将是暂时的,因为renderApp function很快就会变得动态。

function renderApp() {
  return `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">${'Hello World'}</div>
  </body></html>`;
}

处理中间件中的路线

由于此中间件将处理Express中的多个路线,因此我们需要一种检测是否应提供路线的方法。仅在宣布路线时才能提供HTML。在包装/客户端/src/routes.js中,每个路由在Array中声明。您可以直接从文件中导入此Array,以确保使用文件扩展名为“ .js”。这是通过Node.js中的ES2015模块处理导入的方式,该模块反映了浏览器期望声明导入的方式。尽管我们在打字稿文件中进行编码,但“ .js”仍在路径中使用。

import { routes } from '../../../client/src/routes.js';

每个Route键入如下。您可以将此类型引用以编码基于HTTP请求返回Route的算法。您也可以依靠您的IDE IntelliSense。

export type Route = {
  component: string;
  path?: string;
  pathMatch?: RegExp;
  tag: string;
  template: (data?: any) => string;
  title?: string;
  params?: any;
};

在中间件function中,编写一个处理两个不同用例的算法:

  1. 如果路由声明“路径”,请将路由完全匹配到HTTP请求上的当前originalUrl
  2. 如果您声明了“路径匹配”,这是匹配RegExp路由的一种方式,请在RegExp上致电test,以确定正则表达式是否与HTTP请求上的originalUrl匹配。

完成后,记录匹配的路由。它应该在您的终端中记录Route。当用户访问http://localhost:4444而不是http://localhost:4444/path时,应考虑边缘案例,并将其声明为/,仍然应该有匹配项。

export default async (req, res) => {
  let route = routes.find((r) => {
    // handle base url
    if (
      (r.path === '/' && req.originalUrl == '') ||
      (r.path && r.path === req.originalUrl)
    ) {
      return r;
    }
    // handle other routes
    if (r.pathMatch?.test(req.originalUrl)) {
      return r;
    }
  });

  console.log(route);
  ...

如果没有匹配,Array.prototype.find将返回undefined,因此请通过将用户重定向到“ 404”路线来解决此问题。我们现在实际上不会在这条路线上工作,但是对于奖励积分,您以后可以使用404页。

  if (route === undefined) {
    res.redirect(301, '/404');
    return;
  }

接下来,将当前的HTTP请求params添加到Route。这对于具有单个param :slug的单个帖子视图是必需的,该视图现在可以通过route.params.slug访问。

  route = {
    ...route,
    params: req.params,
  };

现在您有了匹配的路线,应该可以访问存储在Route上的路由模板,但是我们首先必须“构建”开发路线,就像它在生产中建立一样。当路由在client软件包中构建时,将每个路线部署到包装/客户端/DIST作为单独的JavaScript捆绑包。我们可以在开发环境中使用编程重建来构建与每个路线匹配的JavaScript捆绑包,从而模拟这一点。

首先,定义一个新的function,该28返回到src目录中的视图源的路径,或者在客户端包的DIST目录中的捆绑包。我们需要这两条路径,因为在开发过程中,我们将构建从SRC目录到Dist的每个视图。

const clientPath = (directory: 'src' | 'dist', route: any) => {
  return resolve(
`${process.cwd()}../../client/${directory}/view/${route.component}/index.js`
  );
};

在中间件的上下文中使用新的clientPath function来构建路线的JavaScript,调用esbuild.build的路径,并使用映射到entryPoints的源文件,而outfile映射到通往距离目录中分布式捆绑包的路径。这将模拟捆绑包的分配方式,但将Esbuild用于开发,这应该在开发环境中对每个请求都快速有效。

 if (env === 'development') {
    await esbuild.build({
      entryPoints: [clientPath('src', route)],
      outfile: clientPath('dist', route),
      format: 'esm',
      minify: env !== 'development',
      bundle: true,
    });
}

渲染模板

要渲染从每个捆绑包导出的声明的阴影dom模板,我们需要动态导入JavaScript捆绑包。重复使用clientPath function,这次是在动态的import语句的上下文中设置了一个名为module的新的const。这将使我们访问从视图的JavaScript捆绑中导出的任何内容。

  const module = await import(clientPath('dist', route));

声明另一个名为compiledTemplateconst,该compiledTemplate称为template functionmodule导出。 template返回了我们之前在packages/client/src/view/main/index.ts中定义的声明的阴影dom模板。

  const compiledTemplate = module.template();

在将template function返回的String传递给另一个名为functionfunction之前,我们需要对html进行消毒。 render最终将是解析和流式传输HTML响应的声明影子DOM模板的函数。我们还可以提供一种自定义的消毒算法,但是无论结果如何通过 @Lit-Labs/ssr导出的unsafeHTML function传递模板。您可以想像React中的dangerouslySetInnerHTML这样的功能。 @lit-labs/ssr导出的render函数期望在调用render之前通过unsafeHTML对任何HTML模板进行消毒。

制作一个名为sanitizeTemplate的新的function,应该标记为async,因为unsafeHTML本身是异步的。 html在此示例中是从LIT软件包导出的,它是LIT库用于处理HTML模板的功能。它本质上与html非常相似

export const sanitizeTemplate = async (template) => {
  return await html`${unsafeHTML(template)}`;
};

在SSR.TS中导出的中间件function中,制造了一个新的const,并将其设置为呼叫sanitizeTemplate,并通过compiledTemplate。最后,将template传递给renderApp

const template = await sanitizeTemplate(compiledTemplate);
const ssrResult = renderApp(template);

renderApp中添加第一个参数,适当地命名为template。替换了早期的“ Hello World”,电话将呼叫render,并通过templaterender是从 @lit-labs/ssr导出的function,负责为整个视图渲染声明性的影子dom模板。有一些逻辑render用于处理用LIT构建的组件,但它也可以接受宣言的阴影DOM模板。

function renderApp(template) {
  return `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">${render(template)}</div>
  </body></html>`;
}

如果您使用IntelliSense透露有关render的更多信息,则您将学习render接受可以解析我的Lit-HTML的任何模板,这是LIT生态系统中的另一个软件包。您还会发现function返回“字符串迭代器”。这里要带走的事情是render与Lit的Vanilla自定义元素和自定义元素一起使用。

Image description

现在,您会注意到模板无法正确渲染。而是在浏览器中打印了[object Generator]。这里发生了什么?提示来自render返回的内容:“字符串迭代器”。 Generator是在ES2015中引入的,这通常是一个不受欢迎但强大的JavaScript语言的方面,尽管LIT正在使用此处的发电机来支持在HTTP请求的请求/响应生命周期中流向该值,例如。

Image description

处理发电机

支持在OUT代码中支持render输出的一种简单方法是将renderApp转换为Generator。这很容易。可以使用function*语法来定义Generator,该语法定义了返回Generatorfunction。调用Generator不会立即执行function*主体中定义的算法。您必须定义yield表达式,该表达式指定迭代器返回的值。 Generator只是一种Iteratoryield*在我们的示例render中将某些东西委派给另一个Generator

renderApp转换为Generator function,将html文档分解为逻辑yieldyield*表达式,如下示例。

function* renderApp(template) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">`;
  yield* render(template);
  yield `</div>
  </body></html>`;
}

完成后,浏览器窗口中的输出应更改。相当厌恶的,是吗?

Image description

有一个提示,原因是render的文档中发生了这种情况。 function“流”结果,我们还没有处理流。在我们这样做之前,请更新中间件function的底部,请确保await renderApp的结果,该结果现在由于是Generator function而异,现在是异步的。

  const ssrResult = await renderApp(template);

流式传输结果

node.js中的流可以表示为Buffer。最终,我们需要做的是将流转换为String,该流可以作为HTML文档发送给HTTP响应中的客户端。 Buffer可以通过调用toString转换为String,该toString也接受utf-8格式作为参数。 UTF-8是必要的,因为它被定义为HTML的默认字符编码。

流是异步的。我们可以使用forawait结合使用,将流的每个“块”推到Buffer,然后使用Buffer.concat组合所有Buffer,然后将结果转换为UTF-8编码的String。制作一个名为streamToString的新的function,可以做到这一点,给它一个名为stream的参数并使function返回String

async function streamToString(stream) {
  const chunks = [];
  for await (let chunk of stream) {
    chunks.push(Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString('utf-8');
}

上面的似乎只会接受我们从renderApp返回的流,但是我们首先应该通过Readable(node.js的实用程序)将流传递给我们,它允许我们读取流。制作另一个名为renderStreamasync function

async function renderStream(stream) {
  return await streamToString(Readable.from(stream));
}

最后,在中间件中,function制作了另一个名为stream的变量,并调用renderStream,并通过ssrResult。然后,使用stream致电res.send

 let stream = await renderStream(ssrResult);
 res.status(200).send(stream);

voilã!在浏览器中,在声明的影子DOM模板中定义的标题现在可见。

Image description

如果您检查了HTML,您会注意到render function中留下的文物,该评论留下了。声明的影子DOM模板已成功地渲染了服务器端。

困难的部分已经结束。我们将返回中间件以增强其部分,但是大部分工作都完成了。您刚刚编码的中间件现在可以用来渲染声明的影子DOM模板。

让我们在视图中添加一些内容。接下来,您将更新Card自定义元素以导出声明的影子DOM模板,然后将模板导入MainView自定义元素。然后,您将使用有关每个博客文章显示元数据的卡填充视图。

用声明的影子DOM更新卡

在本节中,您将学习如何处理迄今为止最复杂的模板后使用Delcarative Shadow dom模板的HTML模板插槽,可重复使用的卡仅通过HTML模板插槽接受内容。

更新Card打开软件包/client/src/component/card/card.ts。就像以前的示例中一样,剪切并粘贴了在constructor中毫无疑问地声明的模板为由名为shadowTemplate的新的function返回的模板字符串。在此function中添加一个参数,该参数允许您传递模板的样式。请注意,每个Card的标题,内容,页脚布局如何接受命名的HTML模板插槽。

const shadowTemplate = ({ styles }) => html`
  <style>
    ${styles}
  </style>
  <header>
    <slot name="header"></slot>
  </header>
  <section>
    <slot name="content"></slot>
  </section>
  <footer>
    <slot name="footer"></slot>
  </footer>
`;

在自定义元素constructor中将innerHTML设置为新的shadowTemplate function

template.innerHTML = shadowTemplate({ styles });

Header示例中的链接,Card是一个叶子节点,因此我们可以在声明性的影子DOM模板中重复使用shadowTemplate批发。制作一个名为template的新的function,它通过一个参数传递并用适当的语法封装了shadowTemplate

const template = ({ styles }) => html`<app-card>
  <template shadowrootmode="open"> 
  ${shadowTemplate({ styles })}
  </template>
</app-card>`;

卡应显示缩略图,博客文章标题,简短说明和链接。在template function的第一个参数上声明新属性,该属性为contentheadlinelinkthumbnail,除了styles

我们仍然可以在声明的影子DOM模板中将内容投影到插槽中。 Shadow dom包含在shadowTemplate函数中。无论元素在<template>之外的位置,都被视为光DOM,并且可以使用slot属性通过插槽投影。对于模型上的每个属性,创建一个新元素。一个带有slot属性的新的<img>标签设置为header将投影到自定义元素的<header>中。将src属性设置为插值的${thumbnail}alt属于插值的${content},以描述屏幕读取器的图像。另外定义了<h2><p>。如果模板的任何部分变得太复杂,则可以将整个模板部分包裹在字符串插值中。在<a>标签上设置href属性就是这种情况,该标签显示了用户的“读取帖子”链接。

const template = ({
  content,
  headline,
  link,
  thumbnail,
  styles,
}) => html`<app-card>
  <template shadowrootmode="open"> ${shadowTemplate({ styles })} </template>
  <img slot="header" src="${thumbnail}" alt="${content}" />
  <h2 slot="header">${headline}</h2>
  <p slot="content">${content}</p>
  ${html`<a href="/post/${link}" slot="footer">Read Post</a>`}
</app-card>`;

由于每个元素都使用一个符合此自定义元素在Shadow dom中找到的插槽的命名HTML模板插槽,因此每个元素都将被项目投入Shadow dom。如果两个元素共享一个命名插槽,则将按定义的顺序进行投影。

最后,从文件中导出stylestemplate

export { styles, template, AppCard };

我们立即将注意力转换为包装/client/src/view/main/index.ts,我们需要更新声明的影子dom模板以接受从card.ts.ts导出的新的template

获取数据模型

现代网站中的视图很少是静态的。通常,必须使用数据库中的内容填充视图。在我们的项目中,数据存储在每个博客文章的Markdown文件中。无论哪种方式,REST API都可以用于从端点获取数据并用内容填充视图。在下一部分中,我们将开发一种模式,用于从REST API获取数据,并将所得的JSON与您在Pervious部分中编码的中间件集成在一起。

您会将数据从两个端点注入MainView声明的阴影dom模板。从'http://localhost:4444/api/meta返回的JSON可用于渲染AppHeader,而PostArrayhttp://localhost:4444/api/posts返回将用于渲染AppCard的列表。

打开软件包/client/src/view/main/index.ts开始编码本节。

就像我们对template所做的一样,我们正在标准化模式。这次是为了将数据注入每个声明的阴影DOM模板中,该声明异步API调用该数据获取数据。本质上,我们正在谈论“视图”的“模型”,因此我们将新的function“ FetchModel”称为。

function应返回Promise。使用TypeScript,我们可以严格键入定义Promise,并将该定义与template的第一个参数匹配。最终,此数据模型将传递给template,使我们能够用内容来补充视图。

function fetchModel(): Promise<DataModel> {}
const template = (data: DataModel) => string;

起初这似乎是断开连接的,因为在index.ts中,这两个function都没有工作。在Express中间件中,我们可以从捆绑包中访问导出,因此,如果我们export fetchModel,我们还可以在中间件的上下文中调用function,并将结果传递给template,该结果期望相同的DataModel。这就是为什么我们需要标准化模式。

MainViewfetchModel实现如下。使用Promise.allfetch调用每个端点,然后将每个响应映射到从端点返回的JSON,最后再次致电then以将所得的JSON映射到DataModel中预期的架构。 http://localhost:4444/api/meta的JSON响应将用于填充AppHeader,而http://localhost:4444/api/posts将用于填充AppCard列表的内容。

function fetchModel(): Promise<DataModel> {
  return Promise.all([
    fetch('http://localhost:4444/api/meta'),
    fetch('http://localhost:4444/api/posts'),
  ])
    .then((responses) => Promise.all(responses.map((res) => res.json())))
    .then((jsonResponses) => {
      const meta = jsonResponses[0];
      const posts = jsonResponses[1].posts;
      return {
        meta,
        posts,
      };
    });
}

使用名为data的新参数更新template并将其定义为DataModel。使用data.meta.title访问的title更新appHeaderTemplate title

const template = (data: DataModel) => html`<main-view>
  <template shadowrootmode="open">
    ${appHeaderTemplate({ styles: appHeaderStyles, title: data.meta.title })}
  </template>
  </main-view>`;

我们仍然需要集成显示每个博客文章预览的卡。从card.js导入AppCardstylestemplate,请确保在适当的地方重命名导入,以免与任何本地变量发生冲突。

import {
  styles as appCardStyles,
  template as appCardTemplate,
  AppCard,
} from '../../component/card/Card.js';

appCardTemplate集成到template function中,将Array映射到data.posts上,以显示显示每个Card所需的模型。您需要将Array转换为String,因此,请使用空的String致电join,作为将Array转换为String的参数。 (可选)使用导入到文件中的名为joinTemplates的实用程序,而不是直接调用join。还在这里注入每个Card的样式。

const template = (data: DataModel) => html`<main-view>
  <template shadowrootmode="open">
    <style>
      ${styles}
    </style>
    <div class="post-container">
    ${appHeaderTemplate({ styles: appHeaderStyles, title: data.meta.title })}
    ${data.posts
      .map(
        (post) =>
          `${appCardTemplate({
            styles: appCardStyles,
            headline: post.title,
            content: post.excerpt,
            thumbnail: post.thumbnail,
            link: post.slug,
          })}`
      )
      .join('')}
     </div>
  </template>
  </main-view>`;`;

从index.ts。
导出以下所有内容

export { template, fetchModel, AppCard, AppHeader, MainView };

处理中间件中的fetchmodel

要集成从捆绑包中导出到中间件的fetchModel,打开套餐/server/src/middleware/ssr.ts。

在中间件的顶部function声明了一个名为fetchedData的新变量。

export default async (req, res) => {
  let fetchedData;

在我们导入捆绑包的行之后,检查ES2015模块是否以条件表达式导出fetchModel。如果说实话,请使用awaitfetchedData设置为module.fetchModel()的结果,因为fetchModel返回Promise。我们不一定想使任何假设的静态布局都需要fetchModel

预计下一个显示单个帖子的路线,将route传递给fetchModel。我们现有的实现将有效地忽略此参数,但是我们需要在下一个示例中有关route.params的信息。最后,将fetchedData传递到module.template的电话中。

  const module = await import(clientPath('dist', route));

  if (module.fetchModel) {
    fetchedData = await module.fetchModel(route);
  }

  const compiledTemplate = module.template(fetchedData);

您现在应该能够在http://localhost:4444上查看服务器端渲染标头和卡片列表。此视图完全是服务器端。网络请求是服务器端的,声明的影子DOM模板是由 @lit-labs/ssr render Generator构建的。接下来,您将采用您所学的知识,并使用相同的方法渲染单个帖子视图。

渲染一个帖子

如果您单击任何“阅读更多”链接,您会以相当令人印象深刻的布局受到欢迎。空白的“作者:”字段和指向“背”的手应该向您打招呼。这是我们之前使用的样板的结果。

Image description

导航到此视图后,如果您仍然启用了console.log,则应使用不同的Route注意终端更新。

Image description

在本节中,我们将使用博客文章的内容填充单个帖子视图。每个博客文章均以Markdown编写,并存储在软件包/服务器/数据/帖子目录中。每个帖子都可以通过http://localhost:4444/api/post/:slug的REST API端点访问。如果有一个带有匹配的“ slug”的帖子,则端点将返回JSON中的博客文章,并在JSON响应中找到了Markdown。

打开软件包/client/src/view/post/index.ts开始针对单个邮政视图进行编码。

import {
  styles as appHeaderStyles,
  template as appHeaderTemplate,
  AppHeader,
} from '../../component/header/Header.js';
  const template = (data: DataModel) => html`<post-view>
  <template shadowrootmode="open">
    <style>
      ${styles}
    </style>
    <div class="post-container">
      ${appHeaderTemplate({ styles: appHeaderStyles, title: data.post.title })}
      <div class="post-content">
        <h2>Author: ${data.post.author}</h2>
        ${data.html}
        <footer>
          <a href="/">👈 Back</a>
        </footer>
      </div>
    </div>
  </template>
</post-view>`;

我们可以在很大程度上从单个后视图中的主视图重用fetchModel function。将函数从main/index.ts复制并粘贴到post/index.ts,对其进行修改以接受新参数。还记得我们在中间件中将route传递给fetchModel的时候吗?这就是为什么。我们需要在route.params上找到的slug属性,以向http://localhost:4444/api/post/:slug提出请求。修改fetchModel function,直到获得一个工作示例,如下。

收到降价后,我们需要将降价转换为可用的HTML。该文件中提供了GitHub API,以帮助解决此目的。博客文章包含代码片段,而Octokit是解析降价并将其转换为HTML的方便实用程序。通过拨打octokit.request,将一个名为request的新的const命名为request,然后通过本地API端点的响应向Octokit API进行了另一个HTTP请求。我们想向/markdown端点提出一个POST请求,并将request.post.content传递到请求正文上的text属性,同时确保在headers中特定特定API版本。


function fetchModel({ params }): Promise<any> {
  const res = async () => {
    const request = await Promise.all([
      fetch('http://localhost:4444/api/meta'),
      fetch(`http://localhost:4444/api/post/${params['slug']}`),
    ])
      .then((responses) => Promise.all(responses.map((res) => res.json())))
      .then((jsonResponses) => {
        return {
          meta: jsonResponses[0],
          post: jsonResponses[1].post,
        };
      });

    const postContentTemplate = await octokit.request('POST /markdown', {
      text: request.post.content,
      headers: {
        'X-GitHub-Api-Version': '2022-11-28',
      },
    });
    return {
      ...request,
      html: postContentTemplate.data,
    };
  };
  return res();
}

postContentTemplate将通过data属性将转换后的降价返回到HTML。在fetchModel function中将其与metapost一起返回,并在名为html的新物业上返回。完成后,导出中间件的所有相关零件。

export { template, fetchModel, AppHeader, PostView };

如果您刷新或导航到 /,请单击任何卡上的“阅读更多”,现在您应该能够查看单个博客文章。您刚刚用声明的Shadow dom完成了第二个服务器端渲染视图!这是研讨会的最后观点。其余部分将涵盖处理SEO,水合等的其他内容。

为SEO处理元数据

在这种特殊情况下,可以从SEO的博客文章内容中收集很多东西。每个降价文件都有一个包含可用于SEO目的的元数据的标题。

---
title: "Form-associated custom elements FTW!"
slug: form-associated-custom-elements-ftw
thumbnail: /assets/form-associated.jpg
author: Myself
excerpt: Form-associated custom elements is a web specification that allows engineers to code custom form controls that report value and validity to `HTMLFormElement`...
---

使用物质软件包将http://localhost:4444/api/post API端点转换为JSON。您可以在包装/服务器/SRC/ROUTE/POST.TS中对自己进行评论。

您可以扩展此内容,为JSON-LD提供相关的元数据,或在HTML文档中设置<meta>标签。出于本研讨会的目的,我们将仅设置document.head(Page <title>)的一个方面,但是您可以进一步推断此方面并将renderApp修改为项目规格。

打开软件包/服务器/src/middleware/ssr.ts开始编码本节并导航到中间件function。在多个await之前的某个点,我们可以在Route上提出相关的seo元数据。如果我们想用每个博客文章的标题覆盖route.title,我们可以通过将属性设置为以fetchedData.meta.title返回的属性来做到这一点。

  route.title = route.title ? route.title : fetchedData.meta.title;

route传递到renderApp function

  const ssrResult = await renderApp(template, route);

route.title设置<title>标签。

function* renderApp(template, route) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <title>${route.title}</title>
   ...

无论您从何处得出SEO元数据,每个项目都可能会有所不同,但是在我们的情况下,我们可以将此元数据存储在每个标记文件中。由于我们具有RoutefetchModel等可重复的模式,因此我们可以可靠地将元数据传递到中间件中渲染的每个HTML文档服务器端。

水合

到目前为止,您所做的所有编码都完全是服务器端。浏览器将播放的任何JavaScript发生在服务器上,并且每个声明的Shadow dom模板都由浏览器解析。但就是这样。如果自定义元素中还有其他任何其他javascript需要在浏览器中运行,例如将回调绑定到单击侦听器,则尚未发生。我们需要在特定视图客户端中进行自定义元素的水合。

有一个简单且性能的修复程序。我们可以在本地托管每个路由的捆绑包,并在提出该请求的HTML文档中添加一个脚本标签,但是更性能的解决方案是嵌入JavaScript,完全消除了网络请求。

readFileSync是一个node.js实用程序,它允许我们从文件中读取捆绑包并将内容转换为String。将String设置为名为script的新的const

  const module = await import(clientPath('dist', route));
  const script = await readFileSync(clientPath('dist', route)).toString();

在第三个参数中将script传递到renderApp function

const ssrResult = await renderApp(template, route, script);

在HTML文档的末端添加一个<script>标签,用名为scriptString设置内容。

function* renderApp(route, template, script) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <title>${route.title}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">`;
  yield* render(template);
  yield `</div>
    <script type="module">${script}</script>
  </body></html>`;
}

我们可以在AppHeader中可用测试水合物。开放
packages/client/src/component/header/header.ts。

else语句添加到constructor中已经定义的条件。此条件的if (!this.shadowRoot)正在检查是否不存在阴影根,如果是的,则迫切声明了新的阴影根。由于自定义元素具有声明的Shadow dom声明的模板,因此if中的内容将无法运行,因此,如果我们希望仅运行其他逻辑,仅客户端,则需要else。通过将Hydrated附加到元素innerText

class AppHeader extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = document.createElement('template');
      template.innerHTML = shadowTemplate({ styles, title });
      shadowRoot.appendChild(template.content.cloneNode(true));
    } else {
      const title = this.shadowRoot.querySelector('h1').innerText;
      this.shadowRoot.querySelector('h1').innerText = `${title} Hydrated`;
    }
  }
}

测试完成后,卸下else。本节演示了如何补充自定义元素客户端的水分,但是服务器如何了解此JavaScript?你们中有些人可能想知道服务器在Node.js中不存在HTMLElement时如何解释class AppHeader extends HTMLElement。最后一部分是解释如何与 @lit-labs/ssr一起使用。编码完成。恭喜!您已经完成了研讨会。

光滑浏览器规格

LIT SHIMS浏览器规格,需要通过 @Lit-Labs/ssr/lib/dom-shim.js导出的function在服务器上运行,名为installWindowOnGlobal。如果您所有的项目依赖于HTMLElement延伸的自主自定义元素,则可以通过在Node.js中的其他任何内容之前调用此function来获得。

在包装/垫片/src/index.ts上找到垫片的自定义实现。我将此自定义的垫片用于书《全栈网络组件》一书中的章节,因为我们在书中的视图包含自定义的内置元素。 LIT足够深思,可以让像我这样的工程师使用服务器端渲染浏览器规格所需的其他模拟扩展垫片。使用此文件作为如何在服务器端渲染项目中溶解浏览器规格的示例。

installShimOnGlobal是从MonorePo中的此软件包导出的,并将其导入到packages/server/src/index.ts。在任何其他代码之前都调用。

import { installShimOnGlobal } from "../../shim/dist/index.js";

installShimOnGlobal();

在任何其他代码在node.js中运行之前,都必须固定浏览器规格。这就是为什么首先执行垫片的原因。

结论

在此研讨会中,您的服务器端使用声明的影子DOM和带有Express Middleware的 @Lit-Labs/SSR软件包渲染了一个博客。您采用了自主的自定义元素,该元素迫切地声明了阴影根,并制造了可重复使用的模板,也支持声明性的影子dom。声明性的影子DOM是一个标准,它允许Web开发人员声明地用特殊的HTML模板将样式和模板封装。 HTML模板的“特殊”部分是元素必须使用shadowrootmode属性。

您学习了如何使用从 @lit-labs/ssr导出的render Generator到HTML文档中的服务器端渲染模板部分。我们只渲染了一个模板,但是您可以推断出所学的内容,以渲染多个模板。

每种Route的静态配置使用,从每个视图捆绑包中导出templatefetchModel。这只是一种工作方式。主要要点是用于服务器端渲染,您应该标准化系统的某些部分以简化开发并轻松进行集成。我们定义的模式可以用于Codegen。通过可靠地导出templatefetchModel,我们可以确保中间件同样可靠地解释声明的影子dom。

我们没有涵盖此研讨会中的整个构建系统。在引擎盖下,Nodemon在Esbuild负责制造生产时正在关注变化。如果您建造生产,您会发现输出是100%缩小的,并且在灯塔中的性能100%得分。有人可以通过Vite进一步简化开发,主要的好处是Hot Module重新加载。

我们还没有涵盖如何使用 @lit-labs/ssr使用litelement,这是故意的。点燃是一个很棒的图书馆。酌情使用它。我发现它更有趣,因为它是由浏览器规格构建的,可以使用浏览器规格(如声明的Shadow dom)进行操作,而无需直接使用LIT编码。我希望您也觉得这很有趣。我想强调的另一个方面。

当我从90年代开始从事Web开发时,我赞赏HTML和JavaScript的平等性质。任何人都可以编码网站,就像任何人都可以读书一样。随着时间的流逝,前端网络开发变得太复杂了。试图简化开发人员体验的框架和图书馆是以用户体验为代价的,同时也使想要学习如何编码网站的人的进入障碍更大。我演示了如何仅使用浏览器标准来渲染网站并利用库中的单个function。任何人都可以学习如何编码声明性的影子DOM,就像几十年前一样,任何人都可以学习如何编码HTML。希望您发现使用声明性的影子DOM完成服务器端渲染很容易,迫不及待地想看看您的构建。

下面的链接评论,以便我们可以在灯塔中看到100%的性能分数。

如果您喜欢本教程,您会在我的有关Web组件的书中找到更多类似的内容。下面的详细信息。

Fullstack Web组件

您现在正在寻找网络组件,但不知道从哪里开始?我写了一本名为Fullstack Web Components的书,该书是用自定义元素编码UI库和Web应用程序的动手指南。在Fullstack Web组件中,您将...

  • 使用自主,自定义的内置和形式相关的自定义元素,Shadow dom,html模板,CSS变量和声明性的影子dom
  • 代码几个组件
  • 开发一个带有打字稿装饰器的微型图书馆,可以简化UI组件开发
  • 学习使用Storybook维护UI Web组件的UI库的最佳实践
  • 使用Web组件和Typescript的代码应用程序

Fullstack Web Components is available now at newline.co

Fullstack Web Components Book Cover