Vue3 v-slot 详解与示例

Vue3 v-slot 详解与示例

v-slot 是 Vue3 中用于处理插槽(Slots)的指令,它提供了更强大和灵活的插槽功能。下面通过详细的示例来展示各种用法。

🎯 基础概念

什么是插槽?

插槽是 Vue 组件的一个强大特性,它允许你在组件中预留位置,让父组件可以插入自定义内容。

📝 基本用法

1. 默认插槽

子组件 BaseLayout.vue

vue

复制代码
<template>
  <div class="container">
    <header>
      <slot></slot> <!-- 默认插槽 -->
    </header>
    <main>
      <p>这是主内容区域</p>
    </main>
    <footer>
      <p>这是页脚</p>
    </footer>
  </div>
</template>

父组件使用

vue

复制代码
<template>
  <BaseLayout>
    <!-- 插入到默认插槽的内容 -->
    <h1>这是页面标题</h1>
    <p>这是插入到header插槽的内容</p>
  </BaseLayout>
</template>

<script setup>
import BaseLayout from './BaseLayout.vue'
</script>

2. 具名插槽

子组件 Card.vue

vue

复制代码
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header"></slot> <!-- 具名插槽 -->
    </div>
    <div class="card-body">
      <slot name="body"></slot> <!-- 具名插槽 -->
    </div>
    <div class="card-footer">
      <slot name="footer"></slot> <!-- 具名插槽 -->
    </div>
    <!-- 默认插槽 -->
    <slot></slot>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  margin: 10px;
}
.card-header {
  font-weight: bold;
  margin-bottom: 10px;
}
.card-body {
  margin-bottom: 10px;
}
.card-footer {
  font-size: 0.8em;
  color: #666;
}
</style>

父组件使用具名插槽

vue

复制代码
<template>
  <Card>
    <!-- 使用 v-slot 指令 -->
    <template v-slot:header>
      <h2>卡片标题</h2>
    </template>
    
    <template v-slot:body>
      <p>这是卡片的主要内容区域</p>
      <p>可以包含任何HTML内容</p>
    </template>
    
    <template v-slot:footer>
      <span>卡片底部信息</span>
      <button @click="handleClick">操作按钮</button>
    </template>
  </Card>
</template>

<script setup>
import Card from './Card.vue'

const handleClick = () => {
  console.log('按钮被点击了')
}
</script>

🔥 v-slot 语法糖

Vue3 提供了 # 符号作为 v-slot: 的简写形式:

vue

复制代码
<template>
  <Card>
    <!-- 简写语法 -->
    <template #header>
      <h2>简写语法示例</h2>
    </template>
    
    <template #body>
      <p>使用 # 代替 v-slot:</p>
    </template>
    
    <template #footer>
      <span>底部内容</span>
    </template>
  </Card>
</template>

💡 作用域插槽

作用域插槽允许子组件向插槽传递数据,父组件可以访问这些数据。

1. 基础作用域插槽

子组件 DataList.vue

vue

复制代码
<template>
  <div class="data-list">
    <ul>
      <li v-for="item in items" :key="item.id">
        <!-- 向插槽传递数据 -->
        <slot :item="item" :index="index"></slot>
      </li>
    </ul>
  </div>
</template>

<script setup>
defineProps({
  items: {
    type: Array,
    default: () => []
  }
})
</script>

父组件使用作用域插槽

vue

复制代码
<template>
  <DataList :items="userList">
    <!-- 接收子组件传递的数据 -->
    <template v-slot:default="slotProps">
      <div class="user-item">
        <span>{{ slotProps.index + 1 }}. </span>
        <strong>{{ slotProps.item.name }}</strong>
        <em>({{ slotProps.item.age }} 岁)</em>
      </div>
    </template>
  </DataList>
</template>

<script setup>
import { ref } from 'vue'
import DataList from './DataList.vue'

const userList = ref([
  { id: 1, name: '张三', age: 25 },
  { id: 2, name: '李四', age: 30 },
  { id: 3, name: '王五', age: 28 }
])
</script>

<style scoped>
.user-item {
  padding: 8px;
  margin: 4px 0;
  background-color: #f5f5f5;
  border-radius: 4px;
}
</style>

2. 解构作用域插槽参数

vue

复制代码
<template>
  <DataList :items="userList">
    <!-- 使用解构语法 -->
    <template #default="{ item, index }">
      <div class="user-item" :class="{ active: index % 2 === 0 }">
        <span>{{ index + 1 }}. </span>
        <strong>{{ item.name }}</strong>
        <span class="age">{{ item.age }} 岁</span>
      </div>
    </template>
  </DataList>
</template>

🎭 具名作用域插槽

子组件 UserCard.vue

vue

