Web组件:入门指南



了解使用Web组件的好处,它们如何工作以及如何开始



使用Web组件(以下称为组件),开发人员可以创建自己的HTML元素。在本指南中,您将学习有关组件的所有知识。我们将从组件的组成,组件的优点以及组件组成开始。



之后,我们将开始构建组件,首先使用HTML模板和影子DOM接口,然后再深入探讨该主题,并了解如何创建自定义的内置元素。



什么是成分?



开发人员喜欢组件(这里指的是“模块”设计模式的实现)。这是定义可随时随地使用的代码块的好方法。多年来,已经进行了或多或少的成功尝试,以将这一想法付诸实践。



Mozilla的XML绑定语言和Microsoft的Internet Explorer 5 HTML组件规范大约在20年前。不幸的是,这两种实现都非常复杂,并且未能引起其他浏览器制造商的兴趣,因此很快被人们遗忘。尽管如此,还是他们为我们今天在这一领域的工作奠定了基础。



像React,Vue和Angular这样的JavaScript框架也采用了类似的方法。其成功的主要原因之一是能够将应用程序的一般逻辑封装到一些模板中,这些模板可以很容易地从一种形式转换为另一种形式。



尽管这些框架改善了开发经验,但一切都是有代价的。需要编译诸如JSX之类的语言功能,并且大多数框架使用JavaScript引擎来管理其抽象。是否有另一种方法可以解决将代码分成组件的问题?答案是网络组件。



组件的4个支柱



组件由三个API组成-自定义元素,HTML模板和shadow DOM,以及它们的基础JavaScript模块(ES6模块)。使用这些接口提供的工具,您可以创建行为类似于其本机对应元素的自定义HTML元素。



组件的使用方式与常规HTML元素相同。可以使用属性来自定义它们,使用JavaScript检索,使用CSS设置样式。最主要的是通知浏览器它们存在。



这允许组件与其他框架和库进行交互。通过使用与常规元素相同的通信机制,它们可以被任何现有框架以及将来出现的工具所使用。



还应注意,这些组件符合网络标准。网络基于向后兼容的思想。这意味着今天创建的组件将长期有效。



让我们分别看一下每个规范。







1.自定义元素



主要特征:



  • 定义元素行为
  • 对属性更改做出反应
  • 扩展现有元素


人们在谈论组件时,通常是指自定义元素的界面。



通过此API,您可以通过定义元素在添加,更新和删除时的行为来扩展元素。



class ExampleElement extends HTMLElement {
  static get observedAttributes() {
      return [...]
  }
  attributeChangedCallback(name, oldValue, newValue) {}
  connectedCallback() {}
}
customElements.define('example-element', ExampleElement)


每个自定义元素都具有相似的结构。它扩展了现有HTMLElements类的功能。



在自定义元素内,有几种称为反应的方法,负责处理元素的特定更改。例如,将项目添加到页面时,将调用connectedCallback。这类似于框架中使用的生命周期阶段(React中的componentDidMount,安装在Vue中)。



更改元素的属性需要更改其行为。发生更新时,将调用attributeChangedCallback,其中包含有关更改的信息。这仅发生在observedAttributes返回的数组中指定的属性上。



必须先定义元素,然后浏览器才能使用它。“定义”方法采用两个参数-标记的名称及其类。所有标签都必须包含“-”字符,以避免与现有和将来的本机元素冲突。



<example-element>Content</example-element>


该元素可以像普通的HTML标签一样使用。找到此类元素后,浏览器会将其行为与指定的类相关联。此过程称为“升级”。



有两种类型的项目-“自主”和“自定义内置”。到目前为止,我们已经研究了独立项目。这些是与现有HTML元素无关的元素。类似于div和span标签,它们没有特定的语义。



自定义内联元素-顾名思义-扩展了现有HTML元素的功能。它们继承了这些元素的语义行为,并且可以对其进行更改。例如,如果已对“输入”元素进行了自定义,则在提交时,它仍将保持输入字段和表单的一部分。



class CustomInput extends HTMLInputElement {}
customElements.define('custom-input', CustomInput, { extends: 'input' })


