JS的工作方式:自定义元素


我们提请您注意SessionStack系列材料中19篇文章的翻译,这些文章涉及JavaScript生态系统的各种机制。 今天,我们将讨论“定制元素”标准-所谓的“定制元素”。 我们将讨论它们允许解决哪些任务,以及如何创建和使用它们。

图片


复习


在本系列的前几篇文章中,我们讨论了Shadow DOM和作为更大现象一部分的其他一些技术-Web组件。 Web组件旨在使开发人员能够通过创建紧凑,模块化和可重用的元素来扩展HTML的标准功能。 这是所有领先的浏览器制造商都已经注意到的相对较新的W3C标准。 当然,可以在生产中找到他,尽管他的作品是由多亲人士提供的(我们将在后面讨论)。

您可能已经知道,浏览器为我们提供了一些用于开发网站和Web应用程序的基本工具。 它是关于HTML,CSS和JavaScript的。 HTML用于构造网页,这要归功于CSS,它们使CSS看起来很漂亮,而JavaScript负责交互功能。 但是,在Web组件出现之前,将JavaScript实现的动作与HTML结构关联起来并不容易。

事实上,这里我们将考虑Web组件的基础-自定义元素。 简而言之,旨在与它们一起使用的API允许程序员使用CSS描述的内置JavaScript逻辑和样式来创建自己的HTML元素。 许多人将自定义元素与Shadow DOM技术混淆了。 但是,这是两个完全不同的事实,它们实际上是相辅相成的,但不能互换。

一些框架(例如Angular或React)试图通过引入其自身的概念来解决自定义元素所解决的相同问题。 可以将自定义元素与Angular指令或React组件进行比较。 但是,自定义元素是浏览器的标准功能;除了普通的JavaScript,HTML和CSS之外,您无需使用其他任何元素。 当然,这不允许我们说它们是普通JS框架的替代品。 现代框架为我们提供的不仅仅是模拟自定义元素行为的能力。 结果,我们可以说框架和用户元素都是可以一起用于解决Web开发任务的技术。

API


在继续之前,让我们看看API给我们带来了使用自定义元素的机会。 即,我们正在谈论具有以下方法的全局customElements对象:

  • define(tagName, constructor, options)方法define(tagName, constructor, options)使您可以定义(创建,注册)新的用户元素。 它带有三个参数-用户元素的标签名称(对应于此类元素的命名规则),类声明和带有参数的对象。 当前仅支持一个参数extends ,这是一个字符串,用于指定要扩展的内联元素的名称。 此功能用于创建标准元素的特殊版本。
  • 如果该元素已经定义,则get(tagName)方法返回user元素的构造函数,否则返回undefined 。 它带有一个参数-用户元素的名称标签。
  • whenDefined(tagName)方法返回创建用户元素后解析的promise。 如果已经定义了元素,则立即解决此承诺。 如果传递给它的标签名称不是用户元素的有效标签名称,则承诺将被拒绝。 此方法接受用户元素的标签名称。

创建自定义项目


