一. 前言


Vue 官方文档中在 “组件基础” 内容中提到,组件可以通过插槽分发内容,那么插槽是怎么使用的呢?它要解决什么场景的问题呢?

我们在构建页面过程中,一般会把比较多的公共的部分抽取出来作为一个单独的组件,但是在实际使用这个组件的时候,却又不能完全的满足需求,我希望在这个组件中添加一点东西,这时候我们就需要用到插槽来分发内容。

注意:以下的内容是基于 vue 版本 2.6.0 起

在 2.6.0 中,具名插槽作用于插槽 引入了一个新的统一语法( v-slot 指令)。它取代了 slotslot-scope ,并且现在网上都说的是一些老版本的内容,官方文档不太容易理解,所以就整理了一下关于插槽(slot)使用的文章。

二. 插槽是什么


slot 通俗的理解就是 “占坑”,在组件模板中占好了位置,当使用该组件标签的时候,组件标签里面的内容就会自动填充(替换组件模板中的 slot 位置),并且可以作为承载分发内容的出口。

下面看一个例子:

定义两个组件 father.vueson.vue

然后在 fast.vue 组件中引用 son.vue 组件

<!-- fasther.vue -->
<template>
  <div>
    <div>大家好我是父组件</div>
    <son>
      <p>测试一下吧内容写在这里了能否显示</p>
    </son>
  </div>
</template>

<script>
  import son from './son';
  export default {
    components: {
      son
    }
  }
</script>

<style></style>
<!-- son.vue -->
<template>
  <div>
    <div>我是子组件</div>
  </div>
</template>

<script></script>

<style></style>

运行代码,发现,最终渲染的效果是:

大家好我是父组件
我是子组件

那如果我想实现显示父组件中 p 标签的内容怎么办?

修改子组件:son.vue

<template>
  <div>
    <div>我是子组件</div>
    <p>现在测试一下 slot</p>
    <slot></slot>
  </div>
</template>

<script></script>

<style></style>

运行代码,可以看到以下效果:

大家好我是父组件
我是子组件
现在测试一下 slot
测试一下吧内容写在这里了能否显示

官方文档对于插槽的应用场景是这样描述的:

我们经常需要向一个组件传递内容,Vue 自定义的 <slot> 元素让这变得非常简单。只要在需要的地方加入插槽就行了 ———— 就这么简单!

结合上面的例子来理解就是这样的:

  1. 父组件在引用子组件时希望向子组件传递模板内容 <p>测试一下吧,内容写在这里了能否显示</p>
  2. 字符键让父组件传过来的模板内容在所在的位置显示。
  3. 子组件中的 <slot> 就是一个槽,可以接受父组件传过来的模板内容,<slot> 元素自身将被替换。
  4. <myslot></myslot> 组件没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

三. 插槽的作用


让用户可以拓展组件,去更好的复用组件和对其做定制化处理。

四. 插槽的分类


1. 默认插槽

定义两个组件 fasther.vueson.vue

然后在 fasther.vue 组件中引用 son.vue 组件

插槽内容可以包含普通文本

<!-- fasther.vue -->
<template>
  <son>
    Hello Word
  </son>
</template>
<!-- son.vue -->
<template>
  <a href="#">
	<slot></slot>
  </a>
</template>

当组件渲染的时候,<slot></slot> 会被替换为 Hello Word

插槽内也可以包含任何模板代码,包括 HTML

在你的 index.html 引入 Font Awesome 图标的样式就直接可以用那里面的图标了
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">

<!-- fasther.vue -->
<template>
  <son>
    <!-- 添加一个 Font Awesome 图标 -->
    <span class="fa fa-user"></span>
    Hello Word
  </son>
</template>

插槽内添加其他组件

<!-- fasther.vue -->
<son>
    <!-- 添加一个图标的组件 -->
    <font-awesome-icon></font-awesome-icon>
    Hello Word
</son>

如果 <son> 中没有包含一个<slot>元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

在插槽中使用数据

插槽跟模板其他地方一样都可以访问相同的实例属性(也就是相同的"作用域"),而不能访问<son>的作用域

<!-- fasther.vue -->
<template>
  <son>
    // 插槽可以获取到 fasther 组件里的内容
	Hello {{enhavo}}
  </son>
</template>

<script>
  import son from './son';
  export default {
    data(){
	  return{
        enhavo:'word'
	  }
    }  
  }