自定义内联元素类扩展了自定义元素类。定义内联元素时,可扩展元素将作为第三个参数传递。



<input is="custom-input" />


标签的使用也略有不同。代替新标签,使用现有标签,指定特殊的“ is”扩展属性。当浏览器遇到此属性时,它知道它正在处理自定义元素并相应地对其进行更新。



大多数现代浏览器都支持独立元素,而自定义内联元素仅受Chrome和Firefox支持。在不支持它们的浏览器中使用时,后者将被视为普通的HTML元素,因此,即使在那些浏览器中,它们也可以安全使用。



2. HTML模板



  • 创建现成的结构
  • 通话前未显示在页面上
  • 包含HTML,CSS和JS


从历史上看,客户端模板涉及JavaScript中的字符串连接或使用诸如Handlebars之类的库来解析自定义标记的块。最近,该规范具有一个“模板”标记,可以包含我们要使用的任何内容。



<template id="tweet">
  <div class="tweet">
    <span class="message"></span>
      Written by @
    <span class="username"></span>
  </div>
</template>


就其本身而言,它不会以任何方式影响页面,即 引擎无法解析它,因此不会发送对资源(音频,视频)的请求。JavaScript无法访问它,对于浏览器来说,它是一个空元素。



const template = document.getElementById('tweet')
const node = document.importNode(template.content, true)
document.body.append(node)


首先我们得到“模板”元素。importNode方法创建其内容的副本,第二个参数(true)表示深层副本。最后,我们将其添加到页面中,就像其他任何元素一样。



模板可以包含普通HTML可以包含的任何内容,包括CSS和JavaScript。将元素添加到页面后,将对其应用样式并启动脚本。请记住,样式和脚本是全局的,这意味着它们可能会覆盖脚本使用的其他样式和值。



模板不限于此。当与组件的其他部分(尤其是影子DOM)一起使用时,它们以其所有的荣耀出现。



3. Shadow DOM



  • 避免样式冲突
  • 输入(例如,类的)名称变得容易
  • 封装实现逻辑


文档对象模型(DOM)是浏览器解释页面结构的方式。通过阅读标记,浏览器确定哪些元素包含哪些内容,并据此决定应在页面上显示的内容。例如,当使用document.getElemetById()时,浏览器将访问DOM以查找其所需的元素。



对于页面布局,这很好,但是隐藏在元素内部的详细信息又如何呢?例如,页面不必关心“视频”元素中包含的界面。这是影子DOM派上用场的地方。



<div id="shadow-root"></div>
<script>
  const host = document.getElementById('shadow-root')
  const shadow = host.attachShadow({ mode: 'open' })
</script>


阴影DOM在应用于元素时创建。任何内容都可以添加到影子DOM中,就像常规(“轻”)DOM一样。影子DOM不受外部情况的影响,即 在它外面。纯DOM也无法直接访问影子。这意味着在影子DOM中,我们可以使用任何类名,样式和脚本,而不必担心可能的冲突。



结合使用影子DOM和自定义元素可获得最佳结果。多亏了影子DOM,当重用组件时,其样式和结构不会影响页面上的其他元素。



ES和HTML模块


  • 根据需要添加
  • 无需预生成
  • 一切都存储在一个地方


尽管前三个规范在其开发过程中已经走了很长一段路,但是如何打包和再使用它们仍然是激烈争论的主题。



HTML导入规范定义了如何导出和导入HTML文档以及CSS和JavaScript。这将允许自定义元素以及模板和影子DOM放置在其他位置并根据需要使用。



但是,Firefox拒绝在其浏览器中实现此规范,并基于JavaScript模块提供了另一种方式。



export class ExampleElement external HTMLElement {}

import { ExampleElement } from 'ExampleElement.js'


模块默认具有自己的名称空间,即 他们的内容不是全球性的。导出的变量,函数和类可以随时随地导入并用作本地资源。



这对于组件非常有用。包含模板和阴影DOM的自定义元素可以从一个文件导出,而在另一个文件中使用。



import { ExampleElement } from 'ExampleElement.html'


