Vue进阶(三)插槽slot,并使用slot开发高级分页组件

如果对组件不太了解,可以先阅读笔者的这两篇文章,在对组件有了一定的了解之后,在查看本篇文章:
vue进阶(一),深入了解组件,自定义组件
Vue进阶(二)设计高级组件——自定义通知

注意:本篇文章的重点是使用slot开发一个分页组件,如果希望详细了解Vue中slot的用法,可以查看官网文档,同时,如果您在阅读本文中发现错误或者使用不当的地方,还请您指出修正!

1. 什么是插槽

Vue实现了一套内容分发的API,而<slot>元素就是承载分发内容的出口。
使用<slot>,我们可以这样写一个组件:
slotTest.vue

  <div>
    <span>这个组件中使用了slot元素</span>
    <slot></slot>
  </div>

使用slotTest.vue:

    <current-name>
      这个内容将展示在slot的位置
    </current-name>

显示效果:
在这里插入图片描述
当组件在渲染时,<slot></slot>将会被替换为“这个内容将展示在slot的位置”。插槽内可以包含任何模板代码,包括 HTML,甚至其他自定义组件:

    <current-name>
      <div>这个内容将展示在slot的位置</div>
    </current-name>

显示结果:
在这里插入图片描述
如果slotTesttemplate中没有包含一个<slot>元素,那么,在使用slotTest是,<slot>任意内容</slot>组件起始标签和结束标签之间的任何内容都会被抛弃。

2. 插槽的具体使用

现在有这样的一个组件,其可以显示一些个人信息。
person.vue:

<template>
  <div>
    <span>这个组件中使用了slot元素</span>
    <div>
      firstName:
      <slot name="firstName"></slot>
    </div>
    <div>
      lastName:
      <slot name="lastName" :last_name="lastname"></slot>
    </div>
    <div>
      sex:
      <slot name="sex">(如果没有使用到这个插槽,那么这就是这个插槽的默认填充)</slot>
    </div>
   <div>
      身高:
      <slot name="height">如果使用了这个插槽,那么这里的内容就无效了</slot>
    </div>
    <div>
      体重:
      <slot name="weight"></slot>
    </div>
    <slot/>
  </div>
</template>

<script>
export default {
  name: "currentName",
  data(){
    return{
      firstName: 'Niall',
      lastname: 'August'
    }
  }
}
</script>

使用person.vue:

<template>
  <div>
    <person>
      <template v-slot:height>180cm</template>
      <template v-slot:[slotName]>{{firstname}}</template>
      <template v-slot:lastName=last>{{last.last_name}}</template>
      <template #weight>90kg</template>
      <template><div>这个内容将展示在default slot的位置</div></template>
    </person>
  </div>
</template>

<script>
import person from "./components/person";
export default {
  name: "Test",
  components: {
    person
  },
  data(){
    return{
      slotName:'firstName',
      firstname: 'Jack'
    }
  }
}
</script>

显示结果:
在这里插入图片描述

2.1 具名插槽

person.vue中,我们设置了多个插槽,为了能够区分不同的插槽,我们为设置了每个插槽的name属性,一个不带name的slot出口都会带有一个默认名字default
当我们在使用组件时,在分发内容上就需要在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称,以上面我们分发height时:

	<template v-slot:height>180cm</template>

这里我们还需要注意,我们根据slot.name来分发内容,但是和我们在使用组件时的顺序没有关系,也就是说,我们分发内容的顺序和显示没有关系,显示的顺序由我们的组件中slot的顺序决定。所以,虽然我们在使用person时将<template v-slot:height>180cm</template>写在了最前面,但是最终的显示内容还是按照了person中slot的顺序来显示的。
在设置了slot.name之后,<template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
也就是上文中的:

<template><div>这个内容将展示在default slot的位置</div>

最终会渲染到默认插槽的位置。

v-bind,v-on可以简写成 :,@ ,类似的,v-slot也可以用 # 来简写:

<template #weight>90kg</template>

2.2 使用动态插槽名

    <template v-slot:[slotName]>{{firstname}}</template>

这里的slotName是Vue实例中的变量名,可以进行动态绑定到person中对应的插槽。此时,slotName = firstName,所以,这种写法也就等价于:

	<template v-slot:firstName>{{firstname}}</template>

2.3 作用域插槽

有时候,我们想要在使用组件的时候访问到子组件内的数据。比如我们在使用person时向其lastName插槽中分发的内容需要使用person里面的lastname数据:

<template v-slot:lastName>{{lastname}}</template>

上述代码是不会正常工作的,因为我们提供的内容实在父级渲染的,person能够访问到lastname,但是我们在使用person时,我们父级实例并不存在lastName
为了让person中的lastname能够在父级渲染时使用,我们可以将person.lastname作为一个属性绑定到person的插槽上面:

<slot name="lastName" :last_name="lastname"></slot>

这里就给person中namelastName的插槽增加了一个last_name属性,其属性值为person.lastname,然后,当我们给lastName插槽分发内容时,我们可以先使用插槽的last_name属性获取person.lastname:

<template v-slot:lastName=last>{{last.last_name}}

