自定义元素 v1 - 可重复使用的网络组件

借助自定义元素,网络开发者可以定义新的 HTML 标记、扩展现有标记并创建可重复使用的网络组件。

借助自定义元素,网络开发者可以创建新的 HTML 标记, 扩充现有 HTML 标记,或者扩展其他开发者已有的组件 编写。API 是网络 组件。它提供一个 使用基于标准的方法创建可重用组件, 原始 JS/HTML/CSS。结果是代码更少、模块化代码更少,并且 我们的应用

简介

浏览器为我们提供了一个用于构建 Web 应用的优秀工具。时间是 称为 HTML您可能听说过它!它具有声明式、可移植、 且易于使用。HTML 可能是多么伟大,它的词汇和 扩展性会受到限制。目前的 HTML 一直无法 自动将 JS 行为与您的标记关联起来。

自定义元素是对 HTML 进行现代化改造的理想选择,填补了缺失的 以及将结构与行为捆绑在一起。如果 HTML 未提供 我们可以创建一个自定义元素来实现此目的。自定义 这些元素既能教给浏览器一些新技巧,又能保留 HTML 的优势

定义新元素

要定义新的 HTML 元素,我们需要 JavaScript 的强大功能!

customElements 全局变量用于定义自定义元素和进行教学 新代码。使用标记名称调用 customElements.define() 以及一个用于扩展基础 HTMLElement 的 JavaScript class

示例 - 定义一个移动抽屉面板 <app-drawer>

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

用法示例:

<app-drawer></app-drawer>

请务必注意,使用自定义元素 使用 <div> 或任何其他元素。实例可以在网页上声明 使用 JavaScript 动态创建、附加事件监听器等。 了解更多示例。

定义元素的 JavaScript API

自定义元素的功能使用 ES2015 定义 class 扩展 HTMLElement。扩展 HTMLElement 可确保自定义元素 继承整个 DOM API,表示您向 类成为元素 DOM 接口的一部分。从本质上讲,使用该类 为您的代码创建公共 JavaScript API

示例 - 定义 DOM 接口 <app-drawer>

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

在此示例中,我们将创建一个具有 open 属性 disabled 的抽屉式导航栏 属性以及 toggleDrawer() 方法。它还以 HTML 形式反映属性 属性

自定义元素的一个优点是,类定义中的 this 指的是 DOM 元素本身,即类的实例。在我们的 例如,this 是指 <app-drawer>。这 (😂?) 就是这个元素 向自身附加 click 监听器!不限于事件监听器。 整个 DOM API 可在元素代码内使用。使用 this 访问 元素的属性,检查其子项 (this.children)、查询节点 (this.querySelectorAll('.items')) 等

有关创建自定义元素的规则

  1. 自定义元素的名称必须包含短划线 (-)。所以,<x-tags><my-element><my-awesome-app> 均为有效名称,而 <tabs><foo_bar> 不是。满足此要求后,HTML 解析器 区分自定义元素和常规元素。它还确保转发 向 HTML 中添加新代码时的兼容性。
  2. 不得多次注册同一代码。如果尝试这样做的话 会抛出 DOMException。将新代码告诉浏览器之后 。不可收回。
  3. 自定义元素不能自闭合,因为 HTML 只允许 元素 具有自动关闭功能始终编写结束标记 (<app-drawer></app-drawer>).

自定义元素回应

自定义元素可以定义特殊的生命周期钩子,以便在 有趣的事物这些称为自定义元素 回应

名称 调用时间
constructor 该元素的实例 创建或升级。可用于初始化 设置事件监听器或 创建影子 dom。 请参阅 spec 了解您在 constructor 中可以执行的操作的限制。
connectedCallback 每次调用 元素插入到 DOM 中。这对于运行设置代码很有用,例如 提取资源或渲染一般来说,您应该将工作延迟 直到现在为止。
disconnectedCallback 每次元素从 DOM 中移除时调用。适用对象 运行清理代码
attributeChangedCallback(attrName, oldVal, newVal) 观察到的属性被触发时调用 已添加、移除、更新或替换。也调用初始值 当解析器创建某个元素时触发,或者 已升级注意observedAttributes属性中列出的属性将 收到此回调。
adoptedCallback 通过 自定义元素已移至新的 document(例如 有人叫document.adoptNode(el))。

回应回调是同步的。如果有人拨打 el.setAttribute() ,则浏览器会立即调用 attributeChangedCallback()。 同样,在元素被覆盖后,您会立即收到 disconnectedCallback() 从 DOM 中移除(例如,用户调用 el.remove())。

示例:向 <app-drawer> 添加自定义元素回应:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

在需要时定义回应。如果您的元素足够复杂 并在 connectedCallback() 中打开与 IndexedDB 的连接,请执行必要的操作 disconnectedCallback()中的清理工作。但要小心!您不能依赖于您的 任何情况下都会从 DOM 中删除元素。例如: 如果用户关闭标签页,则绝不会调用 disconnectedCallback()

