---
title: WebComponent——custom elements
tags: 
  - WebComponent
  - javascript
createTime: 2018/08/01 11:15:27
permalink: /article/m63fd7lf/
author: pengzhanbo
---


在我们的web应用开发中，HTML标签为我们提供了基础的应用和交互，我们使用HTML标签构建了各种各样丰富的web应用。

然而在我们开发web应用的过程中，html标签提供的语义化并不能完全满足我们的场景。虽然在HTML5标准中，也增加了不少包括`<header>`、`<section>`、`<article>`、`<nav>`、`<container>`、`<footer>`等语义化标签，但它们主要是为内容或布局添加的通用语义化标签，在实际的场景中，我们还需要使用 `class` 等一些属性或者辅助说明，声明该标签的具体语义。  

<!-- more -->

``` html
<div class="login-wrapper"></div>
```
如果可以这么做呢：
``` html
<login></login>
```
使用更加语义化的标签，满足我们各种场景，甚至是扩展已有标签的特性。那么我们该怎么做呢？

接下来是我们的主角： __[自定义元素（custom Elements）](http://w3c.github.io/webcomponents/spec/custom/)__

### 自定义元素

> 自定义元素能够帮助web开发者创建拥有自身特性的自定义标签。

### 创建自定义元素

_创建自定义元素有两种方式，这里只讨论 __DOM LEVEL 3__ 提供的 `customElements`，在 __DOM LEVEL 2__ 中的 `document.registerElement` 将作为补充内容在本文最后补充。_

[Custom Element API 规范](http://w3c.github.io/webcomponents/spec/custom/) 定义了`customElements`作为统一的对象管理自定义元素，并对ES6 class提供了更完善的支持。
>规范还定义了 `CustomElementRegistry`, 并且 `customElements instanceof CustomElementRegistry`。

我们可以通过 `customElements.define()` 方法来注册一个custom element，该方法接受以下参数：
`customElements.define(tarName, class[, option])`
- `tarName`: `DOMString`，用于表示所创建的元素名称。名称必须是小写字母开头，且必须包含至少一个`-`，任何不含`-`的自定义标签都会导致错误。例如`my-tag`,`my-list-item`为合法标签，`my_tag`,`myTag`都是非法的自定义标签名称；
- `class`: 类对象，用于定义元素行为.
- `option`: 包含 `extends` 属性的配置对象，可以指定所创建的元素继承自那个内置元素，可以继承任何内置元素；

`customElements`的类对象可以通过 ES 2015的类语法定义：
``` javascript
class MyTag extends HTMLElement {
    constructor() {
        super();
    }
}
customElements.define("my-tag", MyTag);
```

### 使用自定义元素的生命周期回调函数

在`customElements`的构造函数中，我们可以指定多个不同的回调函数，他们会在不同的声明周期被触发。

- `connectedCallback`: 元素首次插入到文档DOM时回调；
- `discannectedCallback`: 元素从文档DOM中删除时回调；
- `attributeChangedCallback`： 元素增加、删除、修改自身属性时回调；
- `adoptedCallback`：元素被移动到新的文档时回调；

``` javascript
class MyCustom extends HTMLElement {
    // 自定义元素开始提升时调用
    // 元素提升并不说明元素已插入到文档中
    // 在此阶段尽量避免进行DOM操作
    constructor() {
        super();
    }
    // 元素插入到文档时回调
    connectedCallback() {
        // do something...
    }
    // 元素从文档中删除时回调
    discannectedCallback() {
        // do something...
    }
    /*
     * 元素属性变化回调
     * @param name {string} 变化的属性名
     * @param oldValue {any} 变化前的值
     * @param newVlalue {any} 变化后的值
     */
    attributeChangedCallback(name, oldValue, newValue) {
        // do something...
    }
    // 元素被移动到新的文档中时调用
    // （When it is adopted into a new document, its adoptedCallback is run.）
    // 具体场景示例：通过document.adoptNode方法修改元素ownerDocument属性时可以触发
    adoptedCallback() {
        // do something...
    }
}
```

如果需要在元素属性发生变化后触发 `attributeChangedCallback`，就必须监听这些属性。 我们可以通过定义静态属性`observedAttributed`的 get函数来添加需要监听的属性：
``` javascript
static get observedAttributed() {
    return ['name'];
}
```

### 使用自定义元素

我们可以在文档的任何地方使用`customElements.define`注册的自定义元素，即使是在自定义元素注册之前。
``` html
<my-tag></my-tag>
```
或者：
``` js
class MyTag extends HTMLElement {
    constructor() {
        super();
    }
}
customElements.define("my-tag", MyTag);

// 方式一：
var tag = document.createElement('my-tag');
document.appendChild(tag);
// 方式二：
var tag = new MyTag();
document.appendChild(tag);
```

### 元素提升

浏览器是如何解析非标准的标签的？为什么对非标准的标签，浏览器不会报错？

> HTML规范：  
> 非规范定义的元素必须使用 _HTMLUnknownElement_ 接口。

我们在页面中声明一个 `<myTag>`标签，由于它是非标准标签，所以会继承 `HTMLUnknownElement`。

对于自定义元素，情况有所不同。 拥有合法元素名称的自定义元素继承自`HTMLElement`。   
对于不支持自定义元素的浏览器，拥有合法元素名称的标签，仍然继承`HTMLUnknownElement`。

### 扩展内置元素特性

在创建自定义元素时，置顶所需的扩展的元素，使用时，在内置元素上声明`is`属性指定自定义元素名称：
``` js
class CustomButton extends HTMLButtonElement {
    constructor() {
        super();
    }
}
customElements.define("custom-button", CustomButton, {
    extends: 'button'
});
```
``` html
<button is="custom-button"></button>
```

### 自定义元素样式

自定义元素和内置元素一样，可以使用CSS各类选择器定义样式。

自定义元素规范还提出了一个新的CSS伪类`:unresolved`。在浏览器调用你的`createdCallback()` 之前，这个伪类可以匹配到未完成元素提升的自定义元素。
``` css
custom-button{
    opacity: 1;
    transition: opacity 300ms;
}
custom-button:unresolved{
    opacity: 0
}
```
> :unresolved 不能用于继承自HTMLUnkownElement的元素。


### 浏览器支持

`Chrome`和`Opera`默认支持custom elements。`Firefox`计划在60/61的版本中默认支持自定义元素。`Safair`目前不支持自定义元素对内置元素的扩展。`Edge`在实现中。


### 补充内容：`document.registerElement`

使用`document.registerElement()` 创建自定义元素
``` javascript
var MyTag = document.registerElement('my-tag');
```
添加自定义元素特性：
``` javascript
var proto = Object.create(HTMLElement.prototype);
proto.hello = 'hello';
proto.sayHello = function () {
    alert(this.hello);
};
var MyTag = document.registerElement('my-tag', {
    prototype: proto
});
```
扩展原生元素特性

`document.registerElement()` 的第二个参数还允许我们为扩展原生素的特性。

``` javascript
var MyButton = document.registerElement('my-button', {
    extend: 'button',
    prototpye: Object.create(HTMLButtonElement.prototype)
});
```
``` html
<button is="my-button"><button>
```
生命周期以及回调方法

1. createdCallback(): 元素创建后回调。
2. attachCallback(): 元素附加到文档后调用。
3. detachCallback(): 元素从文档移除后调用。
4. attributeChangedCallback(): 元素任意属性变化后调用。
``` javascript
var myTagProto = Object.create(HTMLElement.prototype);

myTagProto.createdCallback = function() {
    // 元素创建后回调。
    this.textContent = '我被创建了';
};

var MyTag = document.registerElement('my-tag', {
    prototype: myTagProto
});
```

### 结语

自定义元素作为 `webComponent` 规范中的一部分，为web应用开发提供了更多的可能性，配合`webComponent` 规范的其他内容，可以为web开发者提供更强大的能力。
