熟悉Vue开发和React开发的小伙伴们肯定特别熟悉“组件”这个概念。
通过创建组件,我们获得了功能独立、样式独立的HTML元素片段组。

我们知道,在Vue或React中,所谓的自定义组件,最终还是会编译为基本元素的嵌套与复杂的js逻辑。
当然,这些框架中的组件,还提供了模版语法等优秀的开发方式,以及跨组件数据流等优秀的功能。但是,抛开这些不谈,真正的将“自定义元素”搬上页面的,是 Web Components 规范。

PS:原生万岁。

1. 基本实现

首先,我们需要定义一个类,实现自定义元素的样式、内容与功能。
这个类需要继承自 HTMLElement类,从而成为独立元素,或者继承 HTMLUListElement 等基本HTML元素类。
在类的构造函数中,需要首先调用 super方法。然后,再进行内部的构建(元素结构、样式、事件监听等)。

class CommentCard extends HTML{
    constructor(){
        super();

        ...
    }
}

然后,通过以下方法注册custom elements:

customElements.define('comment-card', CommentCard);

define函数的第一个参数为要使用的标签名称,第二个参数为我们创建的类。
如果我们的 custom elements 继承自基本HTML元素类,还要为define函数传递第三个参数:

customElements.define('comment-card', CommentCard, {extends: 'ul'});

使用:

//独立元素
<comment-card></comment-card>

//继承元素
<ul is="comment-card">
    ...
</ul>

2. 使用<template>标签实现内部DOM结构构件与样式

我们可以通过原生的DOM操作,来构建 custom elements 内部的结构,并设置样式,如下:

class CommentCard extends HTML{
    constructor(){
        super();
        let $info = document.createElement('div');
        $info.classList.add('info');
        let $avatar = document.createElement('img');
        let $nickname = document.createElement('div');

        $info.appendChild($avatar);
        $info.appendChild($nickname);

        //所有的DOM结构都挂在自定义标签内
        this.appendChild($info);
    }
}

不过这样真的是太麻烦了。

更好的方式是,我们把自定义标签的 内部结构与样式 统统写在HTML文档的template标签内部,然后在 custom elements 类的构造函数中,使用DOM方法将其内容取出,复制并挂载到自定义元素中。

<template id="commentCardTemplate">
    <style>
        /* :host伪类代表元素自身的样式 */
        :host{
            ...
        }
        .info{
            ...
        }
        ...
    </style>
    <div class="info">
        <div class="avatar">
            <img src="" alt="" />
        </div>
        <div class="other">
            <div class="nickname"></div>
            <div class="time"></div>
        </div>
    </div>
    <div class="text"></div>
</template>
class CommentCard extends HTMLElement{
    constructor(){
        super();

        let templateEle = document.getElementById('commentCardTemplate');
        let content = templateEle.content.cloneNode(true);

        this.appendChild(content);
    }
}

为了保证代码的可维护性,我们可以把template元素以及内部内容,和 custom elements 的定义都封装在一个js文件中。

let tmpl = `
<template id="commentCardTemplate">
    <style>
        :host{
            ...
        }
        .info{
            ...
        }
        ...
    </style>
    <div class="info">
        <div class="avatar">
            <img src="" alt="" />
        </div>
        <div class="other">
            <div class="nickname"></div>
            <div class="time"></div>
        </div>
    </div>
    <div class="text"></div>
</template>
`;

//这里借用的jq的方法,也可以用其他的替代
document.documentElement.appendChild($.html(tmpl)[0]);

class CommentCard extends HTMLElement{
    constructor(){
        super();

        let templateEle = document.getElementById('commentCardTemplate');
        let content = templateEle.content.cloneNode(true);

        this.appendChild(content);
    }
}

3. 内容的填充

至此,整个自定义标签的结构与样式已经构建完毕。
接下来就是内容的填充了。

我们可以把要填充的内容,作为元素的标签的一个属性写进去。
在 custom elements 类的构造函数中,可以通过getAttribute或者dataset方式获取,并填充在对应的位置。

<comment-card data-avatar="xxx" data-nickname="xxx"></comment-card>
class CommentCard extends HTMLElement{
    constructor(){
        super();

        let templateEle = document.getElementById('commentCardTemplate');
        let content = templateEle.content.cloneNode(true);

        //内容填充
        content.querySelector('img').src = this.dataset.avatar;
        content.querySelector('.nickname').innerHTML = this.dataset.nickname;

        this.appendChild(content);
    }
}

4. 需要注意的几个点

  1. custom elements 的名称,不能是单个单词,其中必须要有短横线。
  2. 记得在样式中,给自定义元素的样式,增加display。最好是在自定义元素的定义中,写在:host伪类内,让其默认拥有一个display样式,将其设置为块状元素或者行内元素等。否则,元素将无法通过设置margin等进行定位。
  3. 如果你把template元素内容,和类的定义都封装在同一个js文件中,记得通过(function(){})()等方式为其设置一个单独的作用域,不然,等设置的自定义元素多了之后,会被变量的名称烦死。
  4. 一个奇怪的问题。custom elements 类的定义、自定义元素的注册等,要放在 DOM文档 自定义标签 加载后。比如将封装的js文件放在body标签后,或者使用$.ready()等方法执行类的定义、自定义元素的注册等。否则的话,DOM文档中自定义元素的attibute内容,在 custom elements 类的构造函数中无法读取出来,导致内容填充失败。
    但是但是,通过js,将自定义元素添加进文档,就不存在问题。
03-05 14:06