聊一聊Vue中的插槽

1,784 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

一、什么是Vue插槽

插槽是Vue中一个非常强大的功能,但是之前学的时候理解的不是很清楚,然后边学边忘。这篇博客主要是梳理学习插槽的内容。

我们先来说一说什么是插槽?

官网的解释是这样的

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

感觉不是特别好理解?没关系

二、梳理几个问题

1. 插槽的作用是什么?为什么要有插槽功能(解决了什么问题)?

定义两个组件 Index 和 Inner

// Index.vue

<template>
  <div class="hello">
    <Inner>
      <p>想把这个p标签插入到Inner</p>
    </Inner>
  </div>
</template>
// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <li><span>列表项02:</span>vue</li>
      <li><span>列表项03:</span>vue</li>
    </ul>
  </div>
</template>

如果现在我们想在<Inner><Inner>内放置一些内容,结果会是怎样?

输出内容还是在 <Inner> 组件中的内容,在 <Inner> 标签内写的内容没起作用。

image.png

这就是插槽出现的作用。

我们在 <Inner> 组件中加入 插槽 <slot></slot>

比如在列表项01和02之间加入,可以发现<Inner> 标签中的内容已经加入到<Inner>组件中了,而且正好在列表项01和02之间。

// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <slot></slot>
      <li><span>列表项02:</span>vue</li>
      <li><span>列表项03:</span>vue</li>
    </ul>
  </div>
</template>

image.png

到现在,我们知道了什么是插槽: 

插槽就是Vue实现的一套内容分发的API,将<slot></slot>元素作为承载分发内容的出口。

简单说,我们可以在组件标签内定义需要的内容,通过插槽加入到组件内部中。

组件内部的<slot></slot>元素就好像一个传送门,也就是所谓的槽,提供了内容的入口,也决定了内容的位置。 组件标签中定义的内容,通过这个“传送门”就可以加入到组件内部中。

2. 插槽中的“插件”在哪里?“槽”在哪里?

这个问题其实在上一个问题中就回答过了。

插槽中的“插件”就是组件标签中的内容。

插槽中的“槽”就是在组件中的<slot></slot>元素。

3. 为什么没有<slot></slot>元素,标签中的内容就无法加入?

你可以这样想一想, 组件中的内容都是排列好的。这个时候你要加入内容, 如果没有<slot></slot>元素,那这个内容应该放在什么位置呢?如果不加约定,可能会打乱原来排列好的内容。

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

Vue的作者可能就是这样设计的,在没有<slot></slot>元素的时候,就不渲染组件标签中的内容。

如果是你来设计,也许也可以设计成默认渲染在内容的最上面(当然这样可能会引入其他的问题)

所以提这个问题主要还是说不要太纠结,参考官方文档就可以。 (如果是真的有重要的考量也欢迎大佬在评论区提出)

三、插槽的分类

1. 默认插槽

上面介绍的例子中用的就是默认插槽,组件标签中的所有内容会通过默认插槽统一渲染。这里就不做过多介绍了。

2. 具名插槽

如果说要将不同的内容放在不同的位置,默认插槽就显得力不从心了。 这个时候就要用到具名插槽了。

顾名思义,具名插槽就是给插槽加一个名字。

我们给“插件”加一个名字,再给“槽”加上相同的名字,这样就可以一一对应了。

// Index.vue

<template>
  <div class="hello">
    <Inner>
      <template v-slot:apple><p>这个是apple</p></template>
      <template v-slot:banana><p>这个是banana</p></template>
      <template v-slot:orange><p>这个是orange</p></template>
      <template ><p>这个是没有指定具名插槽的内容</p></template>
      <template ><p>这一个也是没有指定具名插槽的内容</p></template>
    </Inner>
  </div>
</template>
// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <slot name="apple"></slot>
      <li><span>列表项02:</span>vue</li>
      <slot name="banana"></slot>
      <li><span>列表项03:</span>vue</li>
      <slot name="orange"></slot>

      <br>
      <p>这里是默认插槽</p>
      <slot></slot>
      
      <br>
      <p>这里是名为 default 的具名插槽</p>
      <slot name="default"></slot>
    </ul>
  </div>
