网络基础:网络组件第1部分
#html #网络开发人员 #初学者 #Web组件

内容

  1. Web组件在这里停留
  2. 什么是自定义元素?
  3. 让我们构建
  4. 组件生命周期
  5. 摘要

1. Web组件在这里留下来

Web组件是构建和组织UI代码的抽象。这是一组浏览器本机工具,您可以用来将HTML标记,CSS样式和JavaScript行为封装到可重复使用的组件中[1]。在处理复杂的状态和非平凡的整合时,组件模型是一个有用的抽象,在这个空间中有许多框架提供其独特的主张。 Web组件就是一种解决方案,但它是网络标准。抽象主要为我们提供了代码的作者,因此我们可以将来有效地管理和维护它。

浏览器为此提供的工具是:

  1. 自定义元素
  2. Shadow Dom
  3. HTML模板

在这篇文章中,我们将仔细研究什么是自定义元素,如何创建它们以及它们如何在引擎盖下工作。

2.什么是自定义元素?

自定义元素使您能够定义自己的HTML标签,并将标记和行为封装在其中。例如,如果您要构建一个计数器组件,则可以使用自定义元素来跟踪计数,并为其中的按钮单击侦听器,并给它一个有意义的名称。通过将其命名为标签,而不是像div这样的现有HTML元素一个ID,您可以定义与该组件相关的逻辑相关的一个地方。它的行为大致相同,但在HTML级别具有更好的语义。

Two HTML counter components with a decrement and increment button along with a span showing the current count. The first snippet shows this component implemented with a wrapping  raw `div` endraw  and the second shows the same but with a custom HTML tag called  raw `custom-counter` endraw .

第一种方法是完全有效的,对于像计数器这样的简单组件,这足够了。与Web组件相比,它的JavaScript行还要少,但是,当组件的复杂性增长时,有必要使用JavaScript的其他行。将相关代码的分组组合在一起还减轻了维护负担的一部分,这是寻找可以管理该组件行为的代码库的所有不同部分。

我们需要哪些工具来构建自定义元素?由于Web组件是Web平台的原生,因此您不需要任何其他依赖项即可使您前进。有一些框架有助于删除一些样板,但是从本质上讲,构建您自己的自定义元素的工作流程是创建一个扩展HTMLElement的类,并将其作为参数传递给带有标签名称的内置API customElements.define。简单!

2.让我们建造

最好的学习方法是这样做,因此让我们开始构建和探索任何理论。为了跟随,您只需要一个文本编辑器和浏览器即可。您可能会从代码片段或本文的源代码中注意到我正在使用Astro和Typescript,但是您可以轻松地与普通的HTML和JavaScript一起跟随。这样做时,请直接在浏览器中打开HTML文件以查看您的页面正在操作,或者如果您真的想从静态文件服务器使用它,我建议CLI工具serve [2]。使用该工具很简单:

serve <your-file>.html

在这篇文章中,我们将构建一个计时器组件,并且看起来像这样:

An Excalidraw mockup of a timer component. The component is shown with an initial count of 0 and defined with HTML at the top and below is the same component after one second with the count reading 1. Between the two components is some orange text that reads "Increment count every second". The markup of the component is  raw `<article><h2>Timer</h2><div id="timer"><p>Count: <span>0</span></p></div></article>` endraw .

页面加载后,计数器将每秒开始增加。但是,我们将制作自己的Web组件,而不是具有“计时器” ID的div,而是将其称为“ X-Timer”(我想称其为“计时器”,但自定义元素的规则之一是它需要至少要被连字符隔开的两个单词,因此您可以将它们与内置的HTML元素区分开[3],并且命名事物很难)。这将使最终标记看起来像这样:

An Excalidraw diagram of the markup of the  raw `x-timer` endraw  component. The markup reads:  raw `<article><h2>Timer</h2><x-timer><p>Count: <span>0</span></p></x-timer></article>` endraw .

我们希望组件具有以下内部标记,因此我们可以以增量计数值更新span

<p>
  Count: <span>0</span>
</p>

