Vue插槽启示录:从Web组件到影子DOM

《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插槽的知识。实在有趣。

相关推荐
蜗牛快跑2137 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy8 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR2 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式