在使用时,我们先将lastName插槽赋值给last,然后我们就可以通过last.last_name获取到对应的值了。
使用参数解构:

<template v-slot="{lastname : last}">{{last.last_name}}

last_name这种绑定在<slot>元素上的属性也被称作为 插槽prop

3. 开发分页组件

我们现在使用slot来实现下面的分页组件:
请添加图片描述

我们先根据效果图分析组件的组成,上面的fruit,color,car可以是一个列表,下面是一个div。大家可以认为这个组件很容易写出来,毕竟其仅仅是由两部分构成,上面是一个横置的列表,下面是一个div。但是我们希望这个分页组件具有足够的灵活性,这样来使用组件:

    <tabs value='fruit'>
      <tab value="fruit" label="水果">
        <div>I like apple best</div>
      </tab>
      <tab value="color" label="颜色">
        <div>The sky is blue</div>
      </tab>
      <tab value="car" label="汽车">
        <div>I want to have a nice car</div>
      </tab>
    </tabs>

而不是这样:

    <tabs value='fruit'>
      <tab value="fruit" label="水果"></tab>
      <tab value="color" label="颜色"></tab>
      <tab value="car" label="汽车"></tab>
      <div belong="friut">I like apple best</div>
      <div belong="color">The sky is blue</div>
      <div belong="car">I want to have a nice car</div>
    </tabs>

如果仅仅是按第二种思路来写tab组件的话,直接根据此时tabs.value的值来设置div.show的值就能完成。但是,第二种思路开发出来的分页组件灵活性和复用性都不高。
如果希望我们的组件最后按照第一种写法来使用,我们该怎么做呢?
我们先观察tabs组件

<tabs value='fruit'>
  ...
  <tab value="color" label="颜色">
     <div>The sky is blue</div>
  </tab>
  ...
</tabs>

tabs组件中插入分页tab<tab>是自定义组件,我们也需要使用一个slot来接收。
那么,我们现在可以明确的是,tabs组件中都需要<slot>来分发内容。现在我们先思考tabs的结构,一个列表用于展示列表标签,一个div来展示内容,看到这里,你可能会以为,这样写不就是上文中的思路二吗,带着疑问继续看下去,相信你能明白两者的不同。
tabs.vue:

<template>
  <div class="tabs">
    <ul class="ul">
      <slot></slot>
    </ul>
    <div></div>
  </div>
</template>

这里的<slot>用于接收tabs中所有的tabtab是我们的自定义组件,所有的tab标签会以列表的形式展现出来,所以tab.vue

<template>
  <li>
    {{label}}
  </li>
</template>

<script>
export default {
  name: "tab",
  props:{
    label:{
      type:  String,
      default: 'tab'
    },
    value:{
      type: [Number, String],
      required: true
    }
  }
}
</script>

这时候,我们组件的上半部分就已经完成了,但是每一个分页的内容我们该如何显示出来呢?
我们的tab是这样使用的:

<tab value="color" label="颜色">
     <div>The sky is blue</div>
</tab>

我们在其中插入了<div>标签,我们使用一个slot来接收它?如果我们使用slot接收它,那分页的内容就只能出现在列表里面了,我们现在并不希望在列表中展示分页的内容,而是将分页的内容展示在列表下面的一个容器中。
我们看一看思路和代码结构的矛盾点:
代码方面:
在这里插入图片描述
思路方面:
在这里插入图片描述

现在我们需要解决的是,如何将<tab>接收到的<div>是在tab内部,但是我们需要在tab之外来创建。所以,我们如果要解决这个矛盾,我们需要先将tab中分发的内容提取出来,然后再在tabs中将其创建出来。
所以,我们在tabs中增加一个数据:

 data(){
    return{
      panes: []
    }
  }

然后修改tab,在mounted之后就将自身加到panes中:

 mounted() {
    this.$parent.panes.push(this)
  }

这样,我们在tabs中就可以通过this.panes获取到每一个tab的分页内容。
我们在tabs中再使用一个pane组件,这个也是我们的自定义组件,其作用就是显示某一个分页的内容。
因为this.panes中保存了所有的分页内容,如果需要按照标签来显示内容的话,我们还需要给tabs增加一个selected值,同时在tab中增加一个active标识,用于判断pane中的内容是否是该分页的内容,同时,当我们点击一个tab时,需要更改tabs.selected
在使用pane时,我们向其传入panesSelectedpanesSelected是根据此时分页的active标识来过滤出需要渲染的DOM节点。

修改tabs:

<template>
  <div class="tabs">
    <ul class="ul">
      <slot v-on:click="onChange"></slot>
    </ul>
    <div></div>
    <pane :panes="panesSelected"></pane>
  </div>
</template>

<script>
import pane from "./pane";
export default {
  ...
  components:{
    pane
  },
  data(){
    return{
      panes: [],
      selected: this.value
    }
  },
  computed:{
    panesSelected () {
      return this.panes.map( item => item.active ? item.$slots.default : null
      )
    }
  },
  methods:{
    onChange(index){
      this.selected = index
    }
  }
}
</script>

修改tab:

<template>
  <li @click="handleClick">
    {{label}}
  </li>
