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

相关推荐
To_OC1 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC1 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic1 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy1 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志2 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble3 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长3 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯4 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜4 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司5 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能