深入了解 Web Components

2,013 阅读4分钟

一、Web Components是什么

目前,我们的组件化开发,主要是借助框架来实现的,比如vue/react这些。 那么目前,一套新的技术已经在各个主流浏览器有了不错的支持, 那么就是今天所讲的Web Components 技术。这是一种独立的技术,它不是一个api,而是通过三个关键的技术组成, 分别是: custom elementsshadow domHTML templates。 通过使用,可以做到各个组件间css、js、html都能做到有效隔离,给开发带来极大的便利。

我们目前的开发,越来越向组件化发展, 如果我们使用这套新技术,因为是原生浏览器支持的,所以,运行效率肯定要比框架要快的多。那么我们如果要进行技术选型, 需要先知道我们的使用场景,比如说是TO B类后台管理系统,那么我们可以有效规定用户使用的浏览器,那么这种情况是比较适合的。

先看下目前的支持程度:

image.png

image.png

image.png

主流浏览器的支持程度还是很不错的。(IE马上退出历史舞台了,所以在不考虑IE的项目中格外合适)

二、开始学习

2.1 创建一个自定义标签,从P标签继承

先创建一个js文件:

// custom-elements.js

class WordCount extends HTMLParagraphElement {
    constructor() {
        super();
    }
    //  目前只是新建了一个自定义元素,并没有使用shadow dom,所以样式并不会做到隔绝
    connectedCallback() {
        console.log('首次被插入dom时触发');
        this.render();
    }
    disconnectedCallback() {
        console.log('从dom中删除的时候触发');
    }
    attributeChangedCallback() {
        console.log('当 custom element增加、删除、修改自身属性时,被调用');
    }
    render() {
        const count = this.getAttribute('count');
        const price = 20 * count;
        this.innerHTML = `<p>选择的数量是${count}, 总价格是${price}</p>`;
    }
}


// 创建一个自定义元素,类对象是WordCount, 继承自<p>元素
customElements.define('word-count', WordCount, {extends: 'p'});

在创建的时候,有三个重要的生命周期:

  • connectedCallback 首次被插入dom中的时候触发,所以只会触发一次
  • disconnectedCallback 从dom中删除的时候触发, 所以也只会触发一次
  • attributeChangedCallback当custom elements 增加、删除、修改自身属性的时候,会被触发

html的部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>web components</title>
    <style>
        #title {
            font-size: 24px;
            color: #333;
            height: 60px;
            line-height: 60px;
        }
        p {
            color:aquamarine;
            font-size: 30px;
        }

    </style>
    <script src="./custom-elements.js"></script>

</head>
<body>
    <div id="title">custom Component</div>
    
    <p is="word-count" count="10"></p>
</body>
</html>

这样,把js文件引入了,并且在p标签上,我们自定义了一个叫iscount的属性。

看一下最终浏览器的渲染结果:

image.png

目前有以下基本结论:

  1. 我们创建的自定义组件已经加载到了浏览器中
  2. 组件的加载是在p标签下面的,因为我们这个组件继承自P标签
  3. 外层的css属性能够影响自定义组件中的样式

2.2 自定义一个html组件,并引入shadow

现在创建一个完全自定义的组件,跟vue或react的自定义组件一样,并引入样式隔离

// shadow-dom.js

