组件插槽

在使用 Vue.js 的过程中,有时需要在组件中预先设置一部分内容,但是这部分内容并不确定,而是依赖于父组件的设置,这种情况俗称为 “占坑”。在 Vue.js 中有一个专有名词 slot,或者是组件 <slot>,翻译成中文叫作 “插槽”。如果用生活中的物体形容插槽,它就是一个可以插入插销的槽口,比如插座的插孔。如果用专用术语来理解,插槽是组件的一块 HTML 模板,这块模板显示不显示以及怎样显示由父组件来决定。插槽主要分为 默认插槽具名插槽动态插槽名插槽后备作用域插槽

默认插槽

先来看一个简单的例子,如示例代码 3-3-1 所示。

示例代码 3-3-1 默认插槽的示例
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <title>vue 插槽</title>
    <script src="https://unpkg.com/vue@3.2.28/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
    <children>
        <span>abc</span>
    </children>
</div>

<script type="text/javascript">
    Vue.createApp({
        components: {
            children: {
                template: "<div id='children'>" +
                               "<slot></slot>"+
                          "</div>"
            }
        }
    }).mount("#app")
</script>
</body>
</html>

上面的代码是完整的 HTML 代码,可以直接在浏览器中运行,本节后面的代码都会以此为基础。下面定义一个子组件 childrenchildrentemplate 设置了插槽,同时在根实例中使用了 children 组件,当程序运行时,#appHTML 内容会被替换成:

<div id="app">
    <div id="children">
        <span>abc</span>
    </div>
</div>

插槽理解起来很简单,<slot></slot> 预先占了 “坑”,未被父元素导入时,并不确定这里要显示什么。当这段代码运行时,这里的内容就被替换成了 <span>abc</span>,这就是一个简单的默认插槽。

具名插槽

有时需要多个插槽,需要标识出每个插槽替换哪部分内容,给每个插槽指定名字(name),这就是具名插槽,如示例代码3-3-2所示。

Vue.createApp({
    components: {
        children: {
            template:
                "<div id='children'>"+
                "<slot name='one'></slot>"+
                "<slot name='two'></slot>"+
                "</div>"
        }
    }
});

在向具名插槽提供内容时,可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称,如示例代码 3-3-3 所示。

示例代码3-3-3 具名插槽的使用
<div id="app">
    <children>
        <template v-slot:one><p>Hello One Slot!</p></template>
        <template v-slot:two><p>Hello Two Slot!</p></template>
    </children>
</div>

当然,具名插槽和默认插槽也可以一起使用,如果有些内容没有被包裹在带有 v-slot<template> 中,这些内容就会被视为默认插槽的内容,如示例代码 3-3-4 所示。

<div id="app">
    <children>
        <template v-slot:one><p>Hello One Slot!</p></template>
        <template v-slot:two><p>Hello Two Slot!</p></template>
        <p>Hello Default Slot!</p>
    </children>
</div>

<script type="text/javascript">
    Vue.createApp({
        components: {
            children: {
                template: "<div id='children'>" +
                               "<slot name='one'></slot>" +
                               "<slot name='two'></slot>"
                               "<slot></slot>"+
                          "</div>"
            }
        }
    }).mount("#app")

<slot></slot> 会被替换成 <p>Hello Default Slot!</p>,当然也可以明确给 <template> 指定 default 名字来显示默认插槽。代码如下:

<template v-slot:default>
    <p>Hello Default Slot!</p>
</template>

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 “#”。例如 v-slot:one 可以被重写为 #one,代码如下:

<template #one><p>Hello One Slot!</p></template>
<template #two><p>Hello Two Slot!</p></template>

这样看起来更加简单便捷。

动态插槽名

在之前的章节中讲解过指令的动态参数,也就是用方括号括起来的 JavaScript 表达式作为一个 v-slot 指令的参数,因此我们可以把需要导入的插值 name 通过写在组件的 data 属性中来动态设置插槽名,如示例代码 3-3-5 所示。

<div id="app">
    <children>
        <template v-slot:[slotname]>
            <p>Hello One Slot!</p>
        </template>
    </children>
</div>
Vue.createApp({
    data(){
        return {
            slotname:'one'
        }
    },
    components: {
        children: {
            template:
                "<div id='children'>"+
                "<slot name='one'></slot>"+
                "</div>"
        }
    }
}).mount("#app")

需要注意的是,在指定动态参数时,slotname 要保持全部小写,其中的原因在 2.2.2 节讲解过,这里不再赘述。

插槽后备

有时为一个插槽设置具体的后备(也就是默认的)内容是很有用的,它只会在没有提供内容的时候被渲染。可以把后备理解成写在 <slot></slot> 中的内容,例如在一个自定义的 text 组件中:

const text = {
    template: '<p><slot></slot></p>',
}

若希望这个 text 组件内在绝大多数情况下都渲染文本 “default content”,为了将 “default content” 作为后备内容,可以将它放在 <slot> 标签中:

