插槽Slot #

一、Slot 概述 #

Slot(插槽)是组件内容分发的机制,允许父组件向子组件传递模板内容。

text
┌─────────────────────────────────────────────────────────────┐
│                    Slot 工作原理                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  父组件                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <Card>                                             │   │
│  │    <h2>Title</h2>        ─────┐                     │   │
│  │    <p>Content</p>        ─────┼──→ 传递给子组件     │   │
│  │  </Card>                        │                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                      ↓                      │
│  子组件 (Card.svelte)                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <div class="card">                                 │   │
│  │    <slot />  ← 接收并渲染内容                        │   │
│  │  </div>                                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、默认插槽 #

2.1 基本用法 #

Card.svelte:

svelte
<div class="card">
  <slot />
</div>

<style>
  .card {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
  }
</style>

使用:

svelte
<script>
  import Card from './Card.svelte';
</script>

<Card>
  <h2>Card Title</h2>
  <p>Card content goes here.</p>
</Card>

2.2 默认内容 #

svelte
<div class="card">
  <slot>
    <p>Default content when no slot provided</p>
  </slot>
</div>

使用:

svelte
<Card />

<Card>
  <p>Custom content</p>
</Card>

三、具名插槽 #

3.1 定义具名插槽 #

Layout.svelte:

svelte
<div class="layout">
  <header>
    <slot name="header" />
  </header>
  
  <main>
    <slot />
  </main>
  
  <footer>
    <slot name="footer" />
  </footer>
</div>