Microsoft已提出一项提案,以通过HTML导出/导入来扩展JavaScript模块规范。这将允许您使用声明性和语义HTML创建组件。Chrome和Edge即将推出此功能。



创建自己的组件



尽管您可能会发现许多复杂的组件,但是创建和使用简单的组件只需要花费几行代码。让我们考虑一些例子。





组件使您可以使用HTML模板和阴影DOM接口显示用户注释。



让我们创建一个组件,以使用HTML模板和影子DOM显示用户评论。



1.创建模板


组件在生成标记之前需要复制一个模板。模板可以在页面上的任何位置,定制元素的类可以通过ID访问它。



将“模板”元素添加到页面。在此元素上定义的任何样式都只会影响它。



<template id="user-comment-template">
  <style>
      ...
  </style>
</template>


2.添加标记


除样式外,组件还可以包含布局(结构)。为此,使用“ div”元素。



动态内容通过插槽传递。使用适当的“名称”属性为头像,名称和用户消息添加插槽:



<div class="container">
  <div class="avatar-container">
    <slot name="avatar"></slot>
  </div>
  <div class="comment">
    <slot name="username"></slot>
    <slot name="comment"></slot>
  </div>
</div>


默认广告位内容




当没有信息传递到插槽时,将显示默认内容,传递到插槽的



数据将覆盖模板中的数据。如果没有信息传递到插槽,则显示默认内容。



在这种情况下,如果未传输用户名,则会在其位置显示消息“ No name”:



<slot name="username">
  <span class="unknown">No name</span>
</slot>


3.创建一个班级


创建自定义元素首先要扩展“ HTMLElement”类。设置过程的一部分是创建阴影根,以渲染元素的内容。我们在下一个阶段将其打开以供访问。



最后,我们将新的UserComment类通知浏览器。



class UserComment extends HTMLElement {
  constructor() {
      super()
      this.attachShadow({ mode: 'open' })
  }
}
customElements.define('user-comment', UserComment)


4.应用阴影内容


当浏览器遇到“ user-comment”元素时,它将查看影子根节点以检索其内容。第二个参数告诉浏览器复制所有内容,而不仅仅是第一层(顶层元素)。



将标记添加到影子根节点,该节点将立即更新组件的外观。



connectedCallback() {
  const template = document.getElementById('user-comment-template')
  const node = document.importNode(template.content, true)
  this.shadowRoot.append(node)
}


5.使用组件


该组件现在可以使用了。添加“用户评论”标签,并将必要的信息传递给它。



由于所有插槽都有名称,因此传递到它们之外的所有内容都将被忽略。插槽内的所有内容均按传递的原样复制,包括样式。



<user-comment>
  <img alt="" slot="avatar" src="avatar.png" />
  <span slot="username">Matt Crouch</span>
  <div slot="comment">This is an example of a comment</div>
</user-comment>