在开始编码之前,让我们建立可以定义此内部标记的三种方式:

  1. 定义HTML中的所有标记,并使用JavaScript将x-timer的孩子元素挂钩
  2. 使用JavaScript创建HTML元素,然后将其添加到x-timer的内部,作为儿童元素
  3. 前两种方法的组合

对于我们的x-timer组件,我们将在标记中定义Web组件本身,然后使用JavaScript定义其内部内容:

<article>
  <h2>A single timer</h2>
  <x-timer />
</article>

在我们的JavaScript中,我们可以定义组件,以便将其加载到文档中时,它将创建内部HTML元素并扩展到此:

<article>
  <h2>A single timer</h2>
  <x-timer>
    <p>Count: <span>0</span></p>
  </x-timer>
</article>

真棒!这给了我们一些脚手架和行动计划。

我们需要以下任何要定义的Web组件的结构:

class Timer extends HTMLElement {
  constructor() {
    super();
  }
}

customElements.define("x-timer", Timer);

我们将Timer命名为“ Timer”,这扩展了一个HTMLElement。我们希望我们的自定义标签被称为x-timer,并且要使浏览器识别它,我们需要使用customElements.define定义它,并给它给它一个类,以便知道该标签该怎么做。现在,让我们跳舞:

class Timer extends HTMLElement {
  count: number;

  constructor() {
    super();
    this.count = 0;

    setInterval(() => {
      console.log("Timer called");

      this.count++; 
    }, 1000);
  }
}

/* --snip -- */

我们已经在类中添加了一个属性,以跟踪计数状态,并在构造时用0初始化它。我们还使用JavaScript函数setInterval定义了一个计时器,并延迟通过1000毫秒,以及一个回调,每次调用计时器并将count属性的count属性汇总到Console。这是整个。这是整个组件的机制将驱动计时器,但我们仍然需要将此计数放在屏幕上。

class Timer extends HTMLElement {
  /* --snip -- */
  constructor() {
    /* --snip -- */
    const countSpan = document.createElement("span");
    const countParagraph = document.createElement("p");
    countParagraph.textContent = "Count: ";
    countSpan.textContent = this.count.toString();

    countParagraph.appendChild(countSpan);
    this.appendChild(countParagraph);

    setInterval(() => {
      /* --snip -- */
      countSpan.textContent = this.count.toString();
    }, 1000);
  }
}

/* --snip -- */

应该做到的!我们已经在构造函数中定义了spanp,然后设置其初始文本内容。然后,我们将跨度附加到段落中,最后将段落加入父(我们的x-timer组件),以创建组件的最终结构。在setInterval回调中,我们添加了一行以更新跨度文本内容时,每当计数更改。当您在浏览器中查看此内容时,您应该像我们打算这样做一样看到计数器增加每一秒。添加样式是可选的,但是您现在应该拥有一个大致相似的页面:

A screenshot of the timer component on the screen with a heading "Web components demo" with the browser console open on the right.

我们还没有完成,因为我已经误导了您,并向您展示了可能出乎意料的代码。当我们在HTML中明确使用此自定义元素时,这恰好工作起作用,但是当您像这样动态创建它并尝试将其插入文档中时,它会以奇怪的方式破裂:

const timer = document.createElement("x-timer");
document.body.appendChild(timer);

我们会得到这样的奇怪错误:

Uncaught DOMException: Operation is not supported

要了解这里发生的事情,我们需要将其引入组件的生命周期。

3.组件生命周期

浏览器如何渲染Web组件?当属性更改或从文档中删除组件时,它如何更新?到目前为止,在我们的实施中,我们已经假设浏览器识别Web组件并将其渲染到屏幕上是同一事件。但是,它们是浏览器渲染管道中的不同事件,这对我们定义和管理组件的方式产生了影响。

浏览器的渲染管道可以概括为三个阶段[4]:

  1. HTML parsing
  2. 计算布局和计算绘画细节
  3. 将所有单个元素合成并最终在屏幕上绘制

An Excalidraw diagram visualizing how and when the browser recognizes and renders web components. When the browser encounters the web component in the parsing phase, it calls the constructor to instantiate it, and continues parsing the rest of the component. Only when it reaches the layout and painting phase does it invoke the lifecycle methods, and in this phase the components have access to DOM operations.

