首页 > 编程笔记

Vue slot(插槽)详解

在 Vue 中,组件是当作自定义的 HTML 元素来使用的,其元素可以包括属性和内容,通过组件定义的 prop 来接收属性值,那么组件的内容怎么实现呢?可以使用插槽(slot 元素)来解决。

插槽的基本用法

下面定义一个组件:
vm.component('page', {
    template:'<div><slot></slot></div>'
});
在 page 组件中,div 元素内容定义了 slot 元素,可以把它理解为占位符。

在 Vue 实例中使用这个组件:
<div id="app">
    <page>如今直上银河去,同到牵牛织女家。</page>
</div>
page 元素给出了内容,在渲染组件时,这个内容会置换组件内部的 <slot> 元素。

在 Chrome 浏览器中运行程序,渲染的结果如下图所示。


图 1 插槽的基本用法

如果 page 组件中没有 slot 元素,<page> 元素中的内容将不会渲染到页面。

编译作用域

当想通过插槽向组件传递动态数据时,例如:
<page>欢迎来到{{name}}的官网</page>
代码中,name 属性是在父组件作用域下解析的,而不是 page 组件的作用域。在 page 组件中定义的属性,在父组件中是访问不到的,这就是编译作用域。

有一条规则要记住:父组件模板中的所有内容都是在父级作用域中编译的,子组件模板中的所有内容都是在子作用域中编译的。

默认内容

有时为一个插槽设置默认内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:
<button type="submit">
    <slot></slot>
</button>
如果希望这个 <button> 内绝大多数情况下都渲染文本“Submit”,可以将“Submit”作为默认内容放在 <slot> 标签内:
<button type="submit">
    <slot>Submit</slot>
</button>
现在在一个父组件中使用 <submit-button>,并且不提供任何插槽内容:
<submit-button></submit-button>
默认内容“Submit”将会被渲染:
<button type="submit">
    Submit
</button>
但是如果提供内容:
<submit-button>
   提交
</submit-button>
则这个提供的内容将会替换掉默认值 Submit,渲染如下:
<button type="submit">
    提交
