开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情
我们可以通过描述带有自己的方法、属性和事件等的类来创建自定义 HTML 元素。
在 custom elements (自定义标签)定义完成之后,我们可以将其和 HTML 的内建标签一同使用。
这是一件好事,因为虽然 HTML 有非常多的标签,但仍然是有穷尽的。如果我们需要像 <easy-tabs>、<sliding-carousel>、<beautiful-upload>…… 这样的标签,内建标签并不能满足我们。
我们可以把上述的标签定义为特殊的类,然后使用它们,就好像它们本来就是 HTML 的一部分一样。
Custom elements 有两种:
- Autonomous custom elements (自主自定义标签) —— “全新的” 元素, 继承自
HTMLElement抽象类. - Customized built-in elements (自定义内建元素) —— 继承内建的 HTML 元素,比如自定义
HTMLButtonElement等。
我们将会先创建 autonomous 元素,然后再创建 customized built-in 元素。
在创建 custom elements 的时候,我们需要告诉浏览器一些细节,包括:如何展示它,以及在添加元素到页面和将其从页面移除的时候需要做什么,等等。
通过创建一个带有几个特殊方法的类,我们可以完成这件事。这非常容易实现,我们只需要添加几个方法就行了,同时这些方法都不是必须的。
下面列出了这几个方法的概述:
class MyElement extends HTMLElement {
constructor() {
super();
// 元素在这里创建
}
connectedCallback() {
// 在元素被添加到文档之后,浏览器会调用这个方法
//(如果一个元素被反复添加到文档/移除文档,那么这个方法会被多次调用)
}
disconnectedCallback() {
// 在元素从文档移除的时候,浏览器会调用这个方法
// (如果一个元素被反复添加到文档/移除文档,那么这个方法会被多次调用)
}
static get observedAttributes() {
return [/* 属性数组,这些属性的变化会被监视 */];
}
attributeChangedCallback(name, oldValue, newValue) {
// 当上面数组中的属性发生变化的时候,这个方法会被调用
}
adoptedCallback() {
// 在元素被移动到新的文档的时候,这个方法会被调用
// (document.adoptNode 会用到, 非常少见)
}
// 还可以添加更多的元素方法和属性
}
在申明了上面几个方法之后,我们需要注册元素:
// 让浏览器知道我们新定义的类是为 <my-element> 服务的
customElements.define("my-element", MyElement);
现在当任何带有 <my-element> 标签的元素被创建的时候,一个 MyElement 的实例也会被创建,并且前面提到的方法也会被调用。我们同样可以使用 document.createElement('my-element') 在 JavaScript 里创建元素。
Custom element 名称必须包括一个短横线 -
Custom element 名称必须包括一个短横线 -, 比如 my-element 和 super-button 都是有效的元素名,但 myelement 并不是。
这是为了确保 custom element 和内建 HTML 元素之间不会发生命名冲突。
例子: “time-formatted”
举个例子,HTML 里面已经有 <time> 元素了,用于显示日期/时间。但是这个标签本身并不会对时间进行任何格式化处理。
让我们来创建一个可以展示适用于当前浏览器语言的时间格式的 <time-formatted> 元素:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
this.innerHTML = new Intl.DateTimeFormat("default", {
year: this.getAttribute('year') || undefined,
month: this.getAttribute('month') || undefined,
day: this.getAttribute('day') || undefined,
hour: this.getAttribute('hour') || undefined,
minute: this.getAttribute('minute') || undefined,
second: this.getAttribute('second') || undefined,
timeZoneName: this.getAttribute('time-zone-name') || undefined,
}).format(date);
}
}
customElements.define("time-formatted", TimeFormatted); // (2)
</script>
<!-- (3) -->
<time-formatted datetime="2019-12-01"
year="numeric" month="long" day="numeric"
hour="numeric" minute="numeric" second="numeric"
time-zone-name="short"
></time-formatted>
- 这个类只有一个方法
connectedCallback()—— 在<time-formatted>元素被添加到页面的时候,浏览器会调用这个方法(或者当 HTML 解析器检测到它的时候),它使用了内建的时间格式化工具 Intl.DateTimeFormat,这个工具可以非常好地展示格式化之后的时间,在各浏览器中兼容性都非常好。 - 我们需要通过
customElements.define(tag, class)来注册这个新元素。 - 接下来在任何地方我们都可以使用这个新元素了。
Custom elements 升级
如果浏览器在 customElements.define 之前的任何地方见到了 <time-formatted> 元素,并不会报错。但会把这个元素当作未知元素,就像任何非标准标签一样。
:not(:defined) CSS 选择器可以对这样「未定义」的元素加上样式。
当 customElement.define 被调用的时候,它们被「升级」了:一个新的 TimeFormatted 元素为每一个标签创建了,并且 connectedCallback 被调用。它们变成了 :defined。
我们可以通过这些方法来获取更多的自定义标签的信息:
customElements.get(name)—— 返回指定 custom elementname的类。customElements.whenDefined(name)– 返回一个 promise,将会在这个具有给定name的 custom element 变为已定义状态的时候 resolve(不带值)。
在 connectedCallback 中渲染,而不是 constructor 中
在上面的例子中,元素里面的内容是在 connectedCallback 中渲染(创建)的。
为什么不在 constructor 中渲染?
原因很简单:在 constructor 被调用的时候,还为时过早。虽然这个元素实例已经被创建了,但还没有插入页面。在这个阶段,浏览器还没有处理/创建元素属性:调用 getAttribute 将会得到 null。所以我们并不能在那里渲染元素。
而且,如果你仔细考虑,这样作对于性能更好 —— 推迟渲染直到真正需要的时候。
在元素被添加到文档的时候,它的 connectedCallback 方法会被调用。这个元素不仅仅是被添加为了另一个元素的子元素,同样也成为了页面的一部分。因此我们可以构建分离的 DOM,创建元素并且让它们为之后的使用准备好。它们只有在插入页面的时候才会真的被渲染。