</template>

image.png

从这里我们可以发现

  1. 具名插槽可以根据名称渲染对应的内容
  2. 没有定义名称的内容会被默认插槽统一渲染
  3. 默认插槽其实也是一个具名插槽,名称为 default

3. 作用域插槽

作用域插槽也是之前非常难理解的一个概念。

简单来说就是

插槽中内容的流动方向是从组件标签传到组件内部, 而作用域插槽则让作用域反向流动,从组件内部传到组件标签内。 可以在组件标签内访问组件内部的变量。

有时让插槽内容能够访问子组件中才有的数据是很有用的。

我们还是从需求来说起

我们想在插槽内容中访问apple的信息,然后报错了。

image.png

// Index.vue

<template>
  <div class="hello">
    <Inner>
      <template v-slot:apple><p>这个是apple,{{apple.msg}}</p></template>
      <template ><p>这个是没有指定具名插槽的内容</p></template>
    </Inner>
  </div>
</template>
// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <slot name="apple"></slot>
      <li><span>列表项02:</span>vue</li>
      <li><span>列表项03:</span>vue</li>
      <slot></slot>
    </ul>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
  },
  data() {
    return {
      apple:{
        msg: "味道非常好",
      }
    };
  },
};
</script>

这个是因为编译作用域

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

<Index> 中 是访问不到 <Inner> 中定义的数据的。

这个时候就要用到作用域插槽了

我们可以将 需要的数据 作为 <slot> 元素的一个 attribute 绑定上去

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

// Index.vue

<template>
  <div class="hello">
    <Inner>
      <template v-slot:apple="slotProps"
        ><p>
          {{
            `这个是apple,${slotProps.apple.msg},价格是${slotProps.price},一共是${slotProps.weightCount}kg`
          }}
        </p>
      </template>
      <template><p>这个是没有指定具名插槽的内容</p></template>
    </Inner>
  </div>
</template>

// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <slot name="apple" :apple="apple" :price="price" :weightCount="weightCount"></slot>
      <li><span>列表项02:</span>vue</li>
      <li><span>列表项03:</span>vue</li>
      <slot></slot>
    </ul>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {},
  data() {
    return {
      apple: {
        msg: "味道非常好",
      },
      price: "10元/kg",
      weightCount: 1000,
    };
  },
};
</script>

image.png

关键的两步操作

  1. 将要传出的数据作为 <slot> 元素的一个 attribute 绑定上去,称为插槽 prop
<slot name="apple" :apple="apple" :price="price" :weightCount="weightCount"></slot>
  1. 在组件标签内(父级作用域中),可以使用带值的 v-slot 来获取所有的插槽 prop,v-slot会把所有插槽 prop聚合为一个对象。
<template v-slot:apple="slotProps">
        <p>
          {{
            `这个是apple,${slotProps.apple.msg},价格是${slotProps.price},一共是${slotProps.weightCount}kg`
          }}
        </p>
</template>

这里我们把这个插槽 prop对象定义为 slotProps(这个你可以自定义),然后我们就可以通过这个对象来访问组件内部的是数据了。

如果用过element-ui的同学,一定知道表格就是这样来的!! 这个就是表格的实现原理。

四、插槽的其他特性

关于插槽的其他特性,建议大家查阅官方文档,上面已经写的非常清楚了。

后备内容
独占默认插槽的缩写语法
解构插槽 Prop
动态插槽名
具名插槽的缩写

五、记录一些小问题

1.插槽内容的样式问题 插槽内容的样式是以父组件为准还是以子组件为准

// Index.vue

<template>
  <div class="hello">
    <Inner>
      <p>想把这个p标签插入到Inner</p>
    </Inner>
  </div>
</template>
// Index.vue

p {
  color: #42b983;
}
// Inner.vue

<template>
  <div class="hello">
    <ul>
      <li><span>列表项01:</span>vue</li>
      <li><span>列表项02:</span>vue</li>
      <li><span>列表项03:</span>vue</li>
    </ul>
  </div>
</template>
// Inner.vue

p {
  color: #1e80ff;
}

image.png

参考资料

插槽 - Vue.js

Vue 插槽详解

一次性讲明白vue插槽slot