阐述 Vue 中 Slot(插槽)的类型(默认插槽、具名插槽、作用域插槽)及其高级用法。

各位观众老爷,欢迎来到今天的 Vue 插槽专场!我是你们的老朋友,今天就来和大家唠唠 Vue 里那些既实用又有点小神秘的插槽。

开场白:插槽是啥玩意儿?

咱们先来聊聊,啥是插槽?想象一下,你买了一件衣服,衣服上预留了一些口子,你可以根据自己的喜好往里边塞东西,比如塞个口袋,塞个装饰品,甚至塞个小宠物(别真塞啊!)。在 Vue 里,插槽就相当于这些预留的口子,组件可以预留一些位置,让使用它的父组件往里边填充内容。

为啥要有插槽?因为组件要够灵活啊!如果不使用插槽,子组件的内容就固定死了,父组件只能干巴巴地用,毫无个性化可言。有了插槽,父组件就能根据自己的需求,往子组件里塞各种各样的内容,让子组件变得千变万化。

第一部分:插槽家族的成员介绍

插槽家族可热闹了,里面有默认插槽、具名插槽和作用域插槽这三位成员。咱们一个一个来认识。

1. 默认插槽(Default Slot):最朴素的那个

默认插槽,顾名思义,就是最简单、最常用的插槽。它就像一个没名字的口子,父组件往里塞东西的时候,不用指定塞哪个口子,直接一股脑塞进去就行了。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <p>我是子组件的内容</p>
    <slot></slot>
  </div>
</template>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <p>我是通过默认插槽传递的内容</p>
      <button>点我啊!</button>
    </MyComponent>
  </div>
</template>

在这个例子里,<MyComponent> 组件中有一个 <slot> 标签,它就是默认插槽。父组件 <App.vue> 在使用 <MyComponent> 时,把 <p> 标签和 <button> 标签都塞进了这个默认插槽里。最终渲染出来的效果就是:

<div>
  <p>我是子组件的内容</p>
  <p>我是通过默认插槽传递的内容</p>
  <button>点我啊!</button>
</div>

默认插槽的特点就是简单粗暴,啥也不用说,直接往里塞就行。但它也有缺点,就是只能有一个,如果你想在子组件里预留多个位置,让父组件分别填充不同的内容,那默认插槽就搞不定了。

2. 具名插槽(Named Slot):指名道姓的那个

为了解决默认插槽只能有一个的问题,Vue 引入了具名插槽。具名插槽就像给每个口子都贴上了标签,父组件往里塞东西的时候,必须指明要塞哪个口子。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <template v-slot:header>
        <h1>我是头部</h1>
      </template>
      <template v-slot:default>
        <p>我是主要内容</p>
      </template>
      <template v-slot:footer>
        <p>我是页脚</p>
      </template>
    </MyComponent>
  </div>
</template>

在这个例子里,<MyComponent> 组件定义了三个插槽:一个名为 "header" 的具名插槽,一个默认插槽,和一个名为 "footer" 的具名插槽。父组件 <App.vue> 在使用 <MyComponent> 时,使用 v-slot 指令来指定要往哪个插槽里塞内容。

v-slot 指令可以简写为 #,所以上面的代码也可以写成这样:

<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <template #header>
        <h1>我是头部</h1>
      </template>
      <template #default>
        <p>我是主要内容</p>
      </template>
      <template #footer>
        <p>我是页脚</p>
      </template>
    </MyComponent>
  </div>
</template>

最终渲染出来的效果就是:

<div>
  <header>
    <h1>我是头部</h1>
  </header>
  <main>
    <p>我是主要内容</p>
  </main>
  <footer>
    <p>我是页脚</p>
  </footer>
</div>

具名插槽的特点就是可以定义多个,并且父组件可以精确地控制要往哪个插槽里塞内容。

3. 作用域插槽(Scoped Slot):带数据的那个

具名插槽解决了多个插槽的问题,但是它还有一个问题,就是父组件只能往插槽里塞静态的内容,无法使用子组件的数据。为了解决这个问题,Vue 引入了作用域插槽。

