文章出處

本文是對web components的一次實踐,最終目的是做出一個tab組件,本文涉及Custom Elements(自定義元素)、HTML Imports(HTML導入)、HTML Templates(HTML模板)、Shadow DOM(影子DOM)四部分知識。

自定義元素

自定義元素通過document.registerElement注冊。

第一個參數是自定義元素的標簽名,標簽名需要使用 - 連接,之所以要這樣設計是因為這樣能使解析器能很容易的區分自定義元素和 HTML 規范定義的元素,同時確保了 HTML 增加新標簽時的向前兼容。
第二個參數用于描述該元素的原型。

<my-tab></my-tab>
    <script>
        document.registerElement("my-tab",{
            prototype:Object.create(HTMLElement.prototype)
        });
    </script>

雖然我們創建了一個自定義的元素,但它現在還沒有任何內容,我們去添加點內容吧

<my-tab></my-tab>
    <script>
        document.registerElement("my-tab",{
            prototype:Object.create(HTMLElement.prototype,{
                createdCallback:{
                    value:function(){
                        var div = document.createElement("div");
                        div.textContent = "web compontents";
                        this.appendChild(div);
                    }
                }
            })
        });
    </script>

效果如下

在創建自定義元素時,會發生以下幾個事件

自定義元素的生命周期
createdCallback 創建元素實例
enteredDocumentCallback 向文檔插入實例
leftDocumentCallback 從文檔中移除實例
attributeChangedCallback(attrName, oldVal, newVal) 添加,移除,或修改一個屬性

以上代碼的意思是,在元素創建完成時給當前自定義元素插入一個元素,this指向頁面中的my-tab,這個my-tab的原型指向以下這個對象

Object.create(HTMLElement.prototype,{
                createdCallback:{
                    value:function(){
                        var div = document.createElement("div");
                        div.textContent = "web compontents";
                        this.appendChild(div);
                    }
                }
            })

通過js來創建元素還是太麻煩了些,我們可以通過template模板來寫,如下

    <template id="tmp">
        <style>
            li{
                list-style:none;
            }
            .tab-title{
                display:flex;
            }
            .tab-title li{
                width:100px;
                line-height:35px;
                text-align:center;
                border:1px solid #ccc;
            }
            .tab-title li:not(:last-of-type){
                border-right:none;
            }
            .tab-title .active{
                color:orange;
            }
            .tab-content li{
                display:none;
            }
            .tab-content .active{
                display:block;
            }
        </style>
        <ul class="tab-title">
            <li class="active">標題1</li>
            <li>標題2</li>
            <li>標題3</li>
        </ul>
        <ul class="tab-content">
            <li class="active">內容1</li>
            <li>內容2</li>
            <li>內容3</li>
        </ul>
        <script>
            var titleNode = document.querySelector(".tab-title"),
                titleNodes = titleNode.children,
                contentNodes = document.querySelectorAll(".tab-content > li"),
                preIndex = 0;
            titleNode.addEventListener("click",function(event){
                if(event.target.matches(".tab-title > li")){
                    var index = Array.prototype.indexOf.call(titleNodes,event.target);

                    titleNodes[preIndex].classList.remove("active");
                    titleNodes[index].classList.add("active");
                    contentNodes[preIndex].classList.remove("active");
                    contentNodes[index].classList.add("active");

                    preIndex = index;
                }
            });
        </script>
    </template>
    <my-tab></my-tab>
    <script>
        document.registerElement("my-tab",{
            prototype:Object.create(HTMLElement.prototype,{
                createdCallback:{
                    value:function(){
                        var tmp = document.getElementById("tmp");
                        this.appendChild(tmp.content.cloneNode(true));
                    }
                }
            })
        });
    </script>

用template模板來寫的好處顯而易見,template模板的內容并不會直接顯示在頁面中,我們通過tmp.content.cloneNode(true)復雜了一份模板內容,將內容添加到了自定義元素中。效果如下

Shadow DOM

