Svelte 5 Snippets({snippet ...})完整指南:可复用标记块、作用域规则与 Snippet Props 类型系统
Svelte 5 用 Snippet 配合 {@render ...} 标签取代了 Svelte 4 的 slots 机制,成为组件间传递可复用标记(markup)的核心手段。本文基于仓库文档 06-snippet.md 的完整内容,并结合编译器分析与转换阶段的源码实现,系统讲解 snippet 的语法、词法作用域、显式/隐式 snippet props、TypeScript 类型标注、跨文件导出以及 createRawSnippet 编程式 API,帮助你在 Svelte 5 中写出类型安全、结构清晰的组合式 UI。
什么是 Snippet:语法与参数规则
Snippet 是组件内可复用标记块的基本单元,基本语法如下:
{#snippet name()}...{/snippet}
带参数的形式:
{#snippet name(param1, param2, paramN)}...{/snippet}
与函数声明类似,snippet 可以接受任意数量的参数,参数可以有默认值,也可以对每个参数进行解构。唯一的重要限制是:不能声明 rest 参数(...args)。这一限制在编译器分析阶段就会被强制校验,参见 SnippetBlock 分析访问器:
for (const arg of node.parameters) {
if (arg.type === 'RestElement') {
e.snippet_invalid_rest_parameter(arg);
}
}
snippet 通过 {@render ...} 标签来渲染,该标签的详细规则(包括表达式形式与可选渲染)见 {@render ...} 文档:
{#snippet sum(a, b)}
<p>{a} + {b} = {a + b}</p>
{/snippet}
{@render sum(1, 2)}
{@render sum(3, 4)}
{@render sum(5, 6)}
先看 snippet 解决什么问题。假设你要为每张图片渲染一个 <figure>,部分图片带链接、部分不带:
{#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}
<figure> 结构被完整复制了两份。提取成 snippet 后:
{#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}
重复代码消失了,而 <figure> 的模板只维护一份。
Snippet 作用域:词法可见性
Snippet 可以声明在组件内的任何位置。它可以引用自身外部的值——无论是 <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')}
注意 snippet 内部对 message 的引用发生在调用时的词法环境中,而不是声明位置。
Snippet 对同一词法作用域内的一切(即兄弟节点、以及这些兄弟节点的子节点)可见:
<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()}
从源码结构看,snippet 在编译后就是作用域内的一个普通函数绑定,因此可见性规则完全等价于 JS 的词法作用域:y 只存在于 x 的函数体内,x 的声明也只在其所在的 <div> 片段层级可见。
Snippet 也可以引用自身和其他 snippet,实现递归:
{#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)}
把 Snippet 传给组件
在模板中,snippet 就是普通的值,因此可以像其他值一样作为 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
作为一种书写便利:直接写在组件标签内部的 snippet,会隐式成为该组件的 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>
Table.svelte 与前面完全一致。这一隐式机制由客户端转换阶段的组件访问器实现,见 shared/component.js:遍历组件 children 时,遇到 SnippetBlock 会直接向组件 props 中 push_prop(b.prop('init', child.expression, child.expression))——也就是以 snippet 同名属性传递,无需书写方手写 prop 名。同一处代码还保留了与旧 slots 的互操作:向仍在使用 <slot> 的子组件传递 snippet 时,会同时生成 $$slots 序列化标记(children 映射为 default slot),保证 Svelte 4/5 混用期间的平滑过渡。
隐式 children snippet
组件标签内部不是 snippet 声明的内容,会隐式成为 children snippet 的一部分:
<!-- 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>
从源码结构看,组件转换时对非 snippet 的文本/元素内容会生成一个包装函数并通过 $.wrap_snippet 包成 children prop;若子组件内部还使用旧式 <slot> 或 <svelte:fragment>,编译器会同时写入 $$slots.default。
注意:如果组件标签内有非 snippet 内容,就不能再声明一个名为
children的 prop——因此应避免使用这个名称的 prop。编译器在 SnippetBlock 分析访问器 中对冲突场景有明确报错:显式{#snippet children}与组件内的其他非 snippet 内容并存时触发snippet_conflict;而隐式 snippet 与同名属性/绑定并存时触发snippet_shadowing_prop。
可选的 Snippet props
snippet prop 可能为 undefined(未传入时)。两种处理方式:
使用可选链,未设置时什么都不渲染:
<script>
let { children } = $props();
</script>
{@render children?.()}
或者使用 {#if} 块渲染回退内容:
<script>
let { children } = $props();
</script>
{#if children}
{@render children()}
{:else}
fallback content
{/if}
从运行时实现看,{@render ...} 编译后调用的是 客户端 snippet 块 中的 snippet() 函数:它用 BranchManager 管理分支,并先做 get_snippet() ?? null 归一化,所以 children?.() 与 children() 在不传入时不会渲染任何内容;开发模式下若最终解析出的 snippet 为 null,会抛出 invalid_snippet 错误,帮助你在开发阶段尽早发现问题。
类型标注:Snippet 接口
Snippet 实现了从 'svelte' 导入的 Snippet 接口。该接口的完整定义见 index.d.ts,其签名为:
export interface Snippet<Parameters extends unknown[] = []> {
(
this: void,
...args: number extends Parameters['length'] ? never : Parameters
): {
'{@render ...} must be called with a Snippet': "import type { Snippet } from 'svelte'";
} & typeof SnippetReturn;
}
两个值得注意的细节(可直接从类型源码读出):
- 泛型参数是元组(tuple),因为 snippet 可以有多个参数;类型中的
number extends Parameters['length'] ? never : Parameters条件专门用于只接受定长元组、拒绝数组类型(数组意味着 rest 参数,而 snippet 不支持 rest 参数); - 返回类型带
typeof SnippetReturn唯一符号标记,使编译器/语言服务能强制「只能通过{@render ...}调用 snippet」,误用在普通 JS 表达式处会得到「{@render ...} must be called with a 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 的情况下使用该组件,编辑器会直接报红。
还可以用泛型进一步收紧,使 data 与 row 引用同一类型:
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
let {
data,
children,
row
}: {
data: T[];
children: Snippet;
row: Snippet<[T]>;
} = $props();
</script>
这样 row 的参数类型会自动跟随 data 的元素类型 T 推导,{@render row(d)} 处的 d 也具备精确类型。
导出 Snippet
.svelte 文件顶层声明的 snippet,可以通过 <script module> 导出,供其他组件使用。前提是它不引用非模块 <script> 中的任何声明(无论直接还是经由其他 snippet 间接引用):
<!-- 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}
该特性要求 Svelte 5.5.0 或更新版本。
从源码看,「顶层」与「可提升(可导出)」的判断在 SnippetBlock 分析访问器 的 can_hoist_snippet 函数中完成:它会递归检查 snippet 作用域内所有引用的绑定,只要引用了实例级(函数深度大于 0)且带 blocker 的绑定,就不能提升到模块层;能提升的 snippet 会被注册进 module scope 的 declarations。在转换阶段,客户端 SnippetBlock 访问器 根据 node.metadata.can_hoist 决定声明进入 module_level_snippets 还是 instance_level_snippets:
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
if (node.metadata.can_hoist) {
context.state.module_level_snippets.push(declaration);
} else {
context.state.instance_level_snippets.push(declaration);
}
}
这解释了文档中「不能引用非模块 <script> 声明」的约束:模块层声明在组件实例化之前求值,若依赖实例状态必然不可行,因此编译器直接在分析期就拒绝提升。
编程式 Snippet:createRawSnippet
对于高级场景,可以用 createRawSnippet API 编程式地创建 snippet。该 API 在客户端与服务器端分别导出:
- 客户端:index-client.js 中
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; - 服务器端:index-server.js 中
export { createRawSnippet } from './internal/server/blocks/snippet.js';
两端实现签名一致,传入一个接收参数、返回 { render, setup? } 的函数:
- 客户端实现(snippet.js):调用
fn(...params)后执行render().trim()得到 HTML,经create_fragment_from_html创建片段并插入到锚点前;若处于 hydration 模式,则直接复用已存在的服务器渲染节点。setup(element)的返回值若是函数,会被注册为 teardown; - 服务器端实现(snippet.js):将
render().trim()的结果 push 进渲染流。
一个约束值得特别注意:传给 createRawSnippet 的 render 函数应当返回单个元素(single element)的 HTML,开发模式下若渲染出多个兄弟节点或非元素节点,会触发 invalid_raw_snippet_render 警告(见 客户端警告):
[svelte] invalid_raw_snippet_render
The `render` function passed to `createRawSnippet` should return HTML for a single element
Snippet 与 Slots 的关系
在 Svelte 4 中,向组件传递内容使用 slots。Snippet 更强大也更灵活,因此 slots 在 Svelte 5 中已被废弃。从源码也能看到两者的桥接关系:隐式 snippet props 的转换逻辑会同时生成 $$slots 序列化(children 对应 default),使得 Svelte 5 的 snippet 可以传给仍在使用 <slot> 的旧组件;反向亦然,旧版 slot 写法在迁移场景下仍受支持(相关废弃说明见 legacy 文档)。
小结:关键规则速查
| 主题 | 规则 | 依据 |
|---|---|---|
| 参数 | 任意数量,可默认值、可解构,禁止 rest 参数 | 编译期 snippet_invalid_rest_parameter 报错 |
| 渲染 | 只能通过 {@render ...} 调用 |
Snippet 返回类型的 SnippetReturn 唯一符号 |
| 作用域 | 与普通词法作用域一致,可互相/自我引用 | 编译后为作用域内普通函数绑定 |
| 组件传参 | 显式 prop、组件内隐式同名 prop、非 snippet 内容归入 children |
组件转换中的 push_prop 与 wrap_snippet |
| 可选 snippet | children?.() 或 {#if children} ... {:else} |
客户端 snippet() 的 ?? null 归一化 |
| 类型 | Snippet<[P1, P2]> 元组泛型 + 组件 generics |
index.d.ts |
| 导出 | 顶层 snippet + <script module> export,需 Svelte ≥ 5.5.0 |
can_hoist_snippet 提升检查 |
| 编程式 | createRawSnippet,render 须返回单元素 HTML |
客户端/服务器端实现与 invalid_raw_snippet_render 警告 |
掌握以上内容后,你就可以在 Svelte 5 中用 snippet 替代一切「复制粘贴的标记块」、以类型安全的方式设计可组合的组件 API,并在需要时通过 createRawSnippet 打通模板与命令式代码的边界。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00