{#snippet ...}

<!--- copy: false  --->
{#snippet name()}...{/snippet}
<!--- copy: false  --->
{#snippet name(param1, param2, paramN)}...{/snippet}

代码片段(snippets)与渲染标签是一种在组件内部创建可复用标记片段的方式。与其编写像这样的重复代码……

{#each images as image}
	{#if image.href}
		<a href={image.href}>
			<figure>
				<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
				<figcaption>{image.caption}</figcaption>
			</figure>
		</a>
	{:else}
		<figure>
			<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
			<figcaption>{image.caption}</figcaption>
		</figure>
	{/if}
{/each}

……你可以编写像这样的代码:

{#snippet figure(image)}
	<figure>
		<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
		<figcaption>{image.caption}</figcaption>
	</figure>
{/snippet}

{#each images as image}
	{#if image.href}
		<a href={image.href}>
			{@render figure(image)}
		</a>
	{:else}
		{@render figure(image)}
	{/if}
{/each}

与函数声明一样,代码片段可以拥有任意数量的参数,参数可以有默认值,你也可以对每个参数进行解构。不过,你不能使用剩余参数。

代码片段作用域

代码片段可以声明在组件内部的任何位置。它们可以引用自身之外声明的值,例如 <script> 标签内或 {#each ...} 块中的值……

<!--- file: App.svelte --->
<script>
	let { message = `it's great to see you!` } = $props();
</script>

{#snippet hello(name)}
	<p>hello {name}! {message}!</p>
{/snippet}

{@render hello('alice')}
{@render hello('bob')}

……并且它们对同一个词法作用域内的所有内容都"可见"(即同级元素,以及这些同级元素的子元素):

<div>
	{#snippet x()}
		{#snippet y()}...{/snippet}

		<!-- this is fine -->
		{@render y()}
	{/snippet}

	<!-- this will error, as `y` is not in scope -->
	{@render y()}
</div>

<!-- this will also error, as `x` is not in scope -->
{@render x()}

代码片段可以引用自身以及彼此:

<!--- file: App.svelte --->
{#snippet blastoff()}
	<span>🚀</span>
{/snippet}

{#snippet countdown(n)}
	{#if n > 0}
		<span>{n}...</span>
		{@render countdown(n - 1)}
	{:else}
		{@render blastoff()}
	{/if}
{/snippet}

{@render countdown(10)}

将代码片段传递给组件

显式 props

在模板中,代码片段与其他任何值一样,都是值。因此,它们可以像 props 一样传递给组件:

<!--- file: App.svelte --->
<script>
	import Table from './Table.svelte';

	const fruits = [
		{ name: 'apples', qty: 5, price: 2 },
		{ name: 'bananas', qty: 10, price: 1 },
		{ name: 'cherries', qty: 20, price: 0.5 }
	];
</script>

{#snippet header()}
	<th>fruit</th>
	<th>qty</th>
	<th>price</th>
	<th>total</th>
{/snippet}

{#snippet row(d)}
	<td>{d.name}</td>
	<td>{d.qty}</td>
	<td>{d.price}</td>
	<td>{d.qty * d.price}</td>
{/snippet}

<Table data={fruits} {header} {row} />
<!--- file: Table.svelte --->
<script>
	let { data, header, row } = $props();
</script>

<table>
	{#if header}
		<thead>
			<tr>{@render header()}</tr>
		</thead>
	{/if}

	<tbody>
		{#each data as d}
			<tr>{@render row(d)}</tr>
		{/each}
	</tbody>
</table>

<style>
	table {
		text-align: left;
		border-spacing: 0;
	}

	tbody tr:nth-child(2n+1) {
		background: ButtonFace;
	}

	table :global(th), table :global(td) {
		padding: 0.5em;
	}
</style>

可以把它理解为:向组件传递的是内容,而不是数据。这个概念类似于 Web Components 中的插槽(slots)。

隐式 props

作为一种编写便利,直接声明在组件_内部_的代码片段会隐式成为该组件上的 props:

<!--- file: App.svelte --->
<script>
	import Table from './Table.svelte';

	const fruits = [
		{ name: 'apples', qty: 5, price: 2 },
		{ name: 'bananas', qty: 10, price: 1 },
		{ name: 'cherries', qty: 20, price: 0.5 }
	];
</script>

<Table data={fruits}>
	{#snippet header()}
		<th>fruit</th>
		<th>qty</th>
		<th>price</th>
		<th>total</th>
	{/snippet}

	{#snippet row(d)}
		<td>{d.name}</td>
		<td>{d.qty}</td>
		<td>{d.price}</td>
		<td>{d.qty * d.price}</td>
	{/snippet}
</Table>
<!--- file: Table.svelte --->
<script>
	let { data, header, row } = $props();
</script>

<table>
	{#if header}
		<thead>
			<tr>{@render header()}</tr>
		</thead>
	{/if}

	<tbody>
		{#each data as d}
			<tr>{@render row(d)}</tr>
		{/each}
	</tbody>
</table>

<style>
	table {
		text-align: left;
		border-spacing: 0;
	}

	tbody tr:nth-child(2n+1) {
		background: ButtonFace;
	}

	table :global(th), table :global(td) {
		padding: 0.5em;
	}
</style>

隐式 children 代码片段

组件标签内部任何_不是_代码片段声明的内容,都会隐式成为 children 代码片段的一部分:

<!--- file: App.svelte --->
<script>
	import Button from './Button.svelte';
</script>

<Button>click me</Button>
<!--- file: Button.svelte --->
<script>
	let { children } = $props();
</script>

<!-- result will be <button>click me</button> -->
<button>{@render children()}</button>

[!NOTE] 注意,如果你在组件内部同时有内容,就不能再拥有一个名为 children 的 prop——因此,你应该避免使用该名称的 props。

可选的代码片段 props

你可以将代码片段 props 声明为可选。你既可以使用可选链,在代码片段未被设置时不渲染任何内容……

<script>
    let { children } = $props();
</script>

{@render children?.()}

……也可以使用 #if 块来渲染回退内容:

<script>
    let { children } = $props();
</script>

{#if children}
    {@render children()}
{:else}
    fallback content
{/if}

为代码片段添加类型

代码片段实现了从 'svelte' 导入的 Snippet 接口:

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		data: any[];
		children: Snippet;
		row: Snippet<[any]>;
	}

	let { data, children, row }: Props = $props();
</script>

做了这个改动后,如果你尝试在不提供 data prop 和 row 代码片段的情况下使用该组件,就会出现红色波浪线提示。注意,提供给 Snippet 的类型参数是一个元组(tuple),因为代码片段可以有多个参数。

我们可以通过声明一个泛型进一步收紧类型,使 datarow 引用相同的类型:

<script lang="ts" generics="T">
	import type { Snippet } from 'svelte';

	let {
		data,
		children,
		row
	}: {
		data: T[];
		children: Snippet;
		row: Snippet<[T]>;
	} = $props();
</script>

导出代码片段

.svelte 文件顶层声明的代码片段,可以从 <script module> 中导出,供其他组件使用,前提是它们没有直接或间接地(通过其他代码片段)引用任何非 module 的 <script> 中的声明:

<!--- file: App.svelte --->
<script>
	import { add } from './snippets.svelte';
</script>

{@render add(1, 2)}
<!--- file: snippets.svelte --->
<script module>
	export { add };
</script>

{#snippet add(a, b)}
	{a} + {b} = {a + b}
{/snippet}

[!NOTE] 这需要 Svelte 5.5.0 或更高版本

编程式代码片段

代码片段可以通过 createRawSnippet API 以编程方式创建。这是为高级用例准备的。

代码片段与插槽

在 Svelte 4 中,内容可以使用插槽传递给组件。代码片段更强大、更灵活,因此在 Svelte 5 中插槽已被弃用。