盡管現在已經實現了一個自定義元素,但是還有諸多的問題,如我們在template中寫的樣式依然會影響全局的,全局的也能影響我們自定義元素的樣式,如果想要解決這個問題,我們需要使用到Shadow DOM,如果你對Shadow DOM不熟,強烈建議你看Shadow DOM系列,本文不做詳細介紹。

    <template id="tmp">
        <style>
            li{
                list-style:none;
            }
            .tab-title{
                display:flex;
            }
            .tab-title li{
                width:100px;
                line-height:35px;
                text-align:center;
                border:1px solid #ccc;
            }
            .tab-title li:not(:last-of-type){
                border-right:none;
            }
            .tab-title .active{
                color:orange;
            }
            .tab-content li{
                display:none;
            }
            .tab-content .active{
                display:block;
            }
        </style>
        <ul class="tab-title">
            <li class="active">標題1</li>
            <li>標題2</li>
            <li>標題3</li>
        </ul>
        <ul class="tab-content">
            <li class="active">內容1</li>
            <li>內容2</li>
            <li>內容3</li>
        </ul>
    </template>
    <my-tab></my-tab>
    <my-tab></my-tab>
    <script>
        document.registerElement("my-tab",{
            prototype:Object.create(HTMLElement.prototype,{
                createdCallback:{
                    value:function(){
                        var tmp = document.getElementById("tmp");
                        var shadow = this.createShadowRoot();
                        shadow.appendChild(document.importNode(tmp.content,true));

                        var titleNode = shadow.querySelector(".tab-title"),
                            titleNodes = titleNode.children,
                            contentNodes = shadow.querySelectorAll(".tab-content > li"),
                            preIndex = 0;
                        titleNode.addEventListener("click",function(event){
                            if(event.target.matches(".tab-title > li")){
                                var index = Array.prototype.indexOf.call(titleNodes,event.target);

                                titleNodes[preIndex].classList.remove("active");
                                titleNodes[index].classList.add("active");
                                contentNodes[preIndex].classList.remove("active");
                                contentNodes[index].classList.add("active");

                                preIndex = index;
                            }
                        });
                    }
                }
            })
        });
    </script>

this.createShadowRoot()此句代碼表示,將當前元素作為影子DOM的寄主,其他代碼基本和之前的一樣,不過得注意一下,不能去用document去獲取影子DOM里面的元素了,需通過this.createShadowRoot();返回的對象去操作,效果如下

現在代碼就互不影響啦,不過我們的HTML代碼寫的還是太死,我們再將代碼改改

    <template id="tmp">
        <style>
            li{
                list-style:none;
            }
            .tab-title{
                display:flex;
            }
            .tab-title li{
                width:100px;
                line-height:35px;
                text-align:center;
                border:1px solid #ccc;
            }
            .tab-title li:not(:last-of-type){
                border-right:none;
            }
            .tab-title .active{
                color:orange;
            }
            .tab-content li{
                display:none;
            }
            .tab-content .active{
                display:block;
            }
        </style>
        <content select=".tab-title"></content>
        <content select=".tab-content"></content>
    </template>
    <my-tab>
        <ul class="tab-title">
            <li class="active">標題1</li>
            <li>標題2</li>
            <li>標題3</li>
        </ul>
        <ul class="tab-content">
            <li class="active">內容1</li>
            <li>內容2</li>
            <li>內容3</li>
        </ul>
    </my-tab>

content標簽可以用來獲取my-tab中的內容,select用來選擇對應的內容,只要和class對應起來就行,我們來看看效果

啊,樣式竟然不行了,主要是不能這么用了,給content中的元素設置樣式得用::content,如下

<style>
            ::content li{
                list-style:none;
            }
            ::content .tab-title{
                display:flex;
            }
            ::content .tab-title li{
                width:100px;
                line-height:35px;
                text-align:center;
                border:1px solid #ccc;
            }
            ::content .tab-title li:not(:last-of-type){
                border-right:none;
            }
            ::content .tab-title .active{
                color:orange;
            }
            ::content .tab-content li{
                display:none;
            }
            ::content .tab-content .active{
                display:block;
            }
        </style>

效果如下

我們還得將js中一段話改改

var titleNode = this.querySelector(".tab-title"),
                            titleNodes = titleNode.children,
                            contentNodes = this.querySelectorAll(".tab-content > li"),
                            preIndex = 0;