复制代码
<template>
  <div class="user-card">
    <!-- 具名作用域插槽 -->
    <slot name="avatar" :user="user" :size="avatarSize"></slot>
    
    <div class="user-info">
      <slot name="name" :user="user"></slot>
      <slot name="details" :user="user"></slot>
    </div>
    
    <div class="actions">
      <slot name="actions" :user="user" :onEdit="handleEdit"></slot>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  user: {
    type: Object,
    required: true
  }
})

const avatarSize = ref('medium')

const handleEdit = () => {
  console.log('编辑用户:', props.user.name)
}
</script>

<style scoped>
.user-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin: 10px;
  display: flex;
  align-items: center;
  gap: 16px;
}
.user-info {
  flex: 1;
}
.actions {
  display: flex;
  gap: 8px;
}
</style>

父组件使用具名作用域插槽

vue

复制代码
<template>
  <UserCard :user="currentUser">
    <!-- 具名作用域插槽 -->
    <template #avatar="{ user, size }">
      <div class="avatar" :class="size">
        <img :src="user.avatar" :alt="user.name" />
      </div>
    </template>
    
    <template #name="{ user }">
      <h3>{{ user.name }}</h3>
    </template>
    
    <template #details="{ user }">
      <p>邮箱: {{ user.email }}</p>
      <p>角色: {{ user.role }}</p>
    </template>
    
    <template #actions="{ user, onEdit }">
      <button @click="onEdit">编辑</button>
      <button @click="deleteUser(user.id)">删除</button>
    </template>
  </UserCard>
</template>

<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'

const currentUser = ref({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  role: '管理员',
  avatar: '/avatars/zhangsan.jpg'
})

const deleteUser = (userId) => {
  console.log('删除用户:', userId)
}
</script>

<style scoped>
.avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  overflow: hidden;
}
.avatar.medium {
  width: 50px;
  height: 50px;
}
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

🔄 动态插槽名

vue

复制代码
<template>
  <DynamicComponent>
    <!-- 动态插槽名 -->
    <template v-slot:[dynamicSlotName]>
      <p>这是动态插槽的内容</p>
    </template>
    
    <!-- 使用计算属性 -->
    <template #[computedSlotName]>
      <p>这是计算属性决定的插槽内容</p>
    </template>
  </DynamicComponent>
</template>

<script setup>
import { ref, computed } from 'vue'

const dynamicSlotName = ref('header')
const slotType = ref('primary')

const computedSlotName = computed(() => {
  return `${slotType.value}-content`
})
</script>

📊 高级示例:数据表格组件

子组件 DataTable.vue

vue

复制代码
<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <!-- 动态表头 -->
          <th v-for="column in columns" :key="column.key">
            <slot name="header" :column="column">
              {{ column.title }}
            </slot>
          </th>
        </tr>
      </thead>
      <tbody>
        <!-- 动态数据行 -->
        <tr v-for="(row, index) in data" :key="row.id">
          <td v-for="column in columns" :key="column.key">
            <slot 
              name="cell" 
              :value="row[column.key]" 
              :row="row" 
              :column="column"
              :index="index"
            >
              {{ row[column.key] }}
            </slot>
          </td>
        </tr>
      </tbody>
    </table>
    
    <!-- 空状态插槽 -->
    <div v-if="data.length === 0" class="empty-state">
      <slot name="empty">
        <p>暂无数据</p>
      </slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  columns: {
    type: Array,
    default: () => []
  },
  data: {
    type: Array,
    default: () => []
  }
})
</script>

<style scoped>
.data-table {
  width: 100%;
}
table {
  width: 100%;
  border-collapse: collapse;
}
th, td {
  border: 1px solid #ddd;
  padding: 8px;
  text-align: left;
}
th {
  background-color: #f5f5f5;
}
.empty-state {
  text-align: center;
  padding: 40px;
  color: #666;
}
</style>

父组件使用数据表格

vue

复制代码
<template>
  <DataTable :columns="columns" :data="users">
    <!-- 自定义表头 -->
    <template #header="{ column }">
      <span class="header-cell">
        {{ column.title }}
        <span v-if="column.sortable" class="sort-icon">↕️</span>
      </span>
    </template>
    
    <!-- 自定义单元格内容 -->
    <template #cell="{ value, row, column }">
      <template v-if="column.key === 'avatar'">
        <img :src="value" :alt="row.name" class="avatar" />
      </template>
      <template v-else-if="column.key === 'status'">
        <span :class="['status', value]">{{ getStatusText(value) }}</span>
      </template>
      <template v-else-if="column.key === 'actions'">
        <button @click="editUser(row)">编辑</button>
        <button @click="deleteUser(row.id)">删除</button>
      </template>
      <template v-else>
        {{ value }}
      </template>
    </template>
    
    <!-- 自定义空状态 -->
    <template #empty>
      <div class="custom-empty">
        <p>📊 没有找到任何用户数据</p>
        <button @click="loadUsers">重新加载</button>
      </div>
    </template>
  </DataTable>
