网络组件比你想象的更容易

Avatar of John Rhea
John Rhea

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

当我参加会议(当我们能够做这些事情的时候)并看到有人做关于网络组件的演示时,我一直认为它很不错(是的,显然,我来自 1950 年),但它总是看起来很复杂而且过分。一千行 JavaScript 来节省四行 HTML。演讲者不可避免地要么掩盖了大量的 JavaScript 以使其正常工作,要么他们会详细说明,而我的眼睛会变得模糊,因为我在想我的每日津贴是否包括零食。

但在最近的 参考项目 中,为了使学习 HTML 变得更容易(通过添加僵尸和愚蠢的笑话,当然),我内心完美主义者决定我必须涵盖规范中的每个 HTML 元素。除了那些会议演示之外,这是我第一次接触到 <slot><template> 元素。但当我试图写一些准确且引人入胜的东西时,我不得不更深入地研究。

并且在这个过程中我学到了一些东西:网络组件比我记忆中的容易得多。

文章系列

要么是网络组件自从我上次在会议上梦想着零食时已经有了很大的进步,要么是我对它们的最初恐惧阻碍了我真正了解它们——可能两者都有。

我在这里告诉你,你可以——是的,——创建一个网络组件。让我们暂时把我们的分心、恐惧,甚至我们的零食都留在门口,一起做这件事。

让我们从 <template> 开始

<template> 是一个 HTML 元素,它允许我们创建,嗯,一个模板——网络组件的 HTML 结构。模板不必是大量的代码。它可以像这样简单:

<template>
  <p>The Zombies are coming!</p>
</template>

<template> 元素很重要,因为它将所有东西都整合在一起。它就像建筑的基石;它是构建其他一切的基础。让我们将这段小的 HTML 代码作为 <apocalyptic-warning> 网络组件的模板——你知道的,当僵尸末日来临时,这是一个警告。

然后是 <slot>

<slot> 只是一个像 <template> 一样的 HTML 元素。但在这种情况下,<slot> 自定义了 <template> 在页面上呈现的内容。

<template>
  <p>The <slot>Zombies</slot> are coming!</p>
</template>

在这里,我们在模板化的标记中插入了“Zombies”一词。如果我们不对插槽做任何操作,它将默认为标签之间的内容。在本例中,将是“Zombies”。

使用 <slot> 很像使用占位符。我们可以按原样使用占位符,或者定义其他东西来代替它。我们使用 name 属性来做到这一点。

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

name 属性告诉网络组件模板中哪个内容对应哪个位置。现在,我们有一个名为 whats-coming 的插槽。我们假设在末日中僵尸会先来,但 <slot> 为我们提供了一些灵活性来插入其他东西,例如如果最终是机器人、狼人,甚至是网络组件末日。

使用组件

我们实际上已经“编写”了组件,可以将其放置在我们想要使用它的任何地方。

<apocalyptic-warning>
  <span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

看到我们做了什么吗?我们将 <apocalyptic-warning> 组件放在页面上,就像任何其他 <div> 或其他元素一样。但我们还在那里放置了一个 <span>,它引用了我们 <slot>name 属性。而这个 <span> 之间的内容就是我们希望在组件渲染时替换“Zombies”的内容。

这里有一个值得注意的小问题:自定义元素名称必须包含连字符。这只是你需要了解的事情之一。 规范 规定这样做是为了防止在 HTML 发布具有相同名称的新元素时发生冲突。

到目前为止,你还能跟上吗?还不算太可怕吧?除了僵尸。我们还需要做一些工作才能使 <slot> 交换成为可能,这就是我们开始使用 JavaScript 的地方。

注册组件

正如我所说,你需要一些 JavaScript 来使这一切工作,但这并非像我之前想象的那样超级复杂、上千行的、深入的代码。希望我也可以说服你。

你需要一个注册自定义元素的构造函数。否则,我们的组件就像不死生物:它在那里,但没有完全复活。

这是我们将使用的构造函数

// Defines the custom element with our appropriate name, <apocalyptic-warning>
customElements.define("apocalyptic-warning",

  // Ensures that we have all the default properties and methods of a built in HTML element
  class extends HTMLElement {

    // Called anytime a new custom element is created
    constructor() {

      // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element
      super();

      // Grabs the <template> and stores it in `warning`
      let warning = document.getElementById("warningtemplate");

      // Stores the contents of the template in `mywarning`
      let mywarning = warning.content;

      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
    }
  });

我在那里留下了详细的注释,逐行解释了内容。除了最后一行

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

我们在这里做了很多事情。首先,我们获取我们的自定义元素 (this) 并创建一个秘密特工——我的意思是,影子 DOM。mode: open 仅仅意味着来自 :root 之外的 JavaScript 可以访问和操作影子 DOM 中的元素,这有点像为组件设置后门访问。

