Vuejs--插槽(slot)的相关知识

245 阅读7分钟

插槽的作用

在生活中,拿我们的电脑举例,它有很多接口,比如说USB接口,耳机接口等等。这些接口插槽的作用就是为了让电脑更具拓展性。比如我们能通过USB链接鼠标,操作我们的电脑,能连接u盘,存储数据....我们的slot同样是这个作用。他也是为了让我们的组件更具拓展性,用户能自定义组件的某些内容。使得组件适用于各种各样的环境。
example:移动端的导航栏 移动端的导航栏明显不是一摸一样的,在不同的页面都会存在细微的差别。但是他们大体结构上完全一致。如果我们将每个页面的导航栏封装成一个独立的组件,又显得很浪费。感觉没有必要。那我们该怎么做呢?

  • 第一步,求同,把他们相同的部分封装在组件中。
  • 第二步,存异,把他们在各个页面显示不同内容的部分设置成插槽的形式,使得在不同页面可以自定义显示的内容。

这些大概就是我们插槽的作用所在。

插槽的基本使用

从需求出发,比如说这里我有三个相同的组件,每个组件显示的内容略有不同。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/vue.js"></script>
</head>
<body>
    <div id="app">
        <cpn></cpn>
        <cpn></cpn>
        <cpn></cpn>
    </div>
    <script>
        const app = new Vue({
            el:"#app",
            components:{
                cpn:{
                    template:`<div>
                                    <h1>我是子组件</h1>
                                    <slot></slot>//子组件预留插槽
                                </div>`,
                }
            }
        })
    </script>
</body>
</html>

第一个组件我想显示一个button,第二个组件我想显示一段文本,第三个组件我想显示一张图片。那么问题来了我们在哪添加自定义内容呢?
答案是使用子组件时,自定义内容添加在子组件标签内部。比如这里我想实现需求应该是这样做:

 <div id="app">
        <cpn>
            <button>点击</button>
        </cpn>
        <cpn>
            <p>
                我是猪
            </p>
        </cpn>
        <cpn>
            <img src="https://cdn.chime.me/image/fs/sitebuild/202091/0/original_1904bfb4-855c-4e3a-9c11-d3c088a76eee.jpeg" alt="">
        </cpn>
    </div>

效果图: 如果你没有添加自定义内容,那么插槽就不显示。

插槽默认值

当你某个插槽绝大多数情况都是显示某个内容,只有极个别情况显示其他内容。那这个时候就可以给插槽设置默认值了。就是你在设置插槽的时候在slot标签内部写的内容就是该插槽的默认值。

 components:{
                cpn:{
                    template:`<div>
                                    <h1>我是子组件</h1>
                                    <slot>
                                    	<h1>woshizhu</h1>//这个地方就是该插槽的默认值。
                                    </slot>//子组件预留插槽
                                </div>`,
                }
            }

当你没有设置自定义内容的时候,插槽就显示自己的默认值。

具名插槽的使用

一个组件可能不只有一个插槽,比如说我这里有三个插槽:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/vue.js"></script>
</head>
<body>
    <div id="app">
        <cpn>
           
        </cpn>
        
    </div>
    <script>
        const app = new Vue({
            el:"#app",
            components:{
                cpn:{
                    template:`<div>
                                    <h1>我是子组件</h1>
                                    <slot><h1>左边</h1></slot>
                                    <slot><h1>中间</h1></slot>
                                    <slot><h1>右边</h1></slot>
                                </div>`,
                }
            }
        })
    </script>
</body>
</html>

显示效果: 我如果想给中间的插槽自定义内容该怎么办呢?
如果我还是原来设置自定义内容的方法显然是不行的。

 <div id="app">
        <cpn>
           <h1>我要去中间</h1>//定义的是所有不具名插槽的显示内容
        </cpn>
        
    </div>

显示效果:
显然,我这种设置自定义内容的方式,是指定所有不具名插槽的显示内容。
那如果我要给某个特定的插槽设置特定的内容该怎么做呢?

  • 通过name属性给插槽起名字(具名插槽)<slot name="mid"></slot>
  • 通过slot属性给自定义内容指定插槽
 <div id="app">
        <cpn>
           <h1 slot="mid">我要去中间</h1>
        </cpn>
        
    </div>

效果图:

注意

不具名的插槽其实都有一个隐式的名字:default

具名插槽的更新

在vue 2.6之后,slot属性被废弃了,而是用一种新的形式代替它的功能:使用v-slot指令。这个指令只能使用在template元素上。(这点不同于slot属性可以在任意元素上使用,也就导致具名插槽的内容的根元素只能是template)。那上面的代码就应该这样来实现。

 <div id="app">
        <cpn>
           <template v-slot:mid>
           		<h1>我要去中间</h1>
           </template>
        </cpn>
        
    </div>

任何被template包裹的内容都会传递给相对应的具名插槽,而那些没有被template包裹的内容会被当做不具名插槽的内容。
如果想明确给不具名插槽指定内容可以这样写:

  <cpn>
           <template v-slot:default>
           		不具名插槽的内容
           </template>
        </cpn>

因为不具名插槽的名字就是default。

编译作用域