</script>
<!-- fasther.vue -->
<template>
  // 这里是获取不到 name 的,因为这个值是传给 <son> 的
  <son name='you'>
    Hello {{name}}
  </son>
</template>

规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

2. 后备内容(默认内容)插槽

有时候我们需要给插槽设置一个具体的默认内容,当别的组件没有给你内容的时候,那么默认的内容就会被渲染

<!-- son.vue -->
<template>
  // 在 slot 插槽里设置默认内容 Submit
  <button type="submit">
    <slot>Submit</slot>
  </button>
</template>

fasther.vue 里直接使用 son.vue 如下:

<!-- fasther.vue -->
<template>
 <son></son>
</template>

那么最后设置的默认内容 Submit 将会被渲染

<button>
   Submit
</button>

假如我们提供内容呢?

<!-- fasther.vue -->
<template>
  <son>按钮</son>
</template>

那么这个提供的内容将会替代默认的内容被渲染出来

<button>
   按钮
</button>

3. 具名插槽

有时候我们一个组件里需要多个插槽,那怎么办呢?

对于这样的情况,<slot> 元素有一个特殊的 attribute(属性):name,这个 attribute(属性)可以用来定义额外的插槽:

<template>
  <div class="container">
    <header>
      <!-- 我们希望把页头放这里 -->
    </header>
  
    <main>
      <!-- 我们希望把主要内容放这里 -->
    </main>
  
    <footer>
      <!-- 我们希望把页脚放这里 -->
    </footer>
  </div>
</template>

这时候,我们就可以使用 name 这个属性来定义额外的插槽:

<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>

    <main>
      <slot></slot>
    </main>

    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

如果一个 <slot> 不带 name 属性的话,那么它的 name 默认为 default

父组件在向具名插槽提供内容的时候,我们可以在 <template> 元素上使用 v-slot 指令,并以参数的形式提供其名称

<template>
  <son>
    <div>大家好我是父组件</div>
    <template v-slot:header>
      <h1>Here might be a page title</h1>
    </template>

    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template v-slot:footer>
      <p>Here's footer info</p>
    </template>
  </son>
</template>

<script>
  import son from './son';
  export default {
    components: {
      son
    }
  }
</script>

<style></style>

最终的渲染结果可以看到:

Here might be a page title
大家好我是父组件
A paragraph for the main content.

And another one.

Here's footer info

现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容。

当然,如果你希望更明确一点的话,那就把主体内容那个插槽里设置 name="default",然后把上面的内容包裹起来

<template v-slot:default>
	<p>A paragraph for the main content.</p>
	<p>And another one.</p>
</template>

同样是传递给子组件中不带 name<slot>

注意:

  • v-slot 只能添加在 <template> 上。(只有一种例外情况,下面会说)
  • 具名插槽在书写的时候可以使用缩写,v-slot 用 # 来代替

4. 作用域插槽

这里主要解决的是 父组件在向子组件插槽传递模板内容时存在访问子组件数据的问题

上面已经说了,插槽跟模板其他地方一样都可以访问相同的实例属性(也就是相同的"作用域"),而不能访问 <son> 的作用域

那如果想访问 <son> 作用域该怎么办呢?

我们把需要传递的内容绑到 <slot> 上,然后在父组件中用 v-slot 设置一个值来定义我们提供插槽的名字:

<!-- son.vue -->
<template>
  <div>
    <!-- 设置默认值:{{user.lastName}} 获取 Jun -->
   	<!-- 如果 fasther.vue 中给这个插槽值的话,则不显示 Jun -->
    <!-- 设置一个 usertext 然后把 user 绑到设置的 usertext 上 -->
    <slot v-bind:usertext="user">{{user.lastName}}</slot>
  </div>
</template>

<script>
  export default {
    // 定义内容
    data() {
      return{
        user: {
          firastNmae: "Fan",
          lastNmae: "Jun"
        }
      }
    }
  }
</script>

然后在 fasther.vue 中接收传过来的值:

<!-- fasther.vue -->
<template>
  <div>
    <son v-slot:default="slotProps">
      {{slotProps.usertext.firstName}}
    </son>
  </div>
</template>

这样就可以获得 son.vue 组件传过来的值了

绑定在 <slot> 元素上的特性被称为插槽 prop。在父组件中,我们可以用 v-slot 设置一个值来定义我们提供的插槽 prop 的名字,然后直接使用就好了