作用域插槽就像一个函数,子组件可以把一些数据作为参数传递给这个函数,父组件在使用插槽的时候,就可以拿到这些数据,并根据这些数据来渲染内容。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <slot :user="user"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '张三',
        age: 18
      }
    }
  }
}
</script>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <template v-slot:default="slotProps">
        <p>姓名:{{ slotProps.user.name }}</p>
        <p>年龄:{{ slotProps.user.age }}</p>
      </template>
    </MyComponent>
  </div>
</template>

在这个例子里,<MyComponent> 组件在 <slot> 标签上绑定了一个 user 属性,这个属性的值就是子组件的数据。父组件 <App.vue> 在使用 <MyComponent> 时,使用 v-slot:default="slotProps" 来接收子组件传递的数据,并把这些数据赋值给 slotProps 变量。然后,父组件就可以使用 slotProps.user.nameslotProps.user.age 来访问子组件的数据了。

最终渲染出来的效果就是:

<div>
  <p>姓名:张三</p>
  <p>年龄:18</p>
</div>

作用域插槽的特点就是可以传递数据,父组件可以根据子组件的数据来渲染内容。

总结一下,插槽家族的成员特点:

插槽类型 特点 使用场景
默认插槽 最简单,只能有一个,父组件往里塞内容,不用指定塞哪个口子。 子组件只需要一个内容填充位置,且不需要区分不同区域的内容。例如:一个简单的弹窗组件,只需要填充弹窗的主体内容。
具名插槽 可以有多个,父组件必须指明要往哪个口子塞内容。 子组件需要多个内容填充位置,且需要区分不同区域的内容。例如:一个页面布局组件,需要填充头部、主体、尾部等不同区域的内容。一个复杂的表格组件,需要填充表头、表体、表尾等不同区域的内容。
作用域插槽 可以传递数据,父组件可以根据子组件的数据来渲染内容。 子组件需要将一些内部数据暴露给父组件,让父组件根据这些数据来渲染内容。例如:一个列表组件,需要将列表中的每一项的数据传递给父组件,让父组件自定义每一项的渲染方式。一个下拉菜单组件,需要将选中的选项的数据传递给父组件,让父组件显示选中的选项。

第二部分:插槽的高级用法

了解了插槽的基本类型,咱们再来看看插槽的一些高级用法,让你的组件更加强大。

1. 默认内容(Fallback Content):备胎的自我修养

有时候,我们希望在父组件没有提供插槽内容的时候,子组件能够显示一些默认的内容。这就像一个备胎,在没有人需要它的时候,它就默默地在那里,一旦有人需要它,它就能挺身而出。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <slot>
      <p>我是默认内容</p>
    </slot>
  </div>
</template>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent></MyComponent>
    <MyComponent>
      <p>我是通过插槽传递的内容</p>
    </MyComponent>
  </div>
</template>

在这个例子里,<MyComponent> 组件的 <slot> 标签里有一个 <p> 标签,它就是默认内容。当父组件没有提供插槽内容的时候,子组件就会显示这个默认内容。当父组件提供了插槽内容的时候,子组件就会显示父组件的内容。

最终渲染出来的效果就是:

<div>
  <p>我是默认内容</p>
</div>
<div>
  <p>我是通过插槽传递的内容</p>
</div>

默认内容的作用就是让组件更加健壮,即使父组件没有提供插槽内容,组件也能正常显示。

2. 动态插槽名(Dynamic Slot Names):灵活多变的那个

有时候,我们希望根据不同的条件来选择不同的插槽。这就像一个百变怪,可以根据不同的情况变成不同的样子。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <slot :name="slotName"></slot>
  </div>
</template>

<script>
export default {
  props: {
    slotName: {
      type: String,
      default: 'default'
    }
  }
}
</script>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent :slotName="currentSlot">
      <template #header>
        <h1>我是头部</h1>
      </template>
      <template #default>
        <p>我是主要内容</p>
      </template>
      <template #footer>
        <p>我是页脚</p>
      </template>
    </MyComponent>
    <button @click="toggleSlot">切换插槽</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentSlot: 'default'
    }
  },
  methods: {
    toggleSlot() {
      this.currentSlot = this.currentSlot === 'default' ? 'header' : 'default';
    }
  }
}
</script>

