本文是对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{
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{
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{
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{
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{
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根本没有被分离,它的作用域仍然是全局的,有必要的话请使用自执行函数。