自定义元素探秘及构建可复用组件最佳实践

原文请查阅 这里,略有删减,本文采用 知识共享署名 4.0 国际许可协议共享,BY Troland

这是 JavaScript 工做原理第十九章。javascript

概述

前述文章中,咱们介绍了 Shadow DOM 接口和一些其它概念,而这些都是网页组件的组成部分。网页组件背后的思想即经过建立颗粒化,模块化和可复用的元素来扩展 HTML 内置功能。这是一个已经被全部主流浏览器兼容的相对崭新的 W3C 标准且能够被用在生产环境之中,虽然不兼容的浏览器须要使用垫片库(将在随后的章节中进行讨论)。html

正如开发者所知,浏览器为构建网站和网页程序提供了一些重要的开发工具。咱们所说的 HTML,CSS 和 JavaScript 即开发者使用 HTML 来构建结构,CSS 进行样式化而后使用 JavaScript 来让页面动起来。然而,在网页组件出现以前,把 JavaScript 脚本和 HTML 结构组合起来并不是易事。html5

本文将阐述网页组件的基石-自定义元素。总之,开发者可使用自定义元素接口来建立包含 JavaScript 逻辑和样式的自定义元素(正如名称的字面意思)。许多开发者会把自定义元素和 shadow DOM 混为一谈。可是,他们是彻底不一样的概念且它们互补而不是能够相互替代的。java

一些框架(好比 Angular,React) 试图经过引进其自有概念来解决一样的问题。开发者能够把自定义元素和 Angular 的指令或者 React 组件进行对比。然而,自定义元素是浏览器原生的且只须要原生 JavaScript,HTML 和 CSS。固然了,这并不意味着它能够取代一个典型的 JavaScript 框架。现代框架不只仅为开发者提供模仿自定义元素行为的能力。所以,能够同时使用框架和自定义元素。git

接口

在深刻了解以前,让咱们先大概快速浏览一下接口的内容。全局 customElements 对象为开发者提供了一些方法:github

  • define(tagName, constructor, options) -建立一个新的自定义元素。

    包含三个参数:自定义元素的可用标签名称,自定义元素类定义及选项参数对象。目前仅支持一个选项参数:extends 指定想要扩展的 HTML 内置元素名称的字符串。用来建立定制化内置元素。web

  • get(tagName) -若元素已经定义则返回自定义元素的构造函数不然返回 undefined。只有一个参数:自定义元素的可用标签名称。
  • whenDefined(tagName)-返回一个 promise 对象,当定义自定义元素即解析。若元素已定义则当即进行解析。若自定义元素标签名称不可用则摒弃 promise。只有一个参数:自定义元素的可用标签名称。

如何建立自定义元素

建立自定义元素实际上就是小菜一碟。开发者只须要作两件事:建立扩展 HTMLElement 类元素的类定义,而后以合适的名称注册元素。promise

class MyCustomElement extends HTMLElement {
  constructor() {
    super();
    // …
  }

  // …
}

customElements.define('my-custom-element', MyCustomElement);

或者如你所愿,可使用匿名类以防止弄乱当前做用域浏览器

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

  // …
});

从以上例子可见,使用 customElements.define(...) 方法注册自定义元素。session

自定义元素所解决的问题

实际上,问题是啥?嵌套 DIV 是问题之一。嵌套 Div 是啥?在现代网页程序中这是一个很是常见的现象,开发者会使用多个嵌套块状元素(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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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 class="my-custom-element">
  <input type="text" class="email" />
  <button class="submit"></button>
</div>

若须要在其它地方使用这段代码,开发者须要再次书写相同的 HTML 结构。如今,想象 一下须要稍微修改一下这些元素。开发者须要找出每一个代码须要修改的地方,而后一遍遍地作出一样的修改。太恶心了。。。

若使用以下码岂不会更好?

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

现代网页程序不只仅只有静态 HTML。开发者须要作交互。这就须要 JavaScript。通常来讲,开发者须要作的即建立一些元素而后在上面监听事件以响应用户输入。点击,拖拽或者悬浮事件等等。

var myDiv = document.querySelector('.my-custom-element');

myDiv.addEventListener('click', () => {
  myDiv.innerHTML = '<b> I have been clicked </b>';
});
<div class="my-custom-element">
  I have not been clicked yet.
</div>

使用自定义元素接口能够把全部的逻辑封装进元素自身。如下代码能够实现和上面代码同样的功能:

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);
<my-custom-element>
  I have not been clicked yet