在这个例子里,<MyComponent> 组件的 <slot> 标签的 name 属性绑定了一个 slotName 属性,这个属性的值决定了要使用哪个插槽。父组件 <App.vue> 通过 currentSlot 数据来控制 slotName 的值,从而实现动态切换插槽的效果。

动态插槽名的作用就是让组件更加灵活,可以根据不同的条件来选择不同的插槽。

3. 具名作用域插槽(Named Scoped Slots):既要名字也要数据

前面我们学习了具名插槽和作用域插槽,那么能不能把它们结合起来呢?答案是肯定的!具名作用域插槽就是既有名字又有数据的插槽。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <slot name="header" :title="title"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: '我是标题'
    }
  }
}
</script>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <template #header="slotProps">
        <h1>{{ slotProps.title }}</h1>
      </template>
    </MyComponent>
  </div>
</template>

在这个例子里,<MyComponent> 组件的 <slot> 标签的 name 属性是 "header",并且绑定了一个 title 属性,这个属性的值就是子组件的数据。父组件 <App.vue> 在使用 <MyComponent> 时,使用 v-slot:header="slotProps" 来接收子组件传递的数据,并把这些数据赋值给 slotProps 变量。然后,父组件就可以使用 slotProps.title 来访问子组件的数据了。

具名作用域插槽的作用就是让组件既能区分不同的区域,又能传递数据。

4. 解构插槽 Prop(Destructuring Slot Props):优雅地取数据

在使用作用域插槽的时候,我们通常需要从 slotProps 对象中取出需要的数据。如果数据比较多,就需要写很多 slotProps.xxx,这样比较麻烦。Vue 提供了结构插槽 Prop 的语法,可以让我们更优雅地取出数据。

  • 子组件的代码:
<!-- MyComponent.vue -->
<template>
  <div>
    <slot :user="user" :settings="settings"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '张三',
        age: 18
      },
      settings: {
        theme: 'dark',
        language: 'zh-CN'
      }
    }
  }
}
</script>
  • 父组件的代码:
<!-- App.vue -->
<template>
  <div>
    <MyComponent>
      <template #default="{ user, settings }">
        <p>姓名:{{ user.name }}</p>
        <p>年龄:{{ user.age }}</p>
        <p>主题:{{ settings.theme }}</p>
        <p>语言:{{ settings.language }}</p>
      </template>
    </MyComponent>
  </div>
</template>

在这个例子里,父组件 <App.vue> 在使用 <MyComponent> 时,使用 v-slot:default="{ user, settings }" 来接收子组件传递的数据,并把 usersettings 数据分别赋值给 usersettings 变量。然后,父组件就可以直接使用 user.namesettings.theme 来访问子组件的数据了。

解构插槽 Prop 的作用就是让代码更加简洁,提高开发效率。

第三部分:插槽的实际应用场景

说了这么多理论,咱们来看看插槽在实际开发中都有哪些应用场景。

1. 布局组件(Layout Component):搭积木的乐趣

布局组件是最常见的插槽应用场景之一。我们可以创建一个通用的布局组件,然后在不同的页面中使用它,只需要往不同的插槽里塞入不同的内容即可。

  • 布局组件的代码:
<!-- Layout.vue -->
<template>
  <div class="layout">
    <header class="layout-header">
      <slot name="header"></slot>
    </header>
    <main class="layout-content">
      <slot></slot>
    </main>
    <footer class="layout-footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<style scoped>
