slot 插槽是 Vue 非常实用的一个功能,它通常用于为同一个组件填充不同的内容。然而,官方文档里面提到的编译作用域、具名插槽、作用域插槽等概念有点抽象,让人有点难以理解。我想通过一个简单的例子,即使用插槽的特性实现一个简单的 list 组件,这会有助于我们彻底搞懂 Vue 插槽。
本文代码使用最新的 v-slot 指令(Vue 2.6.0)实现,v-slot 指令与原来的 slot attribute API 差异请参考官方文档的说明:cn.vuejs.org
插槽内容
slot 内容分发的 API,我们可以很快做出来:
list.vue 子组件
<div class="card">
<h2 class="card-header">实时热搜榜</h2>
<div class="card-section">
<ul class="list">
<slot />
</ul>
</div>
</div>
parent.vue 父组件
<base-list>
<li>教育部要求严格国际学生申请资格</li>
<li>央视对话郭杰瑞</li>
<li>情人节挑战</li>
<li>钟南山李兰娟张文宏禁毒宣传片</li>
</base-list>
当组件渲染的时候,list 组件的 <slot></slot> 将会被替换为使用该组件时插槽的内容。
等等,你可能会有所疑惑,为什么我们一定需要使用插槽,上面这个例子使用 Props 将列表数据以数组的形式传递给子组件也完全可以呀!是的,使用 Props 看起来还更加简单且符合逻辑。
<template>
<div class="card">
<h2 class="card-header">实时热搜榜</h2>
<div class="card-section">
<ul class="list">
<li v-for="item in items">{{ item }}</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'baseList',
props: {
items: () => {
return []
}
}
</script>
如果仅仅是数值的变动我们完全可以用 Props 解决,但是真实的项目之中,组件往往是复杂的,组件可能还需要嵌套其他组件。假如我们的项目可能需要用到多种类型的 list,list 的 title 样式以及其他部分是保持一致的,但是内部的列表布局和样式可能会有所不同,如下图所示:
具名插槽
在上面的例子中,我们的 <slot /> 插槽没有 name attribute,实际上,Vue 默认的会给 <slot /> 加上 name="default",这便是默认插槽。我们也可以自定义插槽的 name,这在组件有多个插槽时是特别有用的。假设我们的 list 组件现在需要将 header 头部也调整为插槽,这样我们就有两个插槽了,我们可以这样写:
list.vue 组件
<div class="card">
<div class="card-header">
<slot name="header" />
</div>
<div class="card-section">
<ul class="list">
/* 这里的 slot 我们可以不设置 name 属性,默认为 default */
<slot name="content" />
</ul>
</div>
</div>
parent.vue 组件
<base-list>
<template v-slot:header>
<h2 class="card-title">猜你喜欢</h2>
</template>
<template v-slot:content>
<li>教育部要求严格国际学生申请资格</li>
<li>央视对话郭杰瑞</li>
<li>情人节挑战</li>
<li>钟南山李兰娟张文宏禁毒宣传片</li>
</template>
</base-list>
这样一来,我们 list 组件的 header 部分和 content 部分都变为更加灵活的插槽了,我们也可以为 header 设置独特的样式。
v-slot 跟 v-on 一样,有缩写的形式,例如 v-slot:header 可以被重写为 #header,关于这部分内容请参考官方文档,就不再班门弄斧了。
作用域插槽
在了解了具名插槽后,我们再看看是否还有可以优化的地方,让我们暂时撤回添加 header 插槽的部分,回到第一个例子的状态:
parent.vue 父组件
<base-list>
<li><span>1</span>教育部要求严格国际学生申请资格<small>4百万次</small></li>
<li><span>2</span>央视对话郭杰瑞<small>3百万次</small></li>
<li><span>3</span>情人节挑战<small>2百万次</small></li>
<li><span>4</span>钟南山李兰娟张文宏禁毒宣传片<small>2百万次</small></li>
</base-list>
显然,这里的列表数据都是填充的假数据,我们可以用 v-for 渲染列表数据,以便以后通过后端接口获取真实的数据。
<template>
<base-list>
<li v-for="item in messages">
<span>{{ item.index }}</span>
{{ item.name }}
<small>{{ item.count }}百万次</small>
</li>
</base-list>
</template>
<script>
export default {
name: 'baseList',
data() {
return {
messages: [
{name: '教育部要求严格国际学生申请资格', count: '4'},
{name: '央视对话郭杰瑞', count: '3'},
{name: '情人节挑战', count: '2'},
{name: '钟南山李兰娟张文宏禁毒宣传片', count: '2'}
]
}
}
}
</script>
这仍然不是最优的,试想一下,我们的 v-for 渲染部分是在父组件完成的,这意味着当我们有多个 list 组件时,我们会存在重复的 v-for 部分代码:
parent.vue 父组件
<div id="app">
<base-list title="实时热搜榜">
<!-- v-for 代码部分放在父组件,因此当有两个 list 组件时,vue-for 显得有点重复 -->
<li v-for="item in messages" :key="item.id">
<span>{{ item.index }}</span>
{{ item.name }}
<small>{{ item.count }}百万次</small>
</li>
</base-list>
<base-list title="热门话题">
<!-- 这是第二个 v-for 代码部分 -->
<li class="topic-item" v-for="item in topics" :key="item.id">
<p class="topic-title">#{{ item.name }}#</p>
<p class="topic-desc">{{ item.desc }}</p>
</li>
</base-list>
</div>
我们需要想办法将 v-for 部分放入子组件之中,然而这里又会产生一个问题,将 v-for 部分放入子组件后,数据如何传递给子组件呢?
插槽是具有编译作用域的,**父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。**这也意味着只凭借 Props 传递列表数据是不行的,父组件的内容仍然无法获得 v-for 渲染的每一项的数据。
好在,Vue 给我们提供了作用域插槽,为了让列表数据在父级的插槽内容中可用,我们可以将 item 作为 <slot> 元素的一个 attribute 绑定上去,这被称为插槽 prop。这样就可以让子组件的指定内容被允许在父组件中使用。
list.vue 子组件
<div class="card">
<h2 class="card-header">{{ title }}</h2>
<div class="card-section">
<ul class="list">
<!-- 绑定在 <slot> 元素上的 attribute 被称为插槽 prop -->
<!-- 让列表数据在父级的插槽内容中可用 -->
<li v-for="item in items" :key="item.id">
<slot :item="item" />
</li>
</ul>
</div>
</div>
parent.vue 父组件
<div id="app">
<!-- 在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字 -->
<base-list title="实时热搜榜" :items="messages" v-slot="slotProps">
<div>
<span>{{ slotProps.item.index }}</span>
{{ slotProps.item.name }}
<small>{{ slotProps.item.count }}百万次</small>
</div>
</base-list>
<base-list title="热门话题" :items="topics" v-slot="slotProps">
<div class="topic-item">
<p class="topic-title">#{{ slotProps.item.name }}#</p>
<p class="topic-desc">{{ slotProps.item.desc }}</p>
</div>
</base-list>
</div>
我们完成了作用域插槽,但看起来代码量似乎并没有减少,虽然我们将 v-for 部分移到了子组件之中,但是又多使用了 v-slot 等语句,逻辑还变得更加复杂了,有点得不偿失?
不过,这样一来,父子组件的耦合性显著降低了,结构变得更加清晰,这是最佳的做法。
结语
再来回顾一下,虽然我们在本文只完成了一个简单的 list 组件,但是完全的将 Vue 插槽的特性如具名插槽、编译作用域、作用域插槽等内容都实践了一遍,在面对更加复杂的项目开发时,相信也能得心应手、举一反三。
参考
本文在书写过程中,参考了以下文章中的内容,在此致以诚挚的感谢:
Chor - 「译」一个案例搞懂 Vue.js 的作用域插槽