前面我們用的是shadow來獲取的元素,現在用的是content中的內容,那么獲取元素和我們平常獲取的方式一樣。我猜,你肯定看蒙了,所以啊,還是先去看我前面推薦的那個Shadow Dom教程吧。

我們再添加一個tab

    <my-tab>
        <ul class="tab-title">
            <li class="active">HTML</li>
            <li>CSS</li>
            <li>javascript</li>
        </ul>
        <ul class="tab-content">
            <li class="active">HTMLHTMLHTMLHTML</li>
            <li>CSSCSSCSSCSS</li>
            <li>javascriptjavascriptjavascript</li>
        </ul>
    </my-tab>

效果如下

HTML導入

是不是感覺很強大,不過現在還沒有完,一般我們組件都是放在一個文件里面的,需要的時候引進來進行,所以啊,我們還得干活,HTML的引入具體如下

<link id="tab" rel="import" href="tab.html">

將rel改成import就可以引入html文件了,我們將前面的所有代碼都復制到tab.html中

tab.html
<template id="tmp">
    <style>
        ::content li{
            list-style:none;
        }
        ::content .tab-title{
            display:flex;
        }
        ::content .tab-title li{
            width:100px;
            line-height:35px;
            text-align:center;
            border:1px solid #ccc;
        }
        ::content .tab-title li:not(:last-of-type){
            border-right:none;
        }
        ::content .tab-title .active{
            color:orange;
        }
        ::content .tab-content li{
            display:none;
        }
        ::content .tab-content .active{
            display:block;
        }
    </style>
    <content select=".tab-title"></content>
    <content select=".tab-content"></content>
</template>
<script>
    document.registerElement("my-tab",{
        prototype:Object.create(HTMLElement.prototype,{
            createdCallback:{
                value:function(){
                    var tmp = document.querySelector("#tab").import.querySelector("#tmp");
                    var shadow = this.createShadowRoot();
                    shadow.appendChild(document.importNode(tmp.content,true));

                    var titleNode = this.querySelector(".tab-title"),
                            titleNodes = titleNode.children,
                            contentNodes = this.querySelectorAll(".tab-content > li"),
                            preIndex = 0;
                    titleNode.addEventListener("click",function(event){
                        if(event.target.matches(".tab-title > li")){
                            var index = Array.prototype.indexOf.call(titleNodes,event.target);

                            titleNodes[preIndex].classList.remove("active");
                            titleNodes[index].classList.add("active");
                            contentNodes[preIndex].classList.remove("active");
                            contentNodes[index].classList.add("active");

                            preIndex = index;
                        }
                    });
                }
            }
        })
    });
</script>
index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link id="tab" rel="import" href="tab.html">
    <script src="index2.js" defer></script>
</head>
<body>
    <my-tab>
        <ul class="tab-title">
            <li class="active">標題1</li>
            <li>標題2</li>
            <li>標題3</li>
        </ul>
        <ul class="tab-content">
            <li class="active">內容1</li>
            <li>內容2</li>
            <li>內容3</li>
        </ul>
    </my-tab>
    <my-tab>
        <ul class="tab-title">
            <li class="active">HTML</li>
            <li>CSS</li>
            <li>javascript</li>
        </ul>
        <ul class="tab-content">
            <li class="active">HTMLHTMLHTMLHTML</li>
            <li>CSSCSSCSSCSS</li>
            <li>javascriptjavascriptjavascript</li>
        </ul>
    </my-tab>
</body>
</html>

效果如下

如果你有去看前面的代碼的話,和現在的對吧會發現有一段代碼被我改了,tab.html中的var tmp = document.querySelector("#tab").import.querySelector("#tmp");這句,之前是直接通過document來獲取的template模板,但現在有些不同,雖然我們的代碼是通過html導入過來的,但是tab中的document仍然還是主頁面的document,因此我們還得通過document.querySelector("#tab").import來獲取模板元素,具體可以到網上搜索一下。

到這里總算是完成了這個tab組件了,這里不得不說一句,關于組件中的javascript根本沒有被分離,它的作用域仍然是全局的,有必要的話請使用自執行函數。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()