class PopupInfo extends HTMLElement {
    constructor() {
        super();
    }
    connectedCallback() {
        // 具体代码逻辑
        const shadow = this.attachShadow({mode: 'open'});
        const wrapper = document.createElement('span');
        wrapper.setAttribute('class', 'wrapper');
        const icon = document.createElement('span');
        icon.setAttribute('class', 'icon');
        icon.setAttribute('tabindex', 0);

        const info = document.createElement('span');
        info.setAttribute('class', 'info');

        // 获取text属性上的内容,并添加到一个span标签内
        const text = this.getAttribute('text');
        info.textContent = text;

        // 插入icon
        let imgUrl;
        if(this.hasAttribute('img')) {
            imgUrl = this.getAttribute('img');

        } else {
            imgUrl = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F54%2F79%2F83%2F547983bb743d816230e483da74bccbe1.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1631007635&t=fafafd459d7b1f23c5cfa51812c9e5fe';
        }
        const img = document.createElement('img');
        img.src= imgUrl;
        icon.appendChild(img);

        // 创建css,并创建到shadow dom 上
        const style = document.createElement('style');
        style.textContent = `
            .wrapper {
                display: inline-block;
                width: 700px;
                height: 500px;
                background: #f5f5f5;
                font-size: 20px;
            }
            .icon {
                display: inline-block;
                height: 100px;
            }
            .icon img {
               height: 100%;
            }
            
        `;
        shadow.appendChild(style);
        shadow.appendChild(wrapper);
        wrapper.appendChild(icon);
        wrapper.appendChild(info);
    }

}

customElements.define('popup-info', PopupInfo);

上面逻辑中,关键是创建了一个shadow, this.attachShadow({mode: 'open'});, shadow的解释attachShadow MDN

其中提到我认为比较重要的两点:

  • 些元素不能使用 shadow DOM(例如<a>
  • mode (模式)
    • open shadow root元素可以从js外部访问根节点
    • close 拒绝从js外部访问关闭的shadow root节点

我们创建了几个标签,比如span、img,和一些属性。

在看看 html的操作:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shadow DOM</title>
    <style>
        #title {
            font-size: 24px;
            color: #333;
            height: 60px;
            line-height: 60px;
        }
        .span {
            font-size: 30px;
            color: green;
        }
        

    </style>
    <script src="./shadow-dom.js"></script>

</head>
<body>
    <div id="title">custom Component + shadow Dom</div>
    <popup-info text="Your card validation code (CVC)
    is an extra security feature — it is the last 3 or 4 numbers on the
    back of your card."></popup-info>
    
</body>
</html>

执行一下, 效果是:

image.png

可以看出,组件顺利的渲染了,并且外边的样式也不会影响里面的, 使用 custom element + shadow 已经做到自定义组件+样式隔离了。

2.3 加入HTML templates

上面的两步其实已经把精髓展示了。但是,还缺一步,就是我们对模板的处理都是在js中处理的,开发起来比较麻烦,也不容易进行扩展和维护。 所以template能够有效的解决这个问题。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>web components使用template模板</title>
    <style>
        #title {
            font-size: 24px;
            color: #333;
            height: 60px;
            line-height: 60px;
        }
        p {
            color:aquamarine;
            font-size: 30px;
        }

    </style>
</head>
<body>
    <div id="title">custom Component</div>
    <template id="temp">

        <slot><p>测试测试</p></slot>
    </template>
    
    <my-paragraph>
        <p>新的slot内容,把默认数据替换</p>
    </my-paragraph>
</body>
<script src="./template.js"></script>
</html>

template标签不会展示到页面上,需要获取到其中的内容,然后再进行展示



class MyParagraph extends HTMLElement {
    constructor() {
        super();
    }
    connectedCallback() {
        console.log('首次被插入dom时触发');
        const target = document.getElementById('temp');
        const content = target.content;
        const shadowRoot = this.attachShadow({mode: 'open'})
            .appendChild(content.cloneNode(true));
    }
}

customElements.define('my-paragraph', MyParagraph);



还有一种template的插入方式,是使用 link标签, 进行HTML imports。 比如:

<link rel="import" href="./contents.html">

但是这个功能已经不被支持,很可能在后面被删除,具体见 HTML_Imports

以及:

image.png

所以,大家不要再继续用 HTML-imports 的方式了。

以上就是Web Components的内容。 这里需要再指明的是, 并不是有了这个功能,就能抛弃vue等框架,mvvm框架的数据驱动视图,是无法直接用Web Components实现的。 两者结合起来,会迸发出新的可能性。