我们还是从一段代码出发

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/vue.js"></script>
</head>
<body>
    <div id="app">
        <cpn>
            {{message}}
        </cpn>
    </div>
    <script>
        const app = new Vue({
            el:"#app",
            data:{
                message:"父组件"
            },
            components:{
                cpn:{
                    template:`<h1>
                        <slot></slot>
                    </h1>`,
                    data(){
                        return {
                            message:"子组件"
                        }
                    }
                }
            }
        })
    </script>
</body>
</html>

简单描述一下,这里就两个组件:父子组件。并且父子组件的data中都含有一个变量:message.那么如果我们设置子组件的内容的时候,也就是定义插槽内容的时候:<cpn> {{message}}</cpn>,在子组件的标签内我们使用的message到底是父组件的message,还是子组件的message呢?
答案是父组件的message. 这里会有两个误区:

  1. 逻辑思维上的误区:我在子组件标签内写代码,那当然是优先在子组件中找对应数据(错)
  2. 主观臆断上的误区:由于我设置的内容本质上是子组件template的一部分,所以我使用的数据当然是子组件内部。前半句对,后半句错。

这里涉及到编译的作用域的问题
我个人认为这个是一个特别简单的问题,我的理解就是数据在在哪个组件的模板出现,那么一定是从这个组件的数据中找。现在是根实例的模板,所以只会从根实例的数据中找。如果是在子组件的模板中用到该数据当然是在子组件的数据中找。

作用域插槽

很多情况下,我们还是希望定义插槽的内容的时候能使用子组件中的数据。(毕竟插槽的内容就是子组件的一部分)
**那我们怎么设置插槽内容的时候使用子组件的数据呢? **

  1. 在slot标签内我们应该把想要在插槽中使用的数据传递出来。
    怎么传递出来呢?通过自定义属性传递出来。那就说明你这个插槽就有了这个自定义属性。
  2. 在设置slot内容的时候,在父元素中调用slot-scope属性就可以取得这个取得自定义属性组成的对象(属性名作为对象的属性名,属性值作为对象的属性值)
    并且通过我实验的代码可知,取得的自定义属性组成的对象是和slot名对应的。也就是说所有不具名插槽定义的自定义属性(也就是名为default的slot)只能在设置不具名插槽的内容时,你可以通过slot-scope访问到,设置具名插槽的内容时,通过slot-scope时访问不到的。。。。算了好乱还是举例子说明吧。。。
    example:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="./js/vue.js"></script>
</head>
<body>
    <div id="app">
        <cpn>
            <div slot-scope="props">
                名为default插槽的内容
                {{props.lefta}}
                {{props.rightc}}
        </div>
        <div slot="right" slot-scope="rightprops">
            名为right插槽的内容
            {{rightprops.rightc}}
            {{rightprops.lefta}}
        </div>
        </cpn>
        
    </div>
    <script>
        const app = new Vue({
            el:"#app",
            data:{
                a:''
            },
            components:{
                cpn:{
                    template:`<div>
                    	
                        <slot  lefta="lefta"></slot>//default插槽自定义了lefta属性
                        <slot name="right"  rightc="rightc" ></slot>//right插槽自定义了rightc属性
                    </div>`
                }
            }
        })
    </script>
</body>
</html>

就是说,default插槽自定义了lefta属性,而right插槽自定义了rightc属性,而当你在设置default插槽的内容的时候,slot-scope对应的对象里面的属性只包含default插槽自定义的属性,而不会包含right插槽自定义的属性。所以props.lefta存在。 props.rightc不存在。同理,设置right插槽的内容的时候,slot-scope对应的对象肯定也不含lefta属性。

作用域插槽的更新

实际上,在Vue2.6以上之后,slot和slot-scope属性就被废除了,那么怎么实现在插槽中访问子组件的数据呢?其实是类似的只是不用slot-scope属性了。

//原来的写法
 <cpn>
            <div slot-scope="props">
                名为default插槽的内容
                {{props.lefta}}
                {{props.rightc}}
        </div>
        <div slot="right" slot-scope="rightprops">
            名为right插槽的内容
            {{rightprops.rightc}}
            {{rightprops.lefta}}
        </div>
        </cpn>
 //现在的写法
 <cpn>
 <template v-slot:default="props"> 
            <div>
                名为default插槽的内容
                {{props.lefta}}
                {{props.rightc}}
            </div>
        </template>
        <template v-slot:right="rightprops"> 
            <div>
                名为right插槽的内容
              {{rightprops.rightc}}
               {{rightprops.lefta}}
            </div>
        </template>
        </cpn>

其实还是挺简单的。

v-slot:指令的简写

很多指令都有语法糖,这个也不例外,这个的语法糖就是#
上文的改造

 <cpn>
 <template #default="props"> 
            <div>
                名为default插槽的内容
                {{props.lefta}}
                {{props.rightc}}
            </div>
        </template>
        <template #right="rightprops"> 
            <div>
                名为right插槽的内容
              {{rightprops.rightc}}
               {{rightprops.lefta}}
            </div>
        </template>
        </cpn>

解构赋值和作用域插槽

其实也挺简单的,例如上文的#default可以理解成是自定义属性组成的对象,你可以把他直接赋值给props,那么props就是这个对象。通过它访问里面的属性。也可以直接进行解构赋值({lefta:lefta}={lefta}),取得里面的属性值。(获得变量lefta值为字符串"lefta")

//进行解构赋值
 <template #default="{lefta}"> 
            <div>
               {{lefta}}
            </div>
        </template>