当浏览器遇到Web组件时,它通过调用构造函数并继续解析文档的其余部分来实例化。由于尚未渲染,因此无法访问该文档。这就是为什么我们的组件行为是片状的原因,具体取决于我们直接使用它或以编程方式创建它。正确构建我们的组件的方法是将所谓的“生命周期方法”连接在一起。这些是在可用DOM操作时在渲染管道中稍后在渲染管道中调用的组件上的方法。这些方法是[3]:

  1. connectedCallback
  2. disconnectedCallback
  3. attributeChangedCallback
  4. adoptedCallback

这些回调是在布局和绘画阶段调用的。让我们重构我们的实施以利用connectedCallback

class Timer extends HTMLElement {
  /* --snip -- */
  countSpan: HTMLElement;

  constructor() {
    /* --snip -- */
    this.countSpan = document.createElement("span");

    setInterval(() => {
      /* --snip -- */
      this.countSpan.textContent = this.count.toString();
    }, 1000);
  }

  connectedCallback() {
    console.log("x-timer connected");

    const countParagraph = document.createElement("p");
    countParagraph.textContent = "Count: ";
    this.countSpan.textContent = this.count.toString();

    countParagraph.appendChild(this.countSpan);
    this.appendChild(countParagraph);
  }
}

/* --snip -- */

我们已经将与附加元素相关的所有逻辑移至connectedCallback方法中的Web组件。我们还必须使countSpan成为组件的属性,以便我们可以在构造函数的范围之外引用它。我们可以在构造函数中愉快地将其初始化,因为我们只是创建一个元素,而不是在任何DOM操作中使用它,直到它连接到文档。如果您使用这些更改刷新您的浏览器,您会发现它仍然有效,但已正确实现,以便甚至可以通过编程而没有任何打ic来创建它。

我们并不清楚。当我们的组件从文档中删除时会发生什么?通过查看我们的生命周期方法的名称,disconnectedCallback方法似乎会被调用,但是我们需要对此做任何事情吗?在我们的实施中,只有通过导航或页面关闭而断开整个页面时,我们的组件才会断开连接。在这种情况下,我们无需担心手动断开或删除任何内容,我们可以依靠浏览器执行清理操作。当我们动态删除Web组件时,我们只需要担心它,但这不应该在组件设计中规定。我们应该构建我们的组件,以便可以像其他任何HTML元素一样使用它。那么,我们组件的哪一部分需要手动清理?当父组件被删除时,动态创建的段落和跨度将自动删除。组件的属性也会如此。浏览器的垃圾收集器将像任何JavaScript对象一样处理所有这些。那我们的计时器呢?

setInterval通过接收回调功能和延迟来起作用。然后,它使用我们在每个执行之间定义的延迟反复执行回调。但是,此间隔在记忆中在哪里?它范围范围为其创建的上下文吗?因此,在我们的情况下,这是构造者还是班级?简短的答案是,它没有范围范围内的上下文,并且完全具有单独的执行上下文。

The function signature of  raw `setInterval` endraw  expanded into words like this: "setInterval(any function we want to execute, delay in milliseconds);".

我喜欢将间隔视为生活在某些全球环境中,而setInterval功能为我们提供了一种将事物推向它的方法(要更深入地了解它的工作原理,请参阅JavaScript Event Loop上的这篇文章)。这意味着我们创建的间隔默认情况下会超过组件,因为全局上下文将超过组件。这就是为什么setInterval的完整签名还返回一个可以使用方法clearInterval删除间隔的ID。

The full signature of  raw `setInterval` endraw  showing that it returns an ID. It reads  raw `setInterval(callback, delay) => id` endraw .

如果我们不删除间隔,它将继续在后台执行,并可能导致内存泄漏。当您导航到另一个页面或关闭选项卡时,浏览器的垃圾集合将开始启动,但是如果您打算构建长期寿命的应用程序,那么这将无限期地累积并将应用程序放在磨损中。我们需要稍微重新分配代码以说明间隔的清理:

class Timer extends HTMLElement {
  /* --snip -- */
  timerId: number;

  constructor() {
    /* --snip -- */
    this.timerId = setInterval(/* --snip -- */);
  }

  /* --snip -- */