创建自定义元素非常简单。 为此,必须完成两件事:为应扩展HTMLElement类的元素创建一个类声明,并将该元素注册为所选名称。 看起来是这样的:

 class MyCustomElement extends HTMLElement { constructor() {   super();   // … } // … } customElements.define('my-custom-element', MyCustomElement); 

如果您不想污染当前范围,则可以使用匿名类:

 customElements.define('my-custom-element', class extends HTMLElement { constructor() {   super();   // … } // … }); 

从示例中可以看到,用户元素是使用您已经熟悉的customElements.define(...)方法注册的。

自定义元素解决的问题


让我们谈谈允许我们解决自定义元素的问题。 其中之一是改善代码的结构,并消除所谓的“ div标签汤”(div汤)。 这种现象是现代Web应用程序中非常常见的代码结构,其中许多彼此嵌入的div元素。 可能是这样的:

 <div class="top-container"> <div class="middle-container">   <div class="inside-container">     <div class="inside-inside-container">       <div class="are-we-really-doing-this">         <div class="mariana-trench">           …         </div>       </div>     </div>   </div> </div> </div> 

使用此类HTML代码出于正当理由-描述页面的布局并确保其在屏幕上的正确显示。 但是,这损害了HTML代码的可读性并使它的维护复杂化。

假设我们有一个如下图所示的组件。


组件外观

使用传统方法来描述此类事物,以下代码将与此组件相对应:

 <div class="primary-toolbar toolbar"> <div class="toolbar">   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-undo"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-redo"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-print"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-toggle-button toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-paint-format"> </div>         </div>       </div>     </div>   </div> </div> </div> 

现在想象一下,我们可以使用以下组件描述代替代码:

 <primary-toolbar> <toolbar-group>   <toolbar-button class="icon-undo"></toolbar-button>   <toolbar-button class="icon-redo"></toolbar-button>   <toolbar-button class="icon-print"></toolbar-button>   <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button> </toolbar-group> </primary-toolbar> 

我相信每个人都会同意第二个代码片段看起来要好得多。 这样的代码更易于阅读,易于维护,并且开发人员和浏览器都可以理解。 总而言之,事实是它比嵌套的div标签很多的简单。

使用自定义元素可以解决的下一个问题是代码重用。 开发人员编写的代码不仅应该有效,而且应该受支持。 与不断编写相同的结构相反,重用代码可以提高项目支持能力。
这是一个简单的示例,可以帮助您更好地理解这个想法。 假设我们具有以下元素:

 <div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div> 

如果您经常需要它,那么采用通常的方法,我们将不得不一次又一次地编写相同的HTML代码。 现在想象一下,您需要对此代码进行更改,无论使用它在哪里,都应该反映出来。 这意味着我们需要找到使用此片段的所有位置,然后在各处进行相同的更改。 它漫长,艰辛且充满错误。

如果我们可以在需要此元素的地方放好,只需编写以下内容:

 <my-custom-element></my-custom-element> 

但是,现代Web应用程序不仅仅是静态HTML。 他们是互动的。 它们互动的源头是JavaScript。 通常,为了提供这种功能,将创建一些元素,然后将事件侦听器连接到它们,这使它们可以响应用户的影响。 例如,它们可以响应单击,鼠标指针在其上方的“悬停”,在屏幕上拖动它们等等。 以下是将事件侦听器连接到用鼠标单击时发生的元素的方法:

 var myDiv = document.querySelector('.my-custom-element'); myDiv.addEventListener('click', _ => { myDiv.innerHTML = '<b> I have been clicked </b>'; }); 

这是此元素的HTML代码:

 <div class="my-custom-element"> I have not been clicked yet. </div> 

通过使用API​​处理自定义元素,所有这些逻辑都可以包含在元素本身中。 为了进行比较-以下是用于声明包含事件处理程序的自定义元素的代码:

 class MyCustomElement extends HTMLElement { constructor() {   super();   var self = this;   self.addEventListener('click', _ => {     self.innerHTML = '<b> I have been clicked </b>';   }); } } customElements.define('my-custom-element', MyCustomElement); 

这是页面HTML代码中的外观:

 <my-custom-element> I have not been clicked yet </my-custom-element> 

乍一看,创建自定义元素似乎需要更多的JS代码行。 但是,在实际应用中,很少会创建此类元素仅使用一次。 现代Web应用程序中的另一个典型现象是,其中的大多数元素都是动态创建的。 这导致需要支持两种不同的使用元素的场景-使用JavaScript将元素动态添加到页面中时的情况,以及在页面的原始HTML结构中描述时的情况。 由于使用了自定义元素,因此简化了这两种情况下的工作。

结果,如果我们总结本节的结果,可以说用户元素使代码更清晰,简化了对代码的支持,有助于将代码分解为小模块,这些模块包括所有必要的功能并且适合重用。

现在,我们已经讨论了使用自定义元素的一般问题,让我们谈谈它们的功能。

要求条件


在开始开发自己的定制元素之前,您应该了解创建它们时必须遵循的一些规则。 它们是:

  • 组件名称必须包含连字符( -符号)。 因此,HTML解析器可以区分嵌入式元素和用户元素。 此外,这种方法可确保名称与内置元素(既与现在的元素又与将来的元素)不冲突。 例如,自定义元素的实际名称是>my-custom-element< ,而名称>myCustomElement<<my_custom_element>不合适。
  • 禁止多次注册同一标签。 尝试执行此操作将导致浏览器DOMException错误。 自定义元素无法重新定义。
  • 自定义标签不能自动关闭。 HTML解析器仅支持有限的一组标准自关闭标签(例如<img><link><br> )。

可能性


让我们谈谈如何使用自定义元素。 简而言之,如果您回答这个问题,那么您可以使用它们做很多有趣的事情。

自定义元素最显着的特征之一是元素类的声明是指DOM元素本身。 这意味着您可以在广告中使用this关键字来连接事件侦听器,访问属性,子节点等。

 class MyCustomElement extends HTMLElement { // ... constructor() {   super();   this.addEventListener('mouseover', _ => {     console.log('I have been hovered');   }); } // ... } 

当然,这使得将新数据写入元素的子节点成为可能。 但是,不建议这样做,因为这可能导致元素的意外行为。 如果您以为自己使用的是其他人设计的元素,那么如果您自己放置在元素中的标记被其他元素替换,您可能会感到惊讶。

有几种方法可让您在元素生命周期的某些时间执行代码。

  • 在创建或“升级”元素时, constructor方法将被调用一次(我们将在下面讨论)。 通常,它用于初始化元素的状态,连接事件侦听器,创建Shadow DOM等。 不要忘记,您始终需要在构造函数中调用super()
  • 每次将元素添加到DOM时,都会调用connectedCallback方法。 可以使用它(这正是建议使用的方式),以便将任何操作的执行推迟到元素出现在页面上之前(例如,通过这种方式,您可以延迟某些数据的加载)。
  • 从DOM中删除项目时,将调用disconnectedCallback方法。 通常用于释放资源。 请注意,如果用户关闭带有页面的浏览器选项卡,则不会调用此方法。 因此,在必要时不要依赖他来执行一些特别重要的动作。
  • 添加,删除,更新或替换元素attributeChangedCallback时,调用attributeChangedCallback方法。 另外,在解析器创建元素时调用它。 但是,请注意,此方法仅适用于observedAttributes属性中列出的属性。
  • 当使用document.adoptNode(...)方法时, adoptedCallback调用adoptedCallback方法,该方法用于将节点移至另一个文档。

请注意,以上所有方法都是同步的。 例如,将元素添加到DOM后立即调用connectedCallback方法,程序的其余部分等待此方法的完成。

财产反思


嵌入式HTML元素具有一个非常方便的功能:属性反射。 由于这种机制,某些属性的值直接作为属性反映在DOM中。 假设这是id属性的特征。 例如,我们执行以下操作:

 myDiv.id = 'new-id'; 

相关更改将影响DOM:

 <div id="new-id"> ... </div> 

该机构以相反的方向操作。 这非常有用,因为它允许您声明性地配置元素。

自定义元素没有此内置功能,但您可以自己实现。 为了使用户元素的某些属性具有相似的行为,可以配置其getter和setter。

 class MyCustomElement extends HTMLElement { // ... get myProperty() {   return this.hasAttribute('my-property'); } set myProperty(newValue) {   if (newValue) {     this.setAttribute('my-property', newValue);   } else {     this.removeAttribute('my-property');   } } // ... } 

扩展现有项目


使用自定义元素API,您不仅可以创建新的HTML元素,还可以扩展现有元素。 而且,我们在谈论标准元素和自定义元素。 这是通过在声明类时使用extends来完成的:

 class MyAwesomeButton extends MyButton { // ... } customElements.define('my-awesome-button', MyAwesomeButton);</cosourcede>      ,  , ,    <code>customElements.define(...)</code>,    <code>extends</code>   ,      .     ,        ,        DOM-.   ,          ,      ,       . <source>class MyButton extends HTMLButtonElement { // ... } customElements.define('my-button', MyButton, {extends: 'button'}); 

扩展的标准元素也称为“定制的内置元素”。

建议将始终扩展现有元素并逐步进行作为规则。 这将允许您在新元素中保存先前创建的元素(即属性,属性,函数)中实现的功能。

请注意,现在仅Chrome 67+支持自定义内置元素。 这将出现在其他浏览器中,但是,众所周知,Safari开发人员决定不实施此机会。

更新项目


如前所述, customElements.define(...)方法用于注册自定义元素。 但是,注册不能称为必须首先执行的操作。 用户元素注册可以推迟一段时间,而且,即使元素已经添加到DOM中,也可能会推迟。 此过程称为升级。 为了确定何时注册项目,浏览器提供了customElements.whenDefined(...)方法。 给他指定了元素标记的名称,并且他返回了在元素注册后解析的promise。

 customElements.whenDefined('my-custom-element').then(_ => { console.log('My custom element is defined'); }); 

例如,您可能需要延迟元素的注册,直到声明其子元素为止。 如果项目具有嵌套的用户元素,则这种行为方式可能非常有用。 有时,父级可以依靠子级元素的实现。 在这种情况下,您需要确保孩子在父母之前注册。

影子dom


如前所述,自定义元素和Shadow DOM是互补技术。 第一个允许您将JS逻辑封装在用户元素中,第二个允许您为不受外部影响的DOM片段创建隔离的环境。 如果您觉得需要更好地理解Shadow DOM概念,请阅读我们以前的出版物之一

以下是将Shadow DOM用于自定义元素的方法:

 class MyCustomElement extends HTMLElement { // ... constructor() {   super();   let shadowRoot = this.attachShadow({mode: 'open'});   let elementContent = document.createElement('div');   shadowRoot.appendChild(elementContent); } // ... }); 

如您所见,调用this.attachShadowthis.attachShadow起着关键作用。

模式


在我们以前的一篇文章中,我们讨论了一些模板,尽管实际上它们值得单独撰写。 在这里,我们将看一个简单的示例,说明如何在创建模板时将模板嵌入自定义元素中。 因此,使用<template> ,您可以描述解析器将处理但不会在页面上显示的DOM片段:

 <template id="my-custom-element-template"> <div class="my-custom-element">   <input type="text" class="email" />   <button class="submit"></button> </div> </template> 

这是在自定义元素中应用模板的方法:

 let myCustomElementTemplate = document.querySelector('#my-custom-element-template'); class MyCustomElement extends HTMLElement { // ... constructor() {   super();   let shadowRoot = this.attachShadow({mode: 'open'});   shadowRoot.appendChild(myCustomElementTemplate.content.cloneNode(true)); } // ... }); 

如您所见,自定义元素,Shadow DOM和模板结合在一起。 这使我们能够创建一个隔离在其自身空间中的元素,其中HTML结构与JS逻辑分离。

程式化


到目前为止,我们仅谈论JavaScript和HTML,而忽略CSS。 因此,我们现在谈谈样式主题。 显然,我们需要一些样式来定制元素。 可以在Shadow DOM内添加样式,但是随后出现的问题是,例如,如何从外部对这些元素进行样式设置-如果创建者没有使用它们。 这个问题的答案非常简单-自定义元素的样式与内置元素相同。

 my-custom-element { border-radius: 5px; width: 30%; height: 50%; // ... } 

请注意,外部样式优先于在元素内部声明的样式,并覆盖它们。

您可能已经了解了如何在屏幕上显示页面时在某个点上观察到非风格化的内容(这就是所谓的FOUC-未样式化内容的闪烁)。 您可以通过设置未注册组件的样式并在注册它们时使用一些视觉效果来避免这种现象。 为此,可以使用选择器:defined 。 例如,您可以这样做:

 my-button:not(:defined) { height: 20px; width: 50px; opacity: 0; } 

未知元素和未定义用户元素


HTML规范非常灵活,它允许您声明开发人员所需的任何标签。 而且,如果浏览器无法识别该标签,则解析器会将其作为HTMLUnknownElement

 var element = document.createElement('thisElementIsUnknown'); if (element instanceof HTMLUnknownElement) { console.log('The selected element is unknown'); } 

但是,在使用自定义元素时,这种方案不适用。 , ? , , HTMLElement .

 var element = document.createElement('this-element-is-undefined'); if (element instanceof HTMLElement) { console.log('The selected element is undefined but not unknown'); } 

HTMLElement HTMLUnknownElement , , , , - . , , , . div . .


Chrome 36+. API Custom Components v0, , , , . API, , — . API Custom Elements v1 Chrome 54+ Safari 10.1+ ( ). Mozilla v50, , . , Microsoft Edge API. , , webkit. , , , — IE 11.


, , , customElements
window :

 const supportsCustomElements = 'customElements' in window; if (supportsCustomElements) { // API Custom Elements   } 

:

 function loadScript(src) { return new Promise(function(resolve, reject) {   const script = document.createElement('script');   script.src = src;   script.onload = resolve;   script.onerror = reject;   document.head.appendChild(script); }); } //    -    . if (supportsCustomElements) { //    ,    . } else { loadScript('path/to/custom-elements.min.js').then(_ => {   //   ,     . }); } 

总结


, :

  • HTML- JavaScript-, , CSS-.
  • HTML- ( , ).
  • . , — JavaScript, HTML, CSS, , , .
  • - (Shadow DOM, , , ).
  • , .
  • , .

, Custom Elements v1 , , , , , .

亲爱的读者们! ?

Source: https://habr.com/ru/post/zh-CN419831/


All Articles