从那里开始,影子 DOM 已经创建,我们向其附加了一个节点。该节点将是模板的深层副本,包括模板的所有元素和文本。将模板附加到自定义元素的影子 DOM 后,<slot>slot 属性将接管以将内容与应该放置的位置匹配。

看看这个。现在,我们可以放置两个相同组件的实例,只需更改一个元素即可渲染不同的内容。

为组件设置样式

您可能已经注意到该演示中的样式。正如您所料,我们绝对有能力使用 CSS 为组件设置样式。事实上,我们可以在 <template> 中直接包含一个 <style> 元素。

<template id="warningtemplate">
  <style>
    p {
      background-color: pink;
      padding: 0.5em;
      border: 1px solid red;
    }
  </style>

    <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

这样,由于影子 DOM,样式将直接限定到组件,并且不会泄漏到同一页面上的其他元素。

现在在我的脑海中,我认为自定义元素正在复制模板,插入您添加的内容,然后使用影子 DOM 将其注入页面。虽然在前端看起来是这样,但实际上它在 DOM 中并非如此。自定义元素中的内容会保留在原位,而影子 DOM 就像覆盖一样叠加在上面。

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

由于内容实际上位于模板外部,因此我们在模板的 <style> 元素中使用的任何后代选择器或类都不会对插槽内容产生影响。这无法像我希望或预期的那样实现完全封装。但由于自定义元素是一个元素,因此我们可以在任何 CSS 文件中将其用作元素选择器,包括页面上使用的主样式表。虽然插入的材料实际上并不在模板中,但它自定义元素中,来自 CSS 的后代选择器将起作用。

apocalyptic-warning span {
  color: blue;
}

但要注意!主 CSS 文件中的样式无法访问 <template> 或影子 DOM 中的元素。

让我们把所有这些拼凑起来

让我们看一个例子,比如一个僵尸约会服务的个人资料,比如你在末日之后可能需要的一个。为了对默认内容和任何插入的内容进行样式化,我们需要在<template>同时使用<style> 元素 CSS 文件中的样式。

JavaScript 代码完全相同,只是现在我们正在使用不同的组件名称,<zombie-profile>

customElements.define("zombie-profile",
  class extends HTMLElement {
    constructor() {
      super();
      let profile = document.getElementById("zprofiletemplate");
      let myprofile = profile.content;
      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
    }
  }
);

这是 HTML 模板,包括封装的 CSS

<template id="zprofiletemplate">
  <style>
    img {
      width: 100%;
      max-width: 300px;
      height: auto;
      margin: 0 1em 0 0;
    }
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    h3 {
      margin: 0.5em 0 0 0;
      font-weight: normal;
    }
    .age, .infection-date {
      display: block;
    }
    span {
      line-height: 1.4;
    }
    .label {
      color: #555;
    }
    li, ul {
      display: inline;
      padding: 0;
    }
    li::after {
      content: ', ';
    }
    li:last-child::after {
      content: '';
    }
    li:last-child::before {
      content: ' and ';
    }
  </style>

  <div class="profilepic">
    <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
  </div>

  <div class="info">
    <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>

    <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
    <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>

    <div class="interests">
      <span class="label">Interests: </span>
      <slot name="z-interests">
        <ul>
          <li>Long Walks on Beach</li>
          <li>brains</li>
          <li>defeating humanity</li>
        </ul>
      </slot>
    </div>

    <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>

  </div>
</template>

这是我们主 CSS 文件中<zombie-profile> 元素及其后代的 CSS。请注意,其中存在重复,以确保替换的元素和模板中的元素具有相同的样式。

zombie-profile {
  width: calc(50% - 1em);
  border: 1px solid red;
  padding: 1em;
  margin-bottom: 2em;
  display: grid;
  grid-template-columns: 2fr 4fr;
  column-gap: 20px;
}
zombie-profile img {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
  display: inline;
  padding: 0;
}
zombie-profile li::after {
  content: ', ';
}
zombie-profile li:last-child::after {
  content: '';
}
zombie-profile li:last-child::before {
  content: ' and ';
}

现在一起!

虽然仍然有一些问题和其他细微差别,但我希望你现在比几分钟前更有能力使用 Web 组件。像我们这里一样试水。也许可以在你的工作中撒上一些自定义组件,以了解它以及它在何处有意义。

实际上就是这些。现在,你更害怕什么,Web 组件还是僵尸末日?在不久的过去,我可能会说 Web 组件,但现在我很自豪地说,僵尸是唯一让我担心的事情(好吧,还有我的每日津贴是否够买零食……)