  disconnectedCallback() {
    console.log("x-timer disconnected");

    clearInterval(this.timerId);
  } 
}

/* --snip -- */

我们已经将setInterval返回的timerId分配给了一个属性,因此我们可以在构造函数的范围之外引用它,并清除disconnectedCallback中的间隔。如果我们重新运行页面,所有内容都应该看起来...相同。好吧,这有点反气候。

让我们创建一个全新的组件,其唯一的责任是添加新的计时器并在完成后清除所有计时器。我们可以使用此新组件与x-timer进行修补:

An Excalidraw mockup of the new component. It has the title "Dynamic timers" and two buttons below it that says "Add timer" and "Clear timers". Below it is the text "No timers running". On the right of the mockup, is the markup of the element, which reads  raw `<article><h2>Dynamic timers</h2><x-timers><button>Add timer</button><button>Clear timers</button><div id="timers" /></x-timers></article>` endraw .

要对其进行一些更改,让我们使用HTML定义组件的大多数内部设备,这样我们就可以看到使用JavaScript将其挂接到儿童元素中看起来看起来像什么。我们将动态添加的唯一元素是x-timer,因此我添加了一个空的div作为所有这些容器。那将使我们的html看起来像这样:

<article>
  <h2>Dynamic timers</h2>
  <x-timers>
    <button aria-label="add-timer">Add timer</button>
    <button aria-label="clear-timers">Clear timers</button>
    <div aria-label="timers">
    </div>
  </x-timers>
</article>

注意:我已经使用aria-label来演示文档中选择元素的不同方法,但是您可以选择它们。

然后相应的组件定义看起来像这样:

class Timers extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    console.log("x-timers connected");
    const timersDiv = this.querySelector("[aria-label='timers']");
    const addTimerButton = this.querySelector("[aria-label='add-timer']");
    const clearTimersButton = this.querySelector("[aria-label='clear-timers']");

    // Default state
    timersDiv.textContent = "No timers running";

    clearTimersButton.addEventListener("click", () => {
      // Return to default state
      timersDiv.textContent = "No timers running";
    });

    addTimerButton.addEventListener("click", () => {
      // Clear timers div if no timers present
      if (timersDiv.textContent === "No timers running") {
        timersDiv.textContent = null;
      }
      const xTimer = document.createElement("x-timer");
      timersDiv.prepend(xTimer);
    });
  }
}

customElements.define("x-timers", Timers);

由于该组件不需要设置,因此所有逻辑都活在connectedCallback中。现在,您应该拥有一个大致相似的页面:

A screenshot of the final application. There are two main elements on the screen. The first one is a single timer and the second is the dynamic timer component with no timers currently running.

您可以根据需要创建和清除任意数量的计时器,并且由于我们已将适当的清理逻辑添加到x-timer中,一切都可以正常工作。现在,您可以尝试从x-timer中的disconnectedCallback删除clearInterval时会发生什么。由于我们添加了一些有用的记录语句,因此您应该能够查看何时调用不同的方法。

A screenshot of the application running with the browser's console open to show the logging added to the web components.

概括

自定义元素是Web组件工具链的关键部分。它们使您能够定义自己的HTML标签,还可以让您定义组件的内部工作。通过查看浏览器解析和渲染html的方式,我们看到了为什么需要组件生命周期方法以及何时被调用。我们利用它来确保我们自己清理,以免在应用程序中创建内存泄漏。所有这些都为我们探索了Web组件工具链的功能和功能的功能。

在第2部分中,我们将探索Shadow dom和HTML模板。我们还将与React,Angular和Vue等巨头一起探索Web组件的位置。我们将尽可能客观地对待它,以了解您每种方法进行的真实,不可忽略的权衡。目的是为您提供工具和上下文,以便您可以正确解决问题。

也可以在我的GitHub上找到本文的所有源代码。您甚至可能会偷偷浏览一下我为第2部分

组合的演示和比较

如果您想到我错过的或只是想取得联系,可以通过Mastodon通过Threads通过TwitterLinkedIn与我联系。

参考

  1. MDN Web组件[Website]
  2. 服务CLI工具[NPM]
  3. 使用自定义元素[MDN]
  4. 渲染架构的概述[Website]