.layout {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.layout-header {
  height: 60px;
  background-color: #f0f0f0;
}

.layout-content {
  flex: 1;
  padding: 20px;
}

.layout-footer {
  height: 40px;
  background-color: #f0f0f0;
  text-align: center;
}
</style>
  • 页面组件的代码:
<!-- HomePage.vue -->
<template>
  <Layout>
    <template #header>
      <h1>首页</h1>
    </template>
    <template #default>
      <p>欢迎来到首页!</p>
    </template>
    <template #footer>
      <p>版权所有 © 2023</p>
    </template>
  </Layout>
</template>

<script>
import Layout from './Layout.vue';

export default {
  components: {
    Layout
  }
}
</script>

2. 表格组件(Table Component):数据的艺术

表格组件也是一个很好的插槽应用场景。我们可以创建一个通用的表格组件,然后在不同的页面中使用它,只需要往不同的插槽里塞入不同的列数据即可。

  • 表格组件的代码:
<!-- Table.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">
          {{ column.title }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in data" :key="row.id">
        <td v-for="column in columns" :key="column.key">
          <slot :name="column.key" :row="row">{{ row[column.key] }}</slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props: {
    columns: {
      type: Array,
      required: true
    },
    data: {
      type: Array,
      required: true
    }
  }
}
</script>
  • 页面组件的代码:
<!-- UserList.vue -->
<template>
  <Table :columns="columns" :data="users">
    <template #name="{ row }">
      <a :href="`/users/${row.id}`">{{ row.name }}</a>
    </template>
    <template #age="{ row }">
      <span>{{ row.age }} 岁</span>
    </template>
  </Table>
</template>

<script>
import Table from './Table.vue';

export default {
  components: {
    Table
  },
  data() {
    return {
      columns: [
        { key: 'name', title: '姓名' },
        { key: 'age', title: '年龄' },
        { key: 'email', title: '邮箱' }
      ],
      users: [
        { id: 1, name: '张三', age: 18, email: '[email protected]' },
        { id: 2, name: '李四', age: 20, email: '[email protected]' },
        { id: 3, name: '王五', age: 22, email: '[email protected]' }
      ]
    }
  }
}
</script>

3. 弹窗组件(Modal Component):优雅的提示

弹窗组件也是一个常见的插槽应用场景。我们可以创建一个通用的弹窗组件,然后在不同的页面中使用它,只需要往不同的插槽里塞入不同的内容即可。

  • 弹窗组件的代码:
<!-- Modal.vue -->
<template>
  <div class="modal" v-if="visible">
    <div class="modal-content">
      <div class="modal-header">
        <slot name="header">
          <h2>{{ title }}</h2>
        </slot>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <slot name="footer">
          <button @click="close">关闭</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: '提示'
    }
  },
  methods: {
    close() {
      this.$emit('update:visible', false);
    }
  }
}
</script>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}

.modal-content {
  background-color: #fff;
  border-radius: 5px;
  width: 500px;
}

.modal-header {
  padding: 10px;
  border-bottom: 1px solid #ccc;
}

.modal-body {
  padding: 20px;
}

.modal-footer {
  padding: 10px;
  border-top: 1px solid #ccc;
  text-align: right;
}
</style>
  • 页面组件的代码:
<!-- MyPage.vue -->
<template>
  <div>
    <button @click="showModal">显示弹窗</button>
    <Modal :visible.sync="modalVisible" title="确认删除">
      <p>确定要删除这条记录吗?</p>
      <template #footer>
        <button @click="cancelDelete">取消</button>
        <button @click="confirmDelete">确认删除</button>
      </template>
    </Modal>
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  components: {
    Modal
  },
  data() {
    return {
      modalVisible: false
    }
  },
  methods: {
    showModal() {
      this.modalVisible = true;
    },
    cancelDelete() {
      this.modalVisible = false;
    },
    confirmDelete() {
      // 删除记录
      this.modalVisible = false;
    }
  }
}
</script>

第四部分:总结与展望

今天我们一起学习了 Vue 中插槽的各种类型和用法,以及它们在实际开发中的应用场景。希望通过今天的学习,大家能够更加灵活地使用插槽,让你的组件更加强大。

插槽是 Vue 中非常重要的一个特性,掌握插槽的使用,可以让你更好地组织代码,提高代码的复用性,让你的 Vue 项目更加易于维护。

未来,Vue 可能会在插槽方面有更多的改进和创新,让我们一起期待吧!

好了,今天的插槽专场就到这里,感谢大家的观看!我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注