const text = {
    template: '<p><slot>default content</slot></p>',
}

倘若在一个父级组件中使用 text 组件,并且不提供任何插槽内容,后备内容 “default content” 将会被渲染,代码如下:

<text></text>
// 渲染为
<p>
default content
</p>

如果我们提供了内容,则提供的内容将会被渲染从而取代后备内容,代码如下:

<text>Hello Text!</text>
// 渲染为
<p>
Hello Text!
</p>

在大多数场合下,插槽后备要结合作用域插槽来使用。下面来讲解一下作用域插槽。

作用域插槽

作用域插槽比之前两个插槽相对要复杂一些。虽然 Vue 官方称它为作用域插槽,实际上我们可以把它理解成 “带数据的插槽”。

例如,有时让插槽能够访问当前子组件中才有的数据是很有用的,以此来定义自己的渲染逻辑。例如上面的 text 组件,我们将 person.name 作为后备,代码如下:

const text = {
    data(){
        return {
            person: {
                age: 20,
                name: 'Jack'
            }
        }
    },
    template: '<p><slot>{{person.name}}</slot></p>',
}

插槽在当前的 text 组件中是可以正常使用的,但是有时我们需要将 person 数据带给使用 text 组件的父组件,以便让父组件也可以使用 person,可以进行如下设置,完整的示例代码如3-3-6所示。

<div id="app">
    <children>
        <template v-slot:default="slotProps">
            {{ slotProps.person.age }}
        </template>
    </children>
</div>
const text = {
    data(){
        return {
            person: {
                age: 20,
                name: 'Jack'
            }
        }
    },
    template: '<p><slot v-bind:person="person"></slot></p>',
}
Vue.createApp({
    components: {
        children: text
    }
}).mount("#app")

首先,在 <slot> 中使用 v-bind 来绑定 person 这个值,我们称这个值为插槽的 props,就是标识出这个插槽想要将 person 带给外部父组件使用。同时,在父组件上,可以给 <template> 设置 v-slot:default="slotProps",其中冒号后面的是参数,因为是默认插槽,没有指定名字,所以使用 default,而等号后面的值 slotProps 用于定义我们提供的插槽 props 的名字。在上面的代码段运行后,将会显示出 person.age 的值 20。另外,对于默认插槽还可以采用更简单的方法,直接省略 :default,代码如下:

<template v-slot="slotProps"></template>

上面我们采用的是没有名字的默认插槽,当然也可以使用多个作用域插槽来设置作用域,如示例代码 3-3-7 所示。

<p><slot :person="person">{{person.name}}</slot></p>
<p><slot name="one" :person="person">{{person.name}}</slot></p>
<p><slot name="two" :person="person">{{person.name}}</slot></p>
...
<template v-slot:default="slotProps">
{{ slotProps.person.age }}
</template>
<template v-slot:one="oneProps">
{{ oneProps.person.age }}
</template>
<template v-slot:two="twoProps">
{{ twoProps.person.age }}
</template>

注意,在多个具名插槽的作用域下,就不能使用极简的 v-slot="slotProps" 写法了,否则会导致作用域不明确。

解构插槽 props

在之前的章节中,我们了解了 ES 6 的解构,同样,插槽 props 也支持解构。作用域插槽的内部工作原理是将用户的插槽内容包括在一个传入单个参数的函数中,代码如下:

function (slotProps) {
    ...// 插槽内容
}

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式,就可以使用 ES 6 解构来传入具体的插槽 prop,如示例代码3-3-8所示。

<div id="app">
    <children>
        <template v-slot:default="{ person }">
            {{ person.age }}
        </template>
    </children>
</div>

<script type="text/javascript">
    const text = {
        data() {
            return {
                person: {
                    age: 20,
                    name: 'Jack'
                }
            }
        },
        template: '<p><slot :person="person"></slot></p>'
    }

    Vue.createApp({
        components: {
            children: text
        }
    }).mount("#app")
</script>

这样,可以省略掉 slotProps,直接获取到 person。另外,如果传递多个 props,也可以用一个 v-slot 来接收,如示例代码 3-3-9 所示。

示例代码3-3-9 解构插槽props 2
<div id="app">
    <children>
        <template v-slot:default="{ person,info }">
            {{ person.age }}
            {{ info.title }}
        </template>
    </children>
</div>

<script type="text/javascript">
    const text = {
        data() {
            return {
                person: {
                    age: 20,
                    name: 'Jack'
                },
                info: {
                    title: 'title'
                }
            }
        },
        template: '<p><slot :person="person" :info="info"></slot></p>'
    }

    Vue.createApp({
        components: {
            children: text
        }
    }).mount("#app")
</script>

至此,关于插槽相关的知识已经基本讲解完毕了。总结一下,就是把插槽理解成一个用来 “占坑” 的特殊组件,这样比较容易理解。