Vue中插槽slot的使用

Vue中的一大特性是各种各样的组件, 通过对组件的复用我们可以非常方便的构建一个高大上前端页面。

例如, 我们可以构建一个组件

//子组件
<template>
  <div>
    我是一个单纯的子组件
  </div>
</template>

<script>
  export default {
    name: 'sub'
  }
</script>

<style scoped>

</style>

可以看到这是个非常单纯的子组件,我们可以在父组件中调用它

// 父组件
<template>
  <div class="hello">
    <m-sub></m-sub>
  </div>
</template>

<script>
  import sub from './sub'
export default {
    name: 'HelloWorld',
    components: {
      'm-sub': sub
    }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="stylus" scoped>
</style>

因为我们写的简单, 所以结局也很简单, 界面如下:

上面的图标是Vue官方demo带的, 这里就保留了...

可以看到我们成功的将子组件放到了父组件中, 那么只要我们想要用这个子组件的话, 只需要在用的父组件中注册使用就可以了,可是这和我们要将的插槽有什么关系呢?

我们有时候不满足于这么单纯的功能, 所以我们期望在调用子组件时能够在不同的地方插入一些内容,比如

<m-sub>
   <div>我想要插入一些内容</div>
</m-sub>

毕竟注册后是可以当做标签来使用的, 我们在里面插入内容不过份吧? 可是现实是运行后的结果依然如上图, 内部的内容并没有插入进子组件, 甚至根本没有被渲染出来。

那么我们该如何才能向子组件中添加我们想要的内容呢? 难道对于一个头部组件, 每个页面的头部我们都需要重复定义一个子组件?或者单独抽离出那些少的可怜的共有部分作为子组件?剩下的我们再重写?那各个页面的头部样式怎么保证一致性?那这样的子组件也太low了吧。这就引出了slot的作用!

slot: 插槽

通过slot我们可以向子组件中插入我们想要的内容, 例如:

// 子组件
<template>
  <div>
    <slot></slot>
    我是一个单纯的子组件
  </div>
</template>

在内部添加一个slot标签, 这有啥用呢? 我们看看界面:

可以看到我们想要插入的一些内容成功的插入到了子组件中, 这就是子组件的作用。同时我们可以发现这样的话我们可以在父组件中定义添加的内容的样式:

// 父组件
<style lang="stylus" scoped>
  .sub-add
    font-size 2rem
</style>

这样渲染后的样式就变成了

这样, 我们就可以通过插槽向子组件中添加任意我们想要的内容, 是不是比只能构建单纯的静态子组件要好得多了?

那对于一些组件我们需要添加多条内容怎么办?而且对于有些组件我们需要在不同的时候在不同的位置插入不同的内容, 可能对于前者我们可以在一个slot中把多条内容插入到一个插槽中, 可是对于后者,那是不可能的。

还好Vue为我们想好了, 我们可以为slot起名字, 就像这样:

// 子组件
<template>
  <div>
    <slot name="pre"></slot>
    <slot></slot>
    我是一个单纯的子组件
    <slot name="end"></slot>
  </div>
</template>

这样,我们就可以在父组件中通过不同的名字寻找不同的slot进行插入, 比如:

// 父组件
<template>
  <div class="hello">
    <m-sub>
      <div class="sub-add">我想要插入一些内容</div>
      <div slot="pre">我是头部</div>
      <div slot="end">我是尾部</div>
    </m-sub>
  </div>
</template>

这样的话我们就可以在不同的位置插入不同的内容, 而且就算我们有时候不想插入也没关系, 反正slot默认不显示。这样的话我们就得到了这样的界面:

是不是自由定制又先进了一步?这就是传说中的具名插槽, 对比默认插槽,它支持自由定制程度更高一些。

那么slot的功能就这些吗?不是的, 因为我们还有更加麻烦的情况需要面对...

我们有一个导航条, 根据不同页面的导航条我们需要它展示不同页面的名字, 甚至我们还需要一些其他的信息用于渲染子组件

这意味着什么呢?意味着我们在调用子组件时需要把子组件需要的数据传给子组件,这就牵涉到了父子组件的传值。

例如常用的父子间的传值方式为props(父传子)和emit(子传父),可是这两者和slot有什么关系呢?

答案是我们可以给slot绑定属性, 然后再在父组件中获取slot该属性的数据, 这样我们就能够在父组件中控制子组件的内容展示, 从

某种意义上来讲, 这比利用emit通过钩子向父组件传值方便得多, 事实上, 在这种情况下子组件向父组件传值通过emit极其不便。

例子如下:

// 子组件
<template>
  <div>
    <slot name="pre"></slot>
    <slot></slot>
    我是一个单纯的子组件
    <slot name="end"></slot>
    <div v-for="user in users">
      <slot name="user-slot" :user="user"></slot>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'm-sub',
    props: {
      users: {
        type: Array,
        default: () => []
      }
    }
  }
</script>

可以看到我们在内部寻欢遍历了传进来的数据, 以反复渲染slot中的内容, 同时我们给user-slot绑定了user属性, 这有啥用呢?我们看看父组件:

// 父组件
<template>
  <div class="hello">
    <m-sub :users="users">
      <div class="sub-add">我想要插入一些内容</div>
      <div slot="pre">我是头部</div>
      <div slot="end">我是尾部</div>
      <div slot="user-slot" slot-scope="subuser">
        <span>{{ subuser.user.username }}</span>
        <span>{{ subuser.user.age }}</span>
      </div>
    </m-sub>
  </div>
</template>

<script>
  import subs from './sub'
export default {
    name: 'HelloWorld',
    data () {
      return {
        users: [
          {
            username: 'will',
            age: 32
          },
          {
            username: 'lucky',
            age: 29
          }
        ]
      }
    },
    components: {
      'm-sub': subs
    }
}
</script>

它展示的页面如下图所示:

可以看到我们调用子组件时传入了users, 给子组件传入数据,然后在使用到user-slot的插槽时我们加了个属性slot-scope=‘subuser’,

这个slot-scope是干嘛的?subuser又是哪来的? 实际上,slot-scope就是绑定的子组件, subuser实际上是我们自定义的一个变量名, 通过它,我们可以调用子组件的属性, 比如下面的subuser.user.username里面的user就是子组件绑定的属性, 还记得user-slot绑定了一个属性么?:user='user', 这里的.user调用的就是子组件绑定的user属性, 这样父组件就能获取到每次循环时每个slot上绑定的数据。这样我们就能够在父组件中控制要显示的内容, 以及显示的方式啦。这就是传说中的作用域插槽

可能有些人意识不到这样做有什么好处,可是你想一想, 我们调用子组件的时候, 我们给子组件参数, 然后子组件只能按照他固定的方式和流程去渲染数据, 虽然有时候我们确实需要这么做, 可实际上我们需要面对的开发形式是多种多样的, 难免有时候需要不同的子组件样式或者内容显示控制, 不可能每次都去更改组件代码吧? 通过这种插槽的方式我们就能够在父组件中获取到数据的控制权,能够更自由的进行数据渲染,这就是它的意义之一。事实上我们去写组建时很多时候都会碰到这种情况, 这时候善用插槽能很大程度上避免很多不必要的麻烦。