<style>
  .layout {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
  
  header, footer {
    padding: 1rem;
    background: #f5f5f5;
  }
  
  main {
    flex: 1;
    padding: 1rem;
  }
</style>

3.2 使用具名插槽 #

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

<Layout>
  <h1 slot="header">Page Header</h1>
  
  <p>Main content goes here.</p>
  <p>More content...</p>
  
  <p slot="footer">© 2024 My App</p>
</Layout>

3.3 完整卡片组件 #

Card.svelte:

svelte
<script>
  let { title = '' } = $props();
</script>

<article class="card">
  {#if title}
    <header>
      <h2>{title}</h2>
      <slot name="actions" />
    </header>
  {/if}
  
  <div class="content">
    <slot name="image" />
    <slot />
  </div>
  
  <footer>
    <slot name="footer" />
  </footer>
</article>

<style>
  .card {
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    overflow: hidden;
    background: white;
  }
  
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem;
    border-bottom: 1px solid #e0e0e0;
  }
  
  h2 {
    margin: 0;
    font-size: 1.25rem;
  }
  
  .content {
    padding: 1rem;
  }
  
  footer {
    padding: 1rem;
    border-top: 1px solid #e0e0e0;
    background: #f9f9f9;
  }
</style>

使用:

svelte
<script>
  import Card from './Card.svelte';
</script>

<Card title="Product Name">
  <button slot="actions">Edit</button>
  
  <img slot="image" src="product.jpg" alt="Product" />
  
  <p>Product description goes here.</p>
  
  <div slot="footer">
    <span>$99.99</span>
    <button>Add to Cart</button>
  </div>
</Card>

四、插槽 Props #

4.1 传递数据给插槽 #

List.svelte:

svelte
<script>
  let { items = [] } = $props();
</script>

<ul>
  {#each items as item, index}
    <li>
      <slot {item} {index} />
    </li>
  {/each}
</ul>

4.2 接收插槽 Props #

svelte
<script>
  import List from './List.svelte';
  
  let items = [
    { id: 1, name: 'Apple', price: 1.5 },
    { id: 2, name: 'Banana', price: 2.0 },
    { id: 3, name: 'Orange', price: 1.8 }
  ];
</script>

<List {items} let:item let:index>
  <span>{index + 1}. {item.name}</span>
  <span>${item.price}</span>
</List>

4.3 表格组件示例 #

Table.svelte:

svelte
<script>
  let { 
    data = [],
    columns = []
  } = $props();
</script>

<table>
  <thead>
    <tr>
      {#each columns as column}
        <th>{column.label}</th>
      {/each}
    </tr>
  </thead>
  <tbody>
    {#each data as row, rowIndex}
      <tr>
        {#each columns as column, colIndex}
          <td>
            <slot 
              name="cell"
              {row} 
              {column} 
              {rowIndex}
              {colIndex}
              value={row[column.key]}
            >
              {row[column.key]}
            </slot>
          </td>
        {/each}
      </tr>
    {/each}
  </tbody>
</table>

使用:

svelte
<script>
  import Table from './Table.svelte';
  
  let columns = [
    { key: 'name', label: 'Name' },
    { key: 'email', label: 'Email' },
    { key: 'role', label: 'Role' }
  ];
  
  let data = [
    { id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
    { id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' }
  ];
</script>

<Table {data} {columns}>
  <span slot="cell" let:row let:value>
    {#if value === 'Admin'}
      <strong class="admin">{value}</strong>
    {:else}
      {value}
    {/if}
  </span>
</Table>

五、Svelte 5 Snippet #

5.1 基本语法 #

Svelte 5 引入了 Snippet 作为新的内容分发方式:

svelte
<script>
  let { header, footer, children } = $props();
</script>

<div class="card">
  {#if header}
    <header>
      {@render header()}
    </header>
  {/if}
  
  <main>
    {@render children()}
  </main>
  
  {#if footer}
    <footer>
      {@render footer()}
    </footer>
  {/if}
</div>

5.2 定义和使用 Snippet #

svelte
<script>
  import Card from './Card.svelte';
  
  let header = snippet(() => (
    <h1>Page Title</h1>
  ));
  
  let footer = snippet(() => (
    <p>© 2024</p>
  ));
</script>

<Card {header} {footer}>
  <p>Main content</p>
</Card>

5.3 带参数的 Snippet #

svelte
<script>
  let { items, renderItem } = $props();
</script>

<ul>
  {#each items as item, index}
    <li>
      {@render renderItem(item, index)}
    </li>
  {/each}
</ul>

使用:

svelte
<script>
  import List from './List.svelte';
  
  let items = ['Apple', 'Banana', 'Orange'];
  
  let renderItem = snippet((item, index) => (
    <span>
      {index + 1}. {item}
    </span>
  ));
</script>

<List {items} renderItem={renderItem} />

5.4 Snippet vs Slot 对比 #

特性 Slot Snippet
语法 <slot /> {@render children()}
具名 slot="name" 单独的 prop
数据传递 let:item 函数参数
类型安全 较弱 较强
Svelte 版本 4 及以下 5 及以上

六、高级用法 #

6.1 递归组件 #

Tree.svelte:

svelte
<script>
  let { node, depth = 0 } = $props();
</script>

<div class="tree-node" style="padding-left: {depth * 20}px">
  <slot {node} {depth} />
  
  {#if node.children}
    {#each node.children as child}
      <svelte:self node={child} depth={depth + 1} />
    {/each}
  {/if}
</div>

使用:

svelte
<script>
  import Tree from './Tree.svelte';
  
  let data = {
    name: 'Root',
    children: [
      { name: 'Child 1', children: [{ name: 'Grandchild 1' }] },
      { name: 'Child 2' }
    ]
  };
</script>

<Tree node={data} let:node>
  <span>{node.name}</span>
</Tree>

6.2 条件插槽 #

svelte
<script>
  import { getContext } from 'svelte';
  
  let { hasHeader = false } = $props();
  
  $effect(() => {
    hasHeader = $$slots.header;
  });
</script>

<div class="card">
  {#if hasHeader}
    <header>
      <slot name="header" />
    </header>
  {/if}
  
  <slot />
</div>

6.3 插槽组合 #

Modal.svelte:

svelte
<script>
  let { isOpen = false, onClose } = $props();
</script>

{#if isOpen}
  <div class="modal-overlay" onclick={onClose}>
    <div class="modal" onclick={(e) => e.stopPropagation()}>
      <header>
        <slot name="header">
          <h2>Modal Title</h2>
        </slot>
        <button class="close" onclick={onClose}>×</button>
      </header>
      
      <div class="modal-body">
        <slot />
      </div>
      
      <footer>
        <slot name="footer">
          <button onclick={onClose}>Close</button>
        </slot>
      </footer>
    </div>
  </div>
{/if}

<style>
  .modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .modal {
    background: white;
    border-radius: 8px;
    max-width: 500px;
    width: 100%;
  }
  
  header {
    display: flex;
    justify-content: space-between;
    padding: 1rem;
    border-bottom: 1px solid #eee;
  }
  
  .modal-body {
    padding: 1rem;
  }
  
  footer {
    padding: 1rem;
    border-top: 1px solid #eee;
    text-align: right;
  }
  
  .close {
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
  }
</style>

七、实际应用示例 #

7.1 可复用列表组件 #

VirtualList.svelte:

svelte
<script>
  let { 
    items = [],
    itemHeight = 50,
    containerHeight = 400
  } = $props();
  
  let scrollTop = $state(0);
  
  let visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
  let startIndex = $derived(Math.floor(scrollTop / itemHeight));
  let endIndex = $derived(Math.min(startIndex + visibleCount, items.length));
  
  let visibleItems = $derived(
    items.slice(startIndex, endIndex)
  );
  
  let totalHeight = items.length * itemHeight;
  let offsetY = $derived(startIndex * itemHeight);
</script>

<div 
  class="virtual-list" 
  style="height: {containerHeight}px"
  onscroll={(e) => scrollTop = e.target.scrollTop}
>
  <div style="height: {totalHeight}px; position: relative;">
    <div style="position: absolute; top: {offsetY}px; width: 100%;">
      {#each visibleItems as item, i (startIndex + i)}
        <div style="height: {itemHeight}px">
          <slot {item} index={startIndex + i} />
        </div>
      {/each}
    </div>
  </div>
</div>

<style>
  .virtual-list {
    overflow-y: auto;
    border: 1px solid #ddd;
  }
</style>

使用:

svelte
<script>
  import VirtualList from './VirtualList.svelte';
  
  let items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));
</script>

<VirtualList {items} let:item let:index>
  <div class="item">
    <span>{index}: {item.name}</span>
  </div>
</VirtualList>

7.2 布局组件 #

AppLayout.svelte:

svelte
<script>
  let { sidebarWidth = 250 } = $props();
  
  let sidebarOpen = $state(true);
</script>

<div class="layout">
  <aside class:open={sidebarOpen} style="width: {sidebarWidth}px">
    <slot name="sidebar" {sidebarOpen} />
  </aside>
  
  <main>
    <header>
      <button onclick={() => sidebarOpen = !sidebarOpen}>
        {sidebarOpen ? '◀' : '▶'}
      </button>
      <slot name="header" />
    </header>
    
    <div class="content">
      <slot />
    </div>
    
    <footer>
      <slot name="footer" />
    </footer>
  </main>
</div>

<style>
  .layout {
    display: flex;
    min-height: 100vh;
  }
  
  aside {
    background: #f5f5f5;
    transition: transform 0.3s;
  }
  
  aside:not(.open) {
    transform: translateX(-100%);
  }
  
  main {
    flex: 1;
    display: flex;
    flex-direction: column;
  }
  
  header {
    display: flex;
    align-items: center;
    padding: 1rem;
    border-bottom: 1px solid #eee;
  }
  
  .content {
    flex: 1;
    padding: 1rem;
  }
  
  footer {
    padding: 1rem;
    border-top: 1px solid #eee;
  }
</style>

八、总结 #

类型 语法 说明
默认插槽 <slot /> 接收默认内容
具名插槽 <slot name="header" /> 接收指定名称的内容
默认内容 <slot>默认</slot> 无内容时显示
插槽Props <slot {item} /> 向父组件传递数据
Snippet {@render children()} Svelte 5 新语法

Slot 使用要点:

  • 使用 <slot> 定义插槽位置
  • 使用 name 属性创建具名插槽
  • 使用 let: 接收插槽传递的数据
  • Svelte 5 推荐使用 Snippet 语法
  • 合理设计插槽结构提高组件复用性
最后更新:2026-03-28