</template>

<script setup>
import { ref } from 'vue'
import DataTable from './DataTable.vue'

const columns = ref([
  { key: 'id', title: 'ID', sortable: true },
  { key: 'name', title: '姓名', sortable: true },
  { key: 'avatar', title: '头像' },
  { key: 'email', title: '邮箱' },
  { key: 'status', title: '状态' },
  { key: 'actions', title: '操作' }
])

const users = ref([
  {
    id: 1,
    name: '张三',
    avatar: '/avatars/1.jpg',
    email: 'zhangsan@example.com',
    status: 'active'
  },
  {
    id: 2,
    name: '李四',
    avatar: '/avatars/2.jpg',
    email: 'lisi@example.com',
    status: 'inactive'
  }
])

const getStatusText = (status) => {
  const statusMap = {
    active: '活跃',
    inactive: '非活跃'
  }
  return statusMap[status] || status
}

const editUser = (user) => {
  console.log('编辑用户:', user)
}

const deleteUser = (userId) => {
  console.log('删除用户:', userId)
}

const loadUsers = () => {
  console.log('重新加载用户数据')
}
</script>

<style scoped>
.header-cell {
  display: flex;
  align-items: center;
  gap: 4px;
}
.sort-icon {
  font-size: 12px;
}
.avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
}
.status {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 12px;
}
.status.active {
  background-color: #e8f5e8;
  color: #2e7d32;
}
.status.inactive {
  background-color: #ffebee;
  color: #c62828;
}
.custom-empty {
  text-align: center;
  padding: 40px;
}
</style>

💡 最佳实践与技巧

1. 提供合理的默认内容

vue

复制代码
<!-- 子组件 -->
<template>
  <div class="notification">
    <slot name="icon">
      <!-- 默认图标 -->
      <span class="default-icon">💡</span>
    </slot>
    <div class="content">
      <slot name="title">
        <h3>默认标题</h3>
      </slot>
      <slot name="message">
        <p>默认消息内容</p>
      </slot>
    </div>
  </div>
</template>

2. 使用条件插槽

vue

复制代码
<template>
  <Modal :show="showModal">
    <!-- 条件性渲染插槽内容 -->
    <template #header v-if="hasCustomHeader">
      <CustomHeader :title="modalTitle" />
    </template>
    
    <template #body>
      <div v-if="isLoading">
        <slot name="loading">
          <p>加载中...</p>
        </slot>
      </div>
      <div v-else>
        <slot name="content"></slot>
      </div>
    </template>
  </Modal>
</template>

3. 插槽性能优化

vue

复制代码
<template>
  <VirtualList :items="largeDataSet">
    <!-- 使用 v-memo 优化大量插槽内容 -->
    <template #item="{ item, index }">
      <div v-memo="[item.id]">
        <slot name="item-content" :item="item" :index="index">
          {{ item.name }}
        </slot>
      </div>
    </template>
  </VirtualList>
</template>

🎯 总结

Vue3 的 v-slot 提供了强大的插槽功能:

特性 语法 用途
默认插槽 <slot> 基本内容插入
具名插槽 v-slot:name#name 多个插槽区分
作用域插槽 v-slot="props" 子向父传递数据
动态插槽 v-slot:[dynamicName] 动态决定插槽名
解构语法 #default="{ prop }" 简化数据访问

关键优势

  • 更好的类型推断(配合 TypeScript)

  • 更清晰的语法(特别是简写形式)

  • 更强的灵活性(动态插槽、作用域插槽)

  • 更好的性能(优化的渲染机制)

掌握这些插槽技术可以让你创建出高度可复用和灵活的组件,大大提升开发效率。

相关推荐
FreeBuf_2 小时前
新型域名前置攻击利用Google Meet、YouTube、Chrome及GCP构建流量隧道
前端·chrome
c0detrend2 小时前
技术架构设计:如何打造一个高性能的Chrome截图插件
前端·chrome
幽络源小助理2 小时前
8、幽络源微服务项目实战:前端登录跨域同源策略处理+axios封装+权限的递归查询增删改+鉴权测试
前端·微服务·架构
API开发2 小时前
apiSQL+GoView:一个API接口开发数据大屏
前端·后端·api·数据可视化·数据大屏·apisql
运维开发王义杰2 小时前
nodejs:揭秘 npm 脚本参数 -- 的妙用与规范
前端·npm·node.js
我是日安3 小时前
从零到一打造 Vue3 响应式系统 Day 18 - Reactive:深入 Proxy 的设计思路
前端·vue.js
你的人类朋友3 小时前
🍃说说Base64
前端·后端·安全
ze_juejin3 小时前
Node.js 全局变量完整总结
前端
ttyyttemo3 小时前
Learn-Jetpack-Compose-By-Example---TextFieldValue
前端