</my-custom-element>

咋一看上去,自定义元素技术须要书写更多的 JavaScript 代码。可是在实际程序中,建立不需复用的单一组件的状况是不多见的。一个典型的现代网页程序的重要特征即大多数元素都是动态建立的。那么,开发者就须要分别处理使用 JavaScript 动态添加元素或者使用 HTML 结构中预约义内容。那么可使用自定义元素来实现这些功能。

总之,自定义元素让开发者的代码更易理解和维护,并分割为小型,可复用及可封装的模块。

要求

在建立自定义元素以前,开发者须要遵照以下特殊规则:

  • 名称必须包含一个破折号 - 。这样 HTML 解析器就能够把自定义元素和内置元素区分开来。这样能够保证不会和内置元素出现命名冲突的问题(不论是如今或者未来当添加其它元素的时候)。好比,<my-custom-element> 是正确的而 myCustomElement<my_custom_element> 则否则。
  • 不容许重复注册标签名称。重复注册标签名称会致使浏览器抛出 DOMException 错误。不能够覆盖已注册自定义元素。
  • 自定义元素不能够自关闭。HTML 解析器只容许一小撮内置元素能够自关闭(好比 <img><link><br>)。

功能

那么究竟自定义元素能够实现哪些功能?答案是不少。

最好用的功能之一即元素的类定义能够引用 DOM 元素自身。这意味着开发者能够直接使用 this 来直接监听事件,访问 DOM 属性,访问 DOM 元素子节点等等。

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    this.addEventListener('mouseover', () => {
      console.log('I have been hovered');
    });
  }

  // ...
}

固然,这样开发者就可使用新内容来覆盖元素的子节点。但通常不推荐这样作,由于这可能会致使意外的行为。做为自定义元素的使用者,由于不是使用者开发的,当元素里面的标记被其它内容所取代,用户会以为很奇怪。

在元素生命周期的特定阶段,开发者能够在一些生命周期钩子中执行代码。

constructor

每当建立或者更新元素会触发构造函数(随后再详细讲解下)。通常状况会在该阶段初始化状态,监听事件,建立 shadow DOM 等等。须要记住的是必须老是在构造函数中调用 super()

connectedCallback

每当在 DOM 中添加元素的时候会调用 connectedCallback 方法。能够用来(推荐)延迟执行某些代码直到元素彻底渲染于页面上时候调用(好比获取一个资源)。

disconnectedCallback

connectedCallback 相反,当元素被从 DOM 删除时调用 disconnectedCallback 方法。通常用于释放资源的时候调用。须要注意的是若用户关闭选项卡不会调用 disconnectedCallback 方法。所以,首先开发者须要注意初始化代码。

attributeChangedCallback

每当添加,删除,更新或者替换元素的某一属性的时候调用。当解析器建立的时候也会调用。可是,请注意只有在 observedAttributes 属性白名单中的属性才会触发。

addoptedCallback

当使用 document.adoptNode(...) 来把元素移动到另外一个文档的时候会触发 addoptedCallback方法。

请注意以上全部的回调都是同步。例如,当把元素添加进 DOM 的时候只会触发链接回调。

属性反射

内置 HTML 元素提供了一个很是方便的功能:属性反射。这意味着直接修改某些属性值会直接反射到 DOM 的属性中。例如 id 属性:

myDiv.id = 'new-id';

将会更新 DOM 为

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

反之亦然。这是很是有用的由于这样就使得开发者能够声明式书写元素。

自定义元素自身没有该功能,可是有办法能够实现。为了在自定义元素中实现该相同的功能,开发者须要定义属性的 getters 和 setters 方法。

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');
    }
  }

  // ...
}

扩展元素

开发者不只仅可使用自定义元素接口建立新的 HTML 元素还能够用来扩展示有的 HTML 元素。并且该接口在内置元素和其它自定义元素中工做得很好。仅仅只须要扩展元素的类定义便可。

class MyAwesomeButton extends MyButton {
  // ...
}

customElements.define('my-awesome-button', MyAwesomeButton);

或者当扩展内置元素时,开发者须要为 customElements.define(...) 函数添加第三个 extends 的参数,参数值为须要扩展的元素标签名称。因为许多内置元素共享相同的 DOM 接口,extends 参数会告诉浏览器须要扩展的目标元素。若没有指定须要扩展的元素,浏览器将不会知道须要扩展的功能类别 。

