首页
/ Svelte 5 Snippets({snippet ...})完整指南:可复用标记块、作用域规则与 Snippet Props 类型系统

Svelte 5 Snippets({snippet ...})完整指南:可复用标记块、作用域规则与 Snippet Props 类型系统

2026-09-05 14:19:34作者:宣海椒Queenly

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;
}

两个值得注意的细节(可直接从类型源码读出):

  1. 泛型参数是元组(tuple),因为 snippet 可以有多个参数;类型中的 number extends Parameters['length'] ? never : Parameters 条件专门用于只接受定长元组、拒绝数组类型(数组意味着 rest 参数,而 snippet 不支持 rest 参数);
  2. 返回类型带 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 的情况下使用该组件,编辑器会直接报红。

还可以用泛型进一步收紧,使 datarow 引用同一类型:

<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.jsexport { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
  • 服务器端:index-server.jsexport { 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 进渲染流。

一个约束值得特别注意:传给 createRawSnippetrender 函数应当返回单个元素(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_propwrap_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 提升检查
编程式 createRawSnippetrender 须返回单元素 HTML 客户端/服务器端实现与 invalid_raw_snippet_render 警告

掌握以上内容后,你就可以在 Svelte 5 中用 snippet 替代一切「复制粘贴的标记块」、以类型安全的方式设计可组合的组件 API,并在需要时通过 createRawSnippet 打通模板与命令式代码的边界。

登录后查看全文
热门项目推荐
相关项目推荐