Skip to content
第 118 / 250 章前端⏱ 10 分钟阅读

第 118 章:插槽与作用域

学习目标

  • 掌握插槽的各种用法(默认/具名/作用域)
  • 理解插槽的渲染机制
  • 学会 useSlots 与作用域插槽
  • 掌握动态插槽与插槽透传

一、插槽基础

1.1 为什么需要插槽

插槽让父组件能自定义子组件的内容

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <slot></slot>  <!-- 占位 -->
  </div>
</template>
vue
<!-- 使用 -->
<Card>
  <h2>标题</h2>
  <p>内容</p>
</Card>

1.2 默认内容

vue
<!-- SubmitButton.vue -->
<template>
  <button class="submit-btn">
    <slot>提交</slot>  <!-- 默认内容 -->
  </button>
</template>
vue
<!-- 不传内容显示默认 -->
<SubmitButton />

<!-- 传内容覆盖默认 -->
<SubmitButton>立即购买</SubmitButton>

二、具名插槽

2.1 子组件定义多个插槽

vue
<!-- PageLayout.vue -->
<template>
  <div class="page">
    <header>
      <slot name="header">默认头部</slot>
    </header>

    <main>
      <slot />  <!-- 默认插槽 -->
    </main>

    <aside>
      <slot name="sidebar" />
    </aside>

    <footer>
      <slot name="footer">默认底部</slot>
    </footer>
  </div>
</template>

2.2 父组件填充

vue
<PageLayout>
  <!-- 默认插槽 -->
  <template #default>
    <p>主体内容</p>
  </template>

  <!-- 具名插槽 -->
  <template #header>
    <h1>网站标题</h1>
  </template>

  <template #sidebar>
    <nav>侧边导航</nav>
  </template>

  <template #footer>
    <p>© 2026</p>
  </template>
</PageLayout>

2.3 简写形式

vue
<!-- 完整写法 -->
<template v-slot:header>...</template>

<!-- 简写 -->
<template #header>...</template>

<!-- 默认插槽 -->
<template v-slot:default>...</template>
<template #default>...</template>

三、作用域插槽

3.1 子组件传数据给插槽

vue
<!-- UserList.vue -->
<script setup lang="ts">
interface User {
  id: number;
  name: string;
  age: number;
}

defineProps<{ users: User[] }>();
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- 把 user 暴露给父组件 -->
      <slot :user="user" :index="user.id" :isAdult="user.age >= 18">
        {{ user.name }}  <!-- 默认内容 -->
      </slot>
    </li>
  </ul>
</template>

3.2 父组件接收数据

vue
<UserList :users="userList">
  <template #default="{ user, index, isAdult }">
    <div>
      <span>{{ index }}. {{ user.name }}</span>
      <span v-if="isAdult">(成年)</span>
    </div>
  </template>
</UserList>

3.3 解构重命名

vue
<template #default="{ user: u, index: i }">
  <p>{{ i }}. {{ u.name }}</p>
</template>

四、useSlots 与 useAttrs

4.1 useSlots

vue
<!-- FancyButton.vue -->
<script setup lang="ts">
import { useSlots, computed } from 'vue';

const slots = useSlots();

// 检测是否有插槽内容
const hasContent = computed(() => !!slots.default);
</script>

<template>
  <button>
    <span v-if="hasContent">
      <slot />
    </span>
    <span v-else>默认按钮</span>
  </button>
</template>

4.2 useAttrs

vue
<!-- Wrapper.vue -->
<script setup lang="ts">
import { useAttrs } from 'vue';

const attrs = useAttrs();
console.log(attrs);  // 所有非 props 的 attribute
</script>

<template>
  <input v-bind="attrs" />
</template>

五、动态插槽

5.1 动态插槽名

vue
<!-- TabContainer.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const activeTab = ref('home');
</script>

<template>
  <div>
    <button @click="activeTab = 'home'">Home</button>
    <button @click="activeTab = 'profile'">Profile</button>

    <!-- 动态插槽 -->
    <slot :name="activeTab" />
  </div>
</template>
vue
<TabContainer>
  <template #[activeTab]>
    <p>当前标签的内容</p>
  </template>
</TabContainer>

六、插槽透传

6.1 多层组件透传

vue
<!-- Inner.vue -->
<template>
  <div class="inner">
    <slot />
  </div>
</template>
vue
<!-- Middle.vue -->
<template>
  <Inner>
    <slot />  <!-- 透传给 Inner -->
  </Inner>
</template>
vue
<!-- App.vue -->
<template>
  <Middle>
    <p>最终内容</p>
  </Middle>
</template>