class MyButton extends HTMLButtonElement {
  // ...
}

customElements.define('my-button', MyButton, {extends: 'button'});

一个可扩展原生元素也被称为可定制化内置元素。

开发者须要记住的经验法则即老是扩展存在的 HTML 元素。而后,一点点往里添加功能。这样就能够保留元素以前的功能(属性,函数)。

请注意如今只有 Chrome 67+ 才支持定制化内置元素。之后,其它浏览器也会实现,可是 Safari 彻底没有实现该功能。

更新元素

如上所述,可使用 customElements.define(...) 方法注册自定义元素。但这并不意味着,开发者必须首先注册元素。能够推迟在以后某个时间注册自定义元素。甚至能够在往 DOM 中添加元素后再注册元素也是能够的。这一过程称为更新元素。开发者可使用 customElements.whenDefined(...) 方法获取元素的定义时间。开发者传入元素标签名,返回一个 promise 对象,而后当元素注册的时候解析。

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

例如,开发者也许想要延迟执行代码直到定义元素内全部子元素。若内嵌自定义元素,这将会很是有用。

有时候,父元素有可能会依赖于其子元素的实现。在这种状况下,开发者须要确保子元素在其父元素以前定义。

Shadow DOM

如前所述,须要把自定义元素和 shadow DOM 一块儿使用。前者用来把 JavaScript 逻辑封装进元素然后者用来为一小段 DOM 建立一个不为外部影响的隔绝环境。建议查看以前专门介绍 shadow DOM 的文章以便更好地理解 shadow DOM 概念。

只需调用 this.attachShadow 就能够在自定义元素内使用 shadow DOM

class MyCustomElement extends HTMLElement {
  // ...

  constructor() {
    super();

    let shadowRoot = this.attachShadow({mode: 'open'});
    let elementContent = document.createElement('div');
    shadowRoot.appendChild(elementContent);
  }

  // ...
});

模板

咱们在以前的文章中简单介绍了下模板,须要单独一篇文章来专门介绍模板。这里,咱们将会给出一个简单的示例来介绍如何在自定义元素中使用模板。

经过声明一个 DOM 片断来使用 <template>,该标签内容只会被解析而不会在页面上渲染。

<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 结构和 JavaScript 逻辑完美地隔离开来。

样式化

那么,咱们讲解了 HTML 和 JavaScript,如今还剩下 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 元素。而一个未定义元素没有实现任何内置元素的任何方法或属性。

浏览器兼容

custom elements 初版是在 Chrome 36+ 中引入的。被称为自定义元素接口 v0,虽然如今仍然可用,可是已经被弃用并被认为是糟糕的实现。若想要学习 v0 版,能够阅读这篇文章。从 Chrome 54 和 Safari 10.1(虽然只有部分支持) 开始支持自定义元素接口 v1,微软 Edge 还处于其原型设计阶段而 Mozilla 从 v50 开始支持,但默认不支持须要显式启用。目前只有 webkit 浏览器彻底支持。然而,如上所述,可使用垫片库兼容到包括 IE11 在内的全部浏览器。

检测可用性

经过检查 window 对象中的 customElements 属性是否可用来检查浏览器是否支持自定义元素。

const supportsCustomElements = 'customElements' in window;

if (supportsCustomElements) {
  // 可使用自定义元素接口
}

不然须要使用垫片库:

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);
  });
}

// Lazy load the polyfill if necessary.
if (supportsCustomElements) {
  // 浏览器原生支持自定义元素
} else {
  loadScript('path/to/custom-elements.min.js').then(_ => {
    // 加载自定义元素垫片
  });
}

总之,网页组件标准中的自定义元素为开发者提供了以下功能:

  • 把 JavaScript 和 CSS 样式整合入 HTML 元素
  • 容许开发者扩展已有的 HTML 元素(内置和其它自定义元素)
  • 不须要其它库或者框架的支持。只须要原生 JavaScript,HTML 和 CSS 还有可选的垫片库来支持旧浏览器。
  • 能够和其它网页组件功能无缝衔接(shadow DOM,模板,插槽等)。
  • 和浏览器开发者工具紧密集成在一块儿。
  • 使用已知的可访问功能

总之,自定义元素和开发者已经使用过的组件技术并无什么大的不一样。它只让开发网页程序过程更加便携的另外一种方式。那么,它让更快地构建很是复杂的程序成为可能。

参考资料:

相关文章
相关标签/搜索