插槽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