</button>
【实例】设置插槽的默认内容。
<div id="app">
    <page>流年莫虚掷,华发不相容。</page>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
    //创建一个应用程序实例
    const vm= Vue.createApp({ });
    vm.component('page', {
        template:`<button type="submit">
                    <slot>Submit</slot>
                  </button>
                  '
    });
    //在指定的DOM元素上装载应用程序实例的根组件
    vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,渲染的结果如下图所示。


图 2 设置插槽的默认内容

命名插槽

在组件开发中,有时需要使用多个插槽。例如对于一个带有如下模板的 <page-layout> 组件:
<div class="container">
    <header>
        <!-- 我们希望把页头放这里 -->
    </header>
    <main>
        <!-- 我们希望把主要内容放这里 -->
    </main>
    <footer>
        <!-- 我们希望把页脚放这里 -->
    </footer>
</div>
对于这样的情况,<slot> 元素有一个特殊的特性 name,它用来命名插槽。因此,可以定义多个名字不同的插槽,例如下面的代码:
<div class="container">
    <header>
        <slot name="header"></slot>
    </header>
    <main>
        <slot></slot>
    </main>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>
一个不带 name 的 <slot> 元素,它有默认的名字“default”。

在向命名插槽提供内容的时候,可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<page-layout>
    <template v-slot:header>
        <h1>这里有一个页面标题</h1>
    </template>
    <p>这里有一段主要内容</p>
    <p>和另一段主要内容</p>
    <template v-slot:footer>
        <p>这是一些联系方式</p>
    </template>
</page-layout>
现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。

然而,如果希望更明确一些,仍然可以在一个 <template> 中包裹默认命名插槽的内容:
<page-layout>
    <template v-slot:header>
        <h1>这里有一个页面标题</h1>
    </template>
    <template v-slot:default>
        <p>这里有一段主要内容</p>
        <p>和另一段主要内容</p>
    </template>
    <template v-slot:footer>
        <<p>这是一些联系方式</p>
    </template>
</page-layout>
上面两种写法都会渲染出如下代码:
<div class="container">
    <header>
        <h3>这里有一个页面标题</h3>
    </header>
    <main>
        <p>这里有一段主要内容</p>
        <p>和另一段主要内容</p>
    </main>
    <footer>
        <p>这是一些联系方式</p>
    </footer>
</div>
【实例】命名插槽。
<div id="app">
    <page-layout>
        <template v-slot:header>
            <h2 align='center'>书河上亭壁</h2>
        </template>
        <template v-slot:main>
            <h3>岸阔樯稀波渺茫,独凭危槛思何长。</h3>
            <h3>萧萧远树疏林外,一半秋山带夕阳。</h3>
        </template>
        <template v-slot:footer>
            <p align='right'>经典古诗</p>
        </template>
    </page-layout>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
    //创建一个应用程序实例
    const vm= Vue.createApp({ });
    vm.component('page-layout', {
        template:`
            <div class="container">
                <header>
                    <slot name="header"></slot>
                </header>
                <main>
                    <slot name="main"></slot>
                </main>
                <footer>
                    <slot name="footer"></slot>
                </footer>
            </div>
        `
    });
    //在指定的DOM元素上装载应用程序实例的根组件
    vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 3 命名插槽

与 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容(v-slot:)替换为字符 #。例如下面的代码:
<page-layout>
    <template #header>
        <h1>这里有一个页面标题</h1>
    </template>
    <template #main>
        <p>这里有一段主要内容</p>
        <p>和另一段主要内容</p>
    </template>
    <template #footer>
        <<p>这是一些联系方式</p>
    </template>
</page-layout>

作用域插槽

在父级作用域下,在插槽的内容中是无法访问子组件的数据属性的,但有时需要在父级的插槽内容中访问子组件的属性,我们可以在子组件的 <slot> 元素上使用 v-bind 指令绑定一个 prop 属性。看下面的组件代码:
vm.component('page-layout', {
    data:function(){
      return{
          info:{
              name:'小明',
              age:18,
              sex:"男"
          }
      }
    },
    template:`
        <button>
            <slot v-bind:values="info">
                {{info.name}}
            </slot>
        </button>
    `
});
这个按钮可以显示 info 对象中的任意一个,为了让父组件可以访问 info 对象,在 <slot> 元素上使用 v-bind 指令绑定一个 values 属性,称为插槽 prop,这个 prop 不需要在 props 选项中声明。

在父级作用域下使用该组件时,可以给 v-slot 指令一个值来定义组件提供的插槽 prop 的名字。代码如下:
<page-layout>
    <template v-slot:default="slotProps">
        {{slotProps.values.name}}
    </template>
</page-layout>
因为 <page-layout> 组件内的插槽是默认插槽,所以这里使用其默认的名字 default,然后给出一个名字 slotProps,这个名字可以随便取,代表的是包含组件内所有插槽 prop 的一个对象,然后就可以在父组件中利用这个对象访问子组件的插槽 prop,prop 是绑定到 info 数据属性上的,所以可以进一步访问 info 的内容。示例代码如下。

【实例】访问插槽的内容。
<div id="app">
    <page-layout>
        <template v-slot:default="slotProps">
            {{slotProps.values.city}}
        </template>
    </page-layout>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
    //创建一个应用程序实例
    const vm= Vue.createApp({ });
    vm.component('page-layout', {
        data:function(){
          return{
              info:{
                  name:'苹果',
                  price:8.86,
                  city:"深圳"
              }
          }
        },
        template:`
            <button>
                <slot v-bind:values="info">
                    {{info.city}}
                </slot>
            </button>
        `
    });
    //在指定的DOM元素上装载应用程序实例的根组件
    vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 4 命名插槽

解构插槽prop

作用域插槽的内部工作原理是将插槽内容传入函数的单个参数中:
function (slotProps) {
    // 插槽内容
}
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下(单文件组件或现代浏览器),也可以使用 ES 6 解构来传入具体的插槽 prop,示例代码如下:
<current-verse v-slot="{ verse }">
    {{ verse.firstContent }}
</current-user>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其他可能,例如将 verse 重命名为 poetry:
<current-verse v-slot="{ verse: poetry }">
    {{ poetry.firstContent }}
</current-verse>
甚至可以定义默认的内容,用于插槽 prop 是 undefined 的情形:
<current-verse v-slot="{ verser = { firstContent: '古诗' } }">
    {{ verse.Content}}
</current-verser>
【实例】解构插槽prop。
<div id="app">
    <current-verse>
        <template v-slot="{verse:poetry}">
            {{poetry.firstContent }}
        </template>
    </current-verse>
</div>
<!--引入Vue文件-->
<script src="https://unpkg.com/vue@next"></script>
<script>
    //创建一个应用程序实例
    const vm= Vue.createApp({ });
    vm.component('currentVerse', {
        template: '
<span><slot :verse="verse">{{ verse.lastContent }}</slot></span>',
        data:function(){
            return {
                verse: {
                    firstContent: '此心随去马,迢递过千峰。',
                   secondContent: '野渡波摇月,空城雨翳钟。'
                }
            }
        }
    });
    //在指定的DOM元素上装载应用程序实例的根组件
    vm.mount('#app');
</script>
在 Chrome 浏览器中运行程序,效果如下图所示。


图 5 解构插槽prop

推荐阅读