《JavaScript权威指南》中,记录着那么一篇文章《Web组件》。里面说到
示例用自定义元素实现了
<search-box></search-box>
组件,并使用<template>
标签来提高效率,使用影子根节点做到了封装。
这里有三个关键点:自定义标签、影子DOM、插槽。
首先介绍一下影子节点。
什么是影子DOM
影子DOM(Shadow DOM)是Web组件技术中的一项功能,它允许开发者创建封装的组件,将其样式和行为与页面的其他部分隔离开来。影子DOM可以看作是一个独立的DOM子树,它与主DOM树相互独立,不会受到外部样式和脚本的影响。
通过使用影子DOM,开发者可以创建自定义的HTML元素,这些元素具有自己的样式和行为,与页面的其他部分相互隔离。这样可以避免样式冲突和命名空间污染的问题,同时也提供了更好的封装性和可维护性。
影子DOM通过使用ShadowRoot对象来实现,开发者可以将其附加到一个普通的DOM元素上,从而创建一个具有独立DOM树的影子DOM。影子DOM中的元素可以通过JavaScript进行操作和控制,同时也可以使用CSS样式进行样式化。
总结来说,影子DOM是一种用于创建封装的、独立的DOM子树的技术,它可以帮助开发者构建更可靠、可维护和可重用的Web组件。
例如常用的<video>
标签,就是封装在影子DOM中的,如下图所示:
可以看到video标签下多了一个#shadow-root,一般情况下,他是被隐藏起来的,如果想看到#shadow-root,可以到浏览器中设置一下偏好,如下图所示:
封装searchBox组件
定义一个SearchBox类,继承HTMLElement,然后只需要在body中引用即可。
html
<body>
<search-box></search-box>
</body>
js
class SearchBox extends HTMLElement{
constructor() {
super();
this.attachShadow({ mode:'open'});
this.shadowRoot.append(SearchBox.template.content.cloneNode(true));
this.input = this.shadowRoot.querySelector("#input");
let leftSlot = this.shadowRoot.querySelector('slot[name="left"]');
let rightSlot = this.shadowRoot.querySelector('slot[name="right"]');
this.input.onfocus = () => {this.setAttribute("focused",'');};
this.input.onblur = () => {this.removeAttribute("focused");};
leftSlot.onclick = this.input.onchange = (event) =>{
event.stopPropagation();
if (this.disabled) return;
this.dispatchEvent(new CustomEvent('search',{
detail:this.input.value
}));
};
rightSlot.onclick = (event) =>{
event.stopPropagation();
if (this.disabled) return;
let e = new CustomEvent('clear',{ cancelable: true});
this.dispatchEvent(e);
if (!e.defaultPrevented){
this.input.value = '';
}
};
}
attributeChangedCallback(name,oldValue,newValue){
if (name === 'disabled'){
this.input.disabled = newValue !== null;
}else if (name === 'placeholder'){
this.input.placeholder = newValue;
}else if (name === 'size'){
this.input.size = newValue;
}else if (name === 'value'){
this.input.value = newValue;
}
}
get placeholder(){ return this.getAttribute('placeholder') }
get size(){ return this.getAttribute('size') }
get value(){ return this.getAttribute('value') }
get disabled(){ return this.getAttribute('disabled') }
get hidden(){ return this.getAttribute('hidden') }
set placeholder(val){ this.setAttribute('placeholder',val)}
set size(val){ this.setAttribute('size',val)}
set value(text){ this.setAttribute('value',text)}
set disabled(val){
if (val) this.setAttribute('disabled','');
else this.removeAttribute('disabled')
}
set hidden(val){
if (val) this.setAttribute('hidden','');
else this.removeAttribute('hidden')
}
}
SearchBox.observedAttributes = ['disabled','placeholder','value','size'];
SearchBox.template = document.createElement('template');
SearchBox.template.innerHTML = `
<style>
:host{
display: inline-block;
border: solid black 1px;
border-radius: 5px;
padding: 4px 6px;
}
:host[disabled]{
opacity: 0.5;
}
:host[hidden]{
display: none;
}
:host[focused]{
box-shadow: 0 0 2px 2px #6ae;
}
input{
border-width: 0;
outline: none;
font:inherit;
background: inherit;
}
slot{
cursor: default;
user-select:none;
}
slot[name='left']{
/*font-size: 60px;*/
}
</style>
<div>
<slot name="left">\u{1f50d}</slot>
<input type="text" id="input" />
<slot name="right">\u{2573}</slot>
</div>
`
customElements.define('search-box',SearchBox);
</script>
使用组件插槽
在封装过程中,使用到了插槽
js
<div>
<slot name="left">\u{1f50d}</slot>
<input type="text" id="input" />
<slot name="right">\u{2573}</slot>
</div>
页面上直接使用
html
<search-box>
<span slot="right">取消</span>
</search-box>
联想到Vue
当书籍文章看到这里的时候,不得不联想到Vue的自定义组件和插槽的知识。趁热打铁,到Vue的组件中,翻看代码,Vue可以这样去定义组件。
js
Vue.component('router-link', {
props: {
to: String
},
render(){
let path = this.to;
if(this._self.$router.mode === 'hash'){
path = '#' + path;
}
return <a href={path}>{this.$slots.default}</a>
}
});
Vue插槽
在看Vue官方文档插槽的文档,里面写了很多的知识点。 v2.cn.vuejs.org/v2/guide/co...
其实一开始的时候,我以为,插槽是Vue独有的,直到看了《JavaScript权威指南》中对Web组件的实现,才知道,原来JavaScript中已经有了插槽的概念。
带着对插槽的好奇(其实是觉得Vue的几种插槽类型,老忘记),深入的去了解一下,Vue是如实现插槽的。
插槽使用
首先是在子组件中定义常见的插槽类型,例如下面的几种类型:默认插槽(匿名插槽)、具名插槽、作用域插槽。
html
<template id="son">
<div>
<div>我是头部</div>
<slot>默认</slot>
<slot name="named">具名</slot>
<slot name="scope" item="你好" ></slot>
<button @click="GetSlots">获取插槽</button>
</div>
</template>
然后在父组件中使用。
html
<template id="father">
<div>
<son>
<div>默认插槽</div>
<template #named>具名插槽</template>
<template #scope="{item}">
<div>{{item}},作用域插槽</div>
</template>
</son>
</div>
</template>
在子组件定义一个按钮GetSlotsGetSlots(){ console.log(this.$scopedSlots );}
点击的时候,看一下Vue是如何处理插槽的。如下图可以看到,返回了一个对象,对象的key就是插槽的类型,值就是一个函数,即节点。
由于.$scopedSlots
直接返回的插槽节点,那可以利用render
函数在子组件中直接去执行他,看一执行下效果,也就是说插槽,其实就是用函数的形式来处理的,那么对于函数的处理,就可以非常灵活了。
js
render(createElement){
return createElement('div',null,[
...this.$scopedSlots.default(), // 匿名插槽
...this.$scopedSlots.named(), // 具名插槽
...this.$scopedSlots.scope({item:'你好'}) // 作用域插槽
]
)
}
Vue2插槽完整示例代码
html
<div id="app">
<father></father>
</div>
<template id="father">
<div>
<son>
<div>默认插槽</div>
<template #named>具名插槽</template>
<template #scope="{item}">
<div>{{item}},作用域插槽</div>
</template>
</son>
</div>
</template>
<template id="son">
<!-- <div>-->
<!-- <div>我是头部</div>-->
<!-- <slot>默认</slot>-->
<!-- <slot name="named">具名</slot>-->
<!-- <slot name="scope" item="你好" ></slot>-->
<!-- <button @click="GetSlots">获取插槽</button>-->
<!-- </div>-->
</template>
<script>
// 父组件
Vue.component("father", {
template: "#father",
// 子组件
components: {
"son": {
template: "#son",
// methods: {
// GetSlots(){
// console.log(this.$scopedSlots.scope());
// }
// }
render(createElement){
return createElement('div',null,[
...this.$scopedSlots.default(),
...this.$scopedSlots.named(),
...this.$scopedSlots.scope({item:'你好'})
]
)
}
}
}
});
let vue = new Vue({
el: '#app',
data: {},
computed: {},
components: {}
});
</script>
Vue3示例代码
js
<template>
<Son>
<div>默认插槽</div>
<template #name>具名插槽</template>
<template #scope="{item}">
<div>{{item}},作用域插槽</div>
</template>
</Son>
</template>
<script setup>
import Son from './slots.js'
</script>
slots.js
文件,可以看到和vue2是一样的效果。
js
import { createElementVNode } from 'vue'
export default {
setup(props,{slots}){
const slotDefault = slots.default()
const slotName = slots.name()
const scopeNamed = slots.scope({item:'你好'})
return ()=>{
return createElementVNode('div',null,[
...slotDefault,
...slotName,
...scopeNamed
])
}
}
}
从Web组件到影子DOM,再到Vue自定义组件,最后到Vue插槽的实现,这探索的过程,既能了解到新颖的知识,又能巩固Vue插槽的知识。实在有趣。