独占默认插槽的缩写语法

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上

这样写法还可以更简单,因为不带参数的 v-slot 就被假定为默认插槽,所以上面的代码还可以简化:

<template>
  <div>
    <!-- 可以把 :default 去掉,仅限于默认插槽 -->
    <son v-slot="slotProps">
      {{slotProps.usertext.firstName}}
    </son>
  </div>
</template>

注: 默认插槽 的缩写语法不能和 具名插槽 混用,因为它会导致作用域不明确

<template>
  <div>
    <!-- 可以把 :default 去掉,仅限于默认插槽 -->
    <son v-slot="slotProps">
      {{slotProps.usertext.firstName}}
      <!-- 无效,会警告 -->
      <template v-slot:other="otherSlotProps">
        slotProps is NOT available here
      </template>
    </son>
  </div>
</template>

只要出现多个插槽,始终要为所有的插槽使用完整的基于 <template> 的语法:

<template>
  <son>
    <template v-slot:default="slotProps">
      {{ slotProps.user.firstName }}
    </template>

    <template v-slot:other="otherSlotProps">
      ...
    </template>
  </son>
</template>

解构插槽 Prop

因为 作用域插槽 的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JS 表达式

所以本来是这样写的:

<template>
  <div>
    <son v-slot="slotProps">
      {{slotProps.usertext.firstName}}
    </son>
  </div>
</template>

还可以这样写:

<template>
  <div>
    <son v-slot={usertext}>
      {{usertext.firstName}}
    </son>
  </div>
</template>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,

例如可以将 usertext 重命名为 person:

<template>
  <div>
    <son v-slot={usertext:person}>
      {{person.firstName}}
    </son>
  </div>
</template>

甚至可以定义 后备内容(默认内容),用于插槽没有值时可以使用默认内容的情形:

<template>
  <div>
    <son v-slot="{usertext={firstName:'Yang'}}">
      {{usertext.firstName}}
    </son>
  </div>
</template>

5. 动态插槽名(2.6.0新增)

动态指令参数(需要自己了解)也可以用在 v-slot 上,来定义动态的插槽名:

<template>
  <base-layout>
    <template v-slot:[dynamicSlotName]>
      ...
    </template>
  </base-layout>
</template>

6. 具名插槽的缩写(2.6.0新增)

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

原来是这样写的:

<template>
  <div>
    <template v-slot:header>
      <h1>Here might be a page title</h1>
    </template>

    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template v-slot:footer>
      <p>Here some contact info</p>
    </template>
  </div>  
</template>

现在可以这样写:

<template>
  <div>
    <template #header>
      <h1>Here might be a page title</h1>
    </template>

    <p>A paragraph for the main content.</p>
    <p>And another one.</p>

    <template #footer>
      <p>Here some contact info</p>
    </template>
  </div>
</template>

注:该指令和其他指令一样,只在其有参数的时候才可用

下面的书写形式是错误的:

<template>
  <son #="{ usertext }">
    {{ usertext.firstName }}
  </son>
</template>

如果希望使用缩写的话,必须始终以明确插槽名取而代之

<template>
  <son #default="{ usertext }">
    {{ usertext.firstName }}
  </son>
</template>

7. 其他示例

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。 这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。

例如,我们要实现一个 <todo-list> 组件,它是一个列表且包含布局和过滤逻辑:

<template>
  <ul>
    <li
      v-for="todo in filteredTodos"
      v-bind:key="todo.id"
    >
      {{ todo.text }}
    </li>
  </ul>
</template>

我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:

<templaet>
  <ul>
    <li
      v-for="todo in filteredTodos"
      v-bind:key="todo.id"
    >
      <!--
      我们为每个 todo 准备了一个插槽,
      将 `todo` 对象作为一个插槽的 prop 传入。
      -->
      <slot name="todo" v-bind:todo="todo">
        <!-- 后备内容 -->
        {{ todo.text }}
      </slot>
    </li>
  </ul>
</template>

现在当我们使用 <todo-list> 组件的时候,我们可以选择为 todo 定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取数据:

<template>
  <todo-list v-bind:todos="todos">
    <template v-slot:todo="{ todo }">
      <span v-if="todo.isComplete">✓</span>
      {{ todo.text }}
    </template>
  </todo-list>
</template>

至于那些废弃了的 slotslot-scope 特性,这里就不在阐述了,如果有兴趣了解的话,请参考官方文档