扩展示例代码:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Components Example</title>
    <style>
      body {
        display: grid;
        place-items: center;
      }
      img {
        width: 80px;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <template id="user-comment-template">
      <div class="container">
        <div class="avatar-container">
          <slot name="avatar">
            <slot class="unknown"></slot>
          </slot>
        </div>
        <div class="comment">
          <slot name="username">No name</slot>
          <slot name="comment"></slot>
        </div>
      </div>
      <style>
        .container {
          width: 320px;
          clear: both;
          margin-bottom: 1rem;
        }
        .avatar-container {
          float: left;
          margin-right: 1rem;
        }
        .comment {
          height: 80px;
          display: flex;
          flex-direction: column;
          justify-content: center;
        }
        .unknown {
          display: block;
          width: 80px;
          height: 80px;
          border-radius: 4px;
          background: #ccc;
        }
      </style>
    </template>

    <user-comment>
      <img alt="" slot="avatar" src="avatar1.jpg" />
      <span slot="username">Matt Crouch</span>
      <div slot="comment">Fisrt comment</div>
    </user-comment>
    <user-comment>
      <img alt="" slot="avatar" src="avatar2.jpg" />
      <!-- no username -->
      <div slot="comment">Second comment</div>
    </user-comment>
    <user-comment>
      <!-- no avatar -->
      <span slot="username">John Smith</span>
      <div slot="comment">Second comment</div>
    </user-comment>

    <script>
      class UserComment extends HTMLElement {
        constructor() {
          super();
          this.attachShadow({ mode: "open" });
        }
        connectedCallback() {
          const template = document.getElementById("user-comment-template");
          const node = document.importNode(template.content, true);
          this.shadowRoot.append(node);
        }
      }
      customElements.define("user-comment", UserComment);
    </script>
  </body>
</html>










创建自定义内联元素



如前所述,自定义元素可以扩展现有元素。通过保持育雏器提供的元素的默认行为,可以节省时间。在本节中,我们将研究如何扩展“时间”元素。



1.创建一个类


扩展类时,将显示独立元素之类的内置元素,但它们扩展了特定的类,而不是通用类“ HTMLElement”。



在我们的例子中,此类是HTMLTimeElement-“时间”元素使用的类。它包括与“ datetime”属性有关的行为,包括数据格式。



class RelativeTime extends HTMLTimeElement {}


2.元素定义


该元素由浏览器使用“定义”方法注册。但是,与独立元素不同,注册内联元素时,必须向“定义”方法传递第三个参数-具有设置的对象。



我们的对象将包含一个具有custom元素值的键。它采用标签的名称。在没有这样的密钥的情况下,将引发异常。



customElements.define('relative-time', RelativeTime, { extends: 'time' })


3.设定时间


由于页面上可以有多个组件,因此组件必须提供一种用于设置元素值的方法。在此方法内部,组件将时间值传递到“ timeago”,并将该库返回的值设置为项目值(对不起重言式)。



最后,我们设置“ title”属性,以便用户可以看到悬停时设置的值。



setTime() {
  this.innerHTML = timeago().format(this.getAttribute('datetime'))
  this.setAttribute('title', this.getAttribute('datetime'))
}


4.连接更新


组件在页面上显示后可以立即使用该方法。由于内联组件没有影子DOM,因此它们不需要构造函数。



connectedCAllback() {
  this.setTime()
}


5.跟踪属性的更改


如果您以编程方式更新时间,则组件将不会响应。他不知道自己必须注意“ datetime”属性中的更改。



定义观察到的属性后,只要更改属性,就会调用attributeChangedCallback。



static get observedAttributes() {
  return ['datetime']
}
attributeChangedCallback() {
  this.setTime()
}


6.添加到页面


由于我们的元素是本机元素的扩展,因此其实现略有不同。要使用它,请将标签“ time”添加到具有特殊属性“ is”的页面上,该属性的值是注册期间定义的内置元素的名称。不支持组件的浏览器将呈现后备内容。



<time is="relative-time" datetime="2020-09-20T12:00:00+0000">
  20  2020 . 12:00
</time>


扩展示例代码:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Components Another Example</title>
    <!-- timeago.js -->
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
      integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
      crossorigin="anonymous"
    ></script>
    <style>
      body {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      input,
      button {
        margin-bottom: 0.5rem;
      }
      time {
        font-size: 2rem;
      }
    </style>
  </head>
  <body>
    <input type="text" placeholder="2020-10-20" value="2020-08-19" />
    <button>Set Time</button>

    <time is="relative-time" datetime="2020-09-19">
      19  2020 .
    </time>

    <script>
      class RelativeTime extends HTMLTimeElement {
        setTime() {
          this.innerHTML = timeago.format(this.getAttribute("datetime"));
          this.setAttribute("title", this.getAttribute("datetime"));
        }
        connectedCallback() {
          this.setTime();
        }
        static get observedAttributes() {
          return ["datetime"];
        }
        attributeChangedCallback() {
          this.setTime();
        }
      }
      customElements.define("relative-time", RelativeTime, { extends: "time" });

      const button = document.querySelector("button");
      const input = document.querySelector("input");
      const time = document.querySelector("time");

      button.onclick = () => {
        const { value } = input;
        time.setAttribute("datetime", value);
      };
    </script>
  </body>
</html>










希望我能帮助您对Web组件的用途,用途以及使用方式有一个基本的了解。



All Articles