属性和属性

将属性反映为属性

HTML 属性通常以 HTML 属性。例如,当 hiddenid 的值在 JS:

div.id = 'my-id';
div.hidden = true;

值将作为属性应用到活动 DOM:

<div id="my-id" hidden>

这称为“反映属性 属性”。 几乎所有 HTML 属性都会如此。为什么呢?属性也可用于 以及某些 API(例如无障碍功能和 CSS) 选择器依靠属性来发挥作用。

在您希望保留元素 DOM 的任何位置反映属性都非常有用 与其 JavaScript 状态同步的呈现方式。您可能想要 因此用户定义的样式在 JS 状态发生变化时应用。

回顾我们的 <app-drawer>。此组件的使用者可能希望将其淡出 和/或禁止用户互动:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

disabled 属性在 JS 中发生更改时,我们希望该属性也更改为 添加到 DOM 以使用户的选择器匹配。元素可以提供 将值映射到同名属性,从而:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

观察属性的变化

HTML 属性是用户声明初始状态的一种便捷方式:

<app-drawer open disabled></app-drawer>

元素可通过定义 attributeChangedCallback。每次更改时,浏览器都会调用此方法 observedAttributes 数组中列出的属性。

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

在此示例中,我们在 <app-drawer> 上设置其他属性时, 更改了 disabled 属性。虽然这里我们没有这么做,但您可以 还使用 attributeChangedCallback 来使 JS 属性与其保持一致 属性

元素升级

渐进式增强的 HTML

我们已经了解到,自定义元素是通过调用 customElements.define()。但这并不意味着您必须定义 + 注册 所有自定义元素

自定义元素可在定义注册之前使用

渐进式增强是自定义元素的一项功能。换言之,您可以 在网页上声明一些 <app-drawer> 元素,并且从不调用 customElements.define('app-drawer', ...)。这是因为 浏览器会以不同方式处理潜在的自定义元素,这是由于未知 代码。调用 define() 并向其捐赠现有 元素称为“元素升级”。

要了解标记名称何时被定义,您可以使用 window.customElements.whenDefined()。它会返回一个 promise,当出现 元素。

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

示例 - 延迟工作,直到一组子元素升级

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

元素定义的内容

自定义元素可以通过使用 元素代码中。回应功能可以派上用场。

示例 - 使用一些默认 HTML 创建元素:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

声明此标记会生成:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// TODO:DevSite - 代码示例已移除,因为它使用内联事件处理脚本

创建使用 Shadow DOM 的元素

Shadow DOM 提供了一种方法,可让元素拥有、渲染块和设置其样式。 独立于页面其余部分的 DOM。哎,你甚至可以躲开 将整个应用置于一个标记中:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

要在自定义元素中使用 Shadow DOM,请在自定义元素内调用 this.attachShadowconstructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

用法示例:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

用户的自定义文字

// TODO:DevSite - 代码示例已移除,因为它使用内联事件处理脚本

通过 <template> 创建元素

对于不熟悉的开发者,<template> 元素 可声明 DOM 的片段,这些片段会被解析、在页面加载时休眠,以及 可稍后在运行时激活。它是 Web 中的另一个 API 原语 组件系列。模板是声明 自定义元素的结构

示例:注册一个元素来包含根据 <template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

这几行代码发挥了重要作用。我们来了解一下 日期:

  1. 我们要在 HTML 中定义一个新元素:<x-foo-from-template>
  2. 元素的 Shadow DOM 是通过 <template> 创建的
  3. 由于有 Shadow DOM,元素的 DOM 位于元素的本地
  4. 由于 Shadow DOM,元素的内部 CSS 的作用域限定为该元素

我在 Shadow DOM 中。我的标记是 <template> 的模板。

// TODO:DevSite - 代码示例已移除,因为它使用内联事件处理脚本

设置自定义元素的样式

即使您的元素使用 Shadow DOM 定义了自己的样式,用户也可以设置样式 从他们的网页上获取您的自定义元素这些样式称为“用户定义的样式”。

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

您可能会问自己,如果元素具有样式,CSS 特异性会如何发挥作用。 Shadow DOM 中定义的方法。在特异性方面,用户样式优先。他们 始终覆盖元素定义的样式。请参阅创建元素 使用 Shadow DOM

预设置未注册元素的样式

在元素升级之前,您可以使用 :defined 伪类。这对于预设置组件样式非常有用。对于 例如,您可能想通过隐藏未定义的 FOUC, 组件,并在其获得定义时淡入。

示例 - 在定义 <app-drawer> 之前将其隐藏:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

定义 <app-drawer> 后,选择器 (app-drawer:not(:defined)) 不再匹配。

扩展元素