6.2 包裹并扩展

vue
<!-- EnhancedButton.vue -->
<script setup lang="ts">
defineProps<{ label: string }>();
</script>

<template>
  <button>
    <!-- 在前面加图标 -->
    <span class="icon">🔥</span>

    <!-- 透传默认插槽 -->
    <slot>{{ label }}</slot>
  </button>
</template>

七、实战案例

7.1 数据表格

vue
<!-- DataTable.vue -->
<script setup lang="ts" generic="T extends Record<string, any>">
interface Column<T> {
  key: keyof T;
  title: string;
  width?: string;
}

defineProps<{
  data: T[];
  columns: Column<T>[];
}>();
</script>

<template>
  <table>
    <thead>
      <tr>
        <th
          v-for="col in columns"
          :key="String(col.key)"
          :style="{ width: col.width }"
        >
          {{ col.title }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, index) in data" :key="index">
        <td v-for="col in columns" :key="String(col.key)">
          <!-- 把行数据和列定义暴露出去 -->
          <slot
            :name="`cell-${String(col.key)}`"
            :row="row"
            :column="col"
            :value="row[col.key]"
          >
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

7.2 使用

vue
<script setup lang="ts">
import DataTable from './DataTable.vue';

interface User {
  id: number;
  name: string;
  age: number;
}

const users = ref<User[]>([
  { id: 1, name: 'Tom', age: 18 },
  { id: 2, name: 'Jerry', age: 20 }
]);

const columns = [
  { key: 'id', title: 'ID' },
  { key: 'name', title: '姓名' },
  { key: 'age', title: '年龄' }
];
</script>

<template>
  <DataTable :data="users" :columns="columns">
    <!-- 自定义姓名列 -->
    <template #cell-name="{ row }">
      <strong style="color: blue">{{ row.name }}</strong>
    </template>

    <!-- 自定义年龄列 -->
    <template #cell-age="{ value }">
      <span v-if="value >= 18" style="color: green">{{ value }} 岁</span>
      <span v-else style="color: orange">{{ value }} 岁</span>
    </template>
  </DataTable>
</template>

7.3 模态框

vue
<!-- Modal.vue -->
<script setup lang="ts">
defineProps<{ visible: boolean; title: string }>();
defineEmits<{ 'update:visible': [value: boolean] }>();
</script>

<template>
  <Teleport to="body">
    <div v-if="visible" class="modal-mask" @click.self="$emit('update:visible', false)">
      <div class="modal-container">
        <header class="modal-header">
          <h2>{{ title }}</h2>
          <button @click="$emit('update:visible', false)">×</button>
        </header>

        <main class="modal-body">
          <slot />  <!-- 默认内容 -->
        </main>

        <footer class="modal-footer">
          <slot name="footer">
            <button @click="$emit('update:visible', false)">关闭</button>
          </slot>
        </footer>
      </div>
    </div>
  </Teleport>
</template>
vue
<Modal v-model:visible="showModal" title="确认操作">
  <p>你确定要删除这条记录吗?</p>

  <template #footer>
    <button @click="showModal = false">取消</button>
    <button @click="confirmDelete">确认</button>
  </template>
</Modal>

八、常见错误

8.1 模板插槽名拼写错误

vue
<!-- ❌ 子组件没有 header 插槽 -->
<template #headr>...</template>

<!-- ✅ 子组件定义了 header 插槽 -->
<template #header>...</template>

8.2 作用域插槽没解构

vue
<!-- ❌ 直接用 slotProps.user -->
<UserList #default="slotProps">
  {{ slotProps.user.name }}
</UserList>

<!-- ✅ 解构更清晰 -->
<UserList #default="{ user }">
  {{ user.name }}
</UserList>

8.3 多个默认插槽

vue
<!-- ❌ 不允许 -->
<template>
  <MyComponent>
    <template #default>A</template>
    <template #default>B</template>
  </MyComponent>
</template>

九、本章小结

类型用途语法
默认插槽一处可定制内容<slot />
具名插槽多处不同位置内容<slot name="x" />
作用域插槽子传数据给父<slot :data="x" />
动态插槽运行时决定插槽<template #[name]>
插槽透传多层组件传内容直接 <slot />

动手练习

  1. 数据列表:实现一个列表组件,父组件自定义每一项的渲染
  2. 表格:写一个通用表格组件,支持自定义列渲染
  3. 模态框:实现一个 Modal,支持自定义 header/body/footer
  4. 布局组件:实现一个 Layout,支持多个插槽透传

推荐阅读


下一章第 119 章:组件通信模式

本站基于 VitePress 构建 · 由 Codebook 团队维护