</template>

<script>
export default {
  ...
  methods:{
    handleClick(){
      this.$parent.onChange(this.value)
    }
  },
  computed:{
    active () {
      return this.$parent.selected === this.value
    }
  }
}
</script>

现在我们来设计pane,pane已经能够获取到需要显示的内容了,关键是我们应该如何渲染出slot
panesSelected中存放的是 {{tab(此tab的active为true)}}.$slots.default
我们如何将slot数据渲染出来呢?
我们将使用两种方式来达到我们的期望:

  1. 使用Vue模板
  2. 使用Vue的渲染函数

1. 使用Vue模板

<template>
  <div class="pane">
    <slot/>
  </div>
</template>

<script>
export default {
  name: "pane",
  props:{
    panes:{
      type: Array,
      required: true
    }
  },
  watch:{
    panes(val){
      this.$slots.default = val
      this.$forceUpdate()
    }
  }
}
</script>

我们将监听panes(传入的panesSelected),每当其发生变化时(即分页tab被点击之后,改变了tab.active,导致panesSelected改变),将panes重新赋值给** this.$slots.default**。这里我们还需要调用this.\$forceUpdate(),因为<slot>并不是响应式的,当我们给this.$slots.default重新赋值之后,页面并不会刷新。
注意,使用模板来渲染传入的slot是笔者思考很长时间才实现成功的,就像Vue官方文档所说:
在这里插入图片描述
所以,笔者相信一定存在不使用this.$forceUpdate()并且通过Vue模板来实现这里的pane组件,但是笔者愚钝,还希望您能提出更好的意见。

2. 使用Vue的渲染函数
这种情况下,我们不需要创建模板:

<script>
export default {
  props: {
    panes: {
      type: Array,
      required: true
    }
  },
  render () {
    return (
      <div>
        {panes}
      </div>
    )
  }
}
</script>

这样就能将panes(即传入的panesSelected)渲染出来了。

4. 源码

tabs.vue:

<template>
  <div class="tabs">
    <ul class="ul">
      <slot v-on:click="onChange"></slot>
    </ul>
    <div></div>
    <pane :panes="panesSelected"></pane>
  </div>
</template>

<script>
import pane from "./pane";
export default {
  name: "tabs",
  props:{
    value: {
      type: [String, Number],
      required: true
    }
  },
  components:{
    pane
  },
  data(){
    return{
      panes: [],
      selected: this.value
    }
  },
  computed:{
    panesSelected () {
      return this.panes.map( item => item.active ? item.$slots.default : null
      )
    }
  },
  methods:{
    onChange(index){
      this.selected = index
      this.$emit('change', index)
    }
  }
}
</script>

<style lang="scss" scoped>
.tabs{
  background: #cccccc;
  border-radius: 5px;
  width: 200px;
  padding: 5px;
  ul{
    margin: 0;
    padding: 0;
    display: inline-flex;
    justify-content: space-between;
    line-height: 40px;
    box-sizing: border-box;
    width: 100%;
  }
}

</style>

tab.vue:

<template>
  <li :class="[active?'active':'']" @click="handleClick">
    {{label}}
  </li>
</template>

<script>
export default {
  name: "tab",
  props:{
    label:{
      type:  String,
      default: 'tab'
    },
    value:{
      type: [Number, String],
      required: true
    }
  },
  mounted() {
    this.$parent.panes.push(this)
  },
  methods:{
    handleClick(){
      this.$parent.onChange(this.value)
    }
  },
  computed:{
    active () {
      return this.$parent.selected === this.value
    }
  }
}
</script>

<style lang="scss" scoped>
li{
  background-color: lightgray;
  border: #666666 1px solid;
  border-radius: 5px;
  list-style: none;
}
.active{
  background-color: lightblue;
}
</style>

pane.vue:

<template>
  <div class="pane">
    <slot/>
  </div>
</template>

<script>
export default {
  name: "pane",
  props:{
    panes:{
      type: Array,
      required: true
    }
  },
  watch:{
    panes(val){
      this.$slots.default = val
      this.$forceUpdate()
    }
  }
}
</script>

<style lang="scss" scoped>
.pane{
  margin-top: 10px;
  box-sizing: border-box;
  padding: 10px;
  height: 200px;
  width: 200px;
  border: #666666 1px solid;
  line-height: 30px;
}
</style>

App.vue:

<template>
  <div>
    <tabs value='fruit'>
      <tab :value=item.label v-for="(item, index) in test" :key="index" :label="item.label">
        <div>{{item.content}}</div>
      </tab>
    </tabs>
  </div>
</template>

<script>
import tabs from "./components/tab/tabs";
import tab from "./components/tab/tab";
export default {
  name: "Test",
  components: {
    tabs,
    tab
  },
  data(){
    return{
      obj: {
        fruit: 'I like apple best',
        color: 'The sky is blue',
        car: 'I want to have a nice car'
      },
      test:[
        {
          label:'fruit',
          content:'I like apple best'
        },
        {
          label:'color',
          content: 'The sky is blue'
        },
        {
          label:'car',
          content: 'I want to have a nice car'
        }
      ]
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

结果:
请添加图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值