Custom Elements API 可用于创建新的 HTML 元素, 有助于扩展其他自定义元素,甚至是浏览器的内置 HTML。

扩展自定义元素

对另一个自定义元素的扩展是通过扩展其类定义来完成的。

示例 - 创建扩展 <app-drawer><fancy-app-drawer>

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

扩展原生 HTML 元素

假设您想要创建一个更漂亮的 <button>。您不必复制 <button> 的行为和功能,更好的选择是 使用自定义元素增强现有元素。

自定义内置元素是一种自定义元素,用于扩展 浏览器内置 HTML 代码。扩展现有集群 元素获得其所有功能(DOM 属性、方法、无障碍功能)。 要编写渐进式网页应用 app,而不是逐步增强现有 HTML 元素

要扩展元素,您需要创建继承的类定义 正确的 DOM 接口例如,扩展 <button> 需要从 HTMLButtonElement 而不是 HTMLElement 继承。 同样,扩展 <img> 的元素也需要扩展 HTMLImageElement

示例 - 扩展 <button>

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

请注意,在扩展原生代码时,对 define() 的调用会略有变化 元素。第三个参数用于告知浏览器 扩展。这是必要的,因为许多 HTML 标记共享同一个 DOM 界面。<section><address><em>(等)均共享 HTMLElement<q><blockquote> 共用 HTMLQuoteElement;等等... 指定 {extends: 'blockquote'} 可告知浏览器您正在创建 增强了 <blockquote>,而不是 <q>。请参阅HTML spec ,查看 HTML DOM 接口的完整列表。

自定义内置元素的使用方可以通过多种方式使用它。他们可以 通过在原生标记中添加 is="" 属性来声明它:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

使用 JavaScript 创建实例:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

或使用 new 运算符:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

下面是扩展 <img> 的另一个示例。

示例 - 扩展 <img>

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

用户声明此组件为:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

或在 JavaScript 中创建实例:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

其他详细信息

未知元素与未定义的自定义元素

HTML 的使用较为宽松和灵活。例如,声明 <randomtagthatdoesntexist>,浏览器非常满意 接受它。为什么非标准代码可以发挥作用?答案是 HTML 规范 允许运行。规范未定义的元素将解析为 HTMLUnknownElement

自定义元素则并非如此。解析了可能的自定义元素 如果是使用有效名称(包含“-”)创建的,则为 HTMLElement。您 可以在支持自定义元素的浏览器中检查此项启动控制台: Ctrl + Shift + J(在 Mac 上为 Cmd + Opt + J),并粘贴到 以下代码行:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

API 参考文档

customElements 全局变量定义了处理自定义 元素。

define(tagName, constructor, options)

在浏览器中定义一个新的自定义元素。

示例

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

在给定有效的自定义元素标记名称的情况下,返回元素的构造函数。 如果未注册任何元素定义,则返回 undefined

示例

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

返回在定义自定义元素时进行解析的 promise。如果 元素已定义,则立即解决。如果标记名称并非指定值,则拒绝 有效的自定义元素名称。

示例

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

历史记录和浏览器支持

如果您最近几年一直在关注网络组件, Chrome 36 及更高版本实施了 Custom Elements API 的一个版本,该版本使用 document.registerElement(),而非 customElements.define()。就现在 被视为已弃用的标准版本 v0。 customElements.define() 是当下的最新热门问题以及哪些浏览器供应商 开始实施这称为自定义元素 v1。

如果您对旧版 v0 规范感兴趣,请访问 html5rocks 文章

浏览器支持

Chrome 54(状态)、 Safari 10.1(状态)和 Firefox 63(状态)具有 自定义元素 v1。Edge 已开始 开发

要检测自定义元素,请检查 window.customElements

const supportsCustomElementsV1 = 'customElements' in window;

聚酯纤维

在广泛支持浏览器之前, 独立 polyfill 适用于自定义元素 v1。不过,我们建议您使用 webcomponents.js loader 以最优方式加载网络组件 polyfill。加载器 使用特征检测来仅异步加载必要的 Polyfill 。

安装:

npm install --save @webcomponents/webcomponentsjs

用法:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  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);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

总结

自定义元素提供了一种新工具,可用于在浏览器中定义新的 HTML 标记, 创建可重复使用的组件将它们与其他新平台结合使用 以及 Shadow DOM 和 <template> 等基元,并且我们已开始实现 Web 组件的图片:

  • 用于创建和扩展可重复使用的组件的跨浏览器(Web 标准)。
  • 无需库或框架即可开始使用。原生 JS/HTML 太棒了!
  • 提供熟悉的编程模型。只有 DOM/CSS/HTML。
  • 与其他网络平台功能(Shadow DOM、<template>、CSS)完美配合 自定义属性等)
  • 与浏览器的开发者工具紧密集成。
  • 利用现有的无障碍功能。