NexaGrid技术博客

NexaGrid技术博客

Svelte 5 完全指南: 从入门到跨端应用开发 - 模板语法

2025-08-04
Svelte 5 完全指南: 从入门到跨端应用开发 - 模板语法

Svelte 5 完全指南: 从入门到跨端应用开发 - 模板语法

一、概述

svelte中引入了一些模板语法,你可以简单理解为定义了一些语法,这些语法相当于一些额外的HTML标记。

二、模板语法

1. Tags

小写标签(例如 <div>)表示常规 HTML 元素。大写标签或使用点符号的标签(例如 <Widget><my.stuff>)表示组件

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

<div>
	<Widget />
</div>

2. Element attributes

默认情况下,属性的工作方式与 HTML 属性完全相同。

<div class="foo">
	<button disabled>can't touch this</button>
</div>

同样类似HTML可以不封闭

<input type=checkbox />

同样可以加入 javascript表达式

<button disabled={!clickable}>...</button>
<a href="page/{p}">page {p}</a>

当key和变量名相同时,可以省略属性key

<button {disabled}>...</button>
<!-- equivalent to
<button disabled={disabled}>...</button>
-->

3. Component props

按照惯例,传递给组件的值被称为属性props而不是属性,后者是 DOM 的一个特性。

与元素一样,name={name}可以用 {name}简写代替。

<Widget foo={bar} answer={42} text="hello" />

4. Spread attributes

扩展属性允许将许多属性或特性一次传递给元素或组件。

元素或组件可以具有多个展开属性,并与常规属性交织在一起。顺序很重要,先定义会被后定义的覆盖——如果 things.a存在,它将优先于 a="b",而如果存在 c="d",它将优先于 things.c

<Widget a="b" {...things} c="d" />

5. Events

通过向元素添加以 开头的属性,可以监听 DOM 事件 on。例如,要监听 click事件,请 onclick向按钮添加属性:

<button onclick={() => console.log('clicked')}>click me</button>

事件属性区分大小写。onclick监听 click事件的 和 onClick监听 Click事件的 是不同的。这确保你可以监听包含大写字符的自定义事件。

因为事件只是属性,所以适用与属性相同的规则:

  • 您可以使用简写形式:<button {onclick}>click me</button>
  • 你可以传播它们:<button {...thisSpreadContainsEventAttributes}>click me</button>

从时间上来看,事件属性总是在绑定事件之后触发(例如,oninput总是在更新到 之后触发 bind:value)。在底层,一些事件处理程序直接附加到 addEventListener,而另一些则委托给

使用 ontouchstartontouchmove事件属性时,处理程序处于被动状态,以提高性能。这大大提高了响应速度,因为浏览器可以立即滚动文档,而无需等待事件处理程序是否调用 event.preventDefault()

在极少数情况下,您需要防止这些事件默认值,而应该使用on(例如在操作内部)。

Event delegation

为了减少内存占用并提高性能,Svelte 使用了一种称为事件委托的技术。这意味着,对于某些事件(请参阅下面的列表),应用程序根目录中的单个事件监听器将负责运行事件路径上的所有处理程序。

有几个问题需要注意:

  • 当您使用委托侦听器手动调度事件时,请确保设置该 { bubbles: true }选项,否则它将无法到达应用程序根目录
  • 直接使用时 addEventListener,请避免调用 stopPropagation,否则事件将无法到达应用程序根目录,处理程序也无法被调用。同样,在捕获阶段和冒泡阶段,在应用程序根目录内手动添加的处理程序将先于在 DOM 中更深层声明添加的处理程序(例如使用 onclick={...})运行。出于这些原因,最好使用 on从 导入的函数,svelte/events而不是 addEventListener,因为它可以确保顺序得到保留并 stopPropagation得到正确处理。

委托以下事件处理程序:

  • beforeinput
  • click
  • change
  • dblclick
  • contextmenu
  • focusin
  • focusout
  • input
  • keydown
  • keyup
  • mousedown
  • mousemove
  • mouseout
  • mouseover
  • mouseup
  • pointerdown
  • pointermove
  • pointerout
  • pointerover
  • pointerup
  • touchend
  • touchmove
  • touchstart

6. Text expressions

可以通过用花括号将 JavaScript 表达式括起来作为文本。

{text_expression}

该表达式将被字符串化并转义,以防止代码注入。如果您要渲染 HTML,请使用该 {@html}标签。

{@html potentiallyUnsafeHtmlString}

7. Comments

您可以在组件内部使用 HTML 注释。

<!-- this is a comment! --><h1>Hello world</h1>

svelte-ignore以禁用下一段标记的警告开头的注释。通常,这些是可访问性警告;请确保禁用它们是出于正当理由。

<!-- svelte-ignore a11y_autofocus -->
<input bind:value={name} autofocus />

您可以添加以 开头的特殊注释,使用 @component 注释当鼠标悬停在其他文件中的组件名称上时会显示该注释。

<!--
@component
- You can use markdown here.
- You can also use code blocks here.
- Usage:
  ```html
  <Main name="Arethra">
-->

三、模板标签

1. if 条件标签

在 Svelte 中,{#if} 指令用于实现条件渲染,即根据表达式的真假来决定是否显示特定内容。它的使用方式灵活,能满足各种条件判断场景。

基本用法

最基础的 {#if} 语法结构如下:

{#if expression}
  <!-- 当表达式为真时显示的内容 -->
{/if}

其中,expression 是一个返回布尔值的表达式。当表达式结果为 true 时,{#if}{/if} 之间的内容会被渲染;当结果为 false 时,这部分内容则不会显示。

例如:

{#if answer === 42}
  <p>what was the question?</p>
{/if}

answer 的值等于 42 时,会显示 <p> 标签中的文本“what was the question?”。

多条件判断

当需要判断多个条件时,可以使用 {:else if} 来添加额外的判断条件,语法如下:

{#if expression1}
  <!-- 当 expression1 为真时显示的内容 -->
{:else if expression2}
  <!-- 当 expression1 为假且 expression2 为真时显示的内容 -->
{/if}

包含默认情况

如果需要在所有条件都不满足时显示默认内容,可以使用 {:else} 来定义,语法结构为:

{#if expression1}
  <!-- 当 expression1 为真时显示的内容 -->
{:else if expression2}
  <!-- 当 expression1 为假且 expression2 为真时显示的内容 -->
{:else}
  <!-- 当所有条件都为假时显示的内容 -->
{/if}

例如判断粥的温度:

{#if porridge.temperature > 100}
  <p>too hot!</p>
{:else if 80 > porridge.temperature}
  <p>too cold!</p>
{:else}
  <p>just right!</p>
{/if}

当粥的温度高于 100 时,显示“too hot!”;当温度低于 80 时,显示“too cold!”;其他温度(即 80 到 100 之间)则显示“just right!”。

特殊使用场景

{#if} 块不仅可以包裹整个元素,还能包裹元素内的文本。比如:

<p>
  The weather is 
  {#if isSunny}
    sunny
  {:else}
    cloudy
  {/if}
  today.
</p>

根据 isSunny 的值,这句话会显示为“The weather is sunny today.” 或 “The weather is cloudy today.”。

2. each 循环标签

Svelte的 {#each}块用于迭代数组、类数组对象、Map、Set等可迭代对象,支持通过 as指定迭代项和索引;可添加唯一键(key) 优化列表diff性能;支持解构和rest模式处理迭代项;允许省略迭代项仅根据长度渲染多次;还可搭配 {:else}块在列表为空时显示内容。

思维导图

- {#each}块 - 基本用法 - 语法:`{#each expression as name}...{/each}` - 迭代对象:数组、类数组、Map、Set等(支持Array.from的对象) - 索引使用 - 语法:`{#each expression as name, index}...{/each}` - 作用:获取迭代项在列表中的位置(类似array.map的第二个参数) - Keyed each blocks - 语法:`{#each expression as name (key)}...{/each}` - 关键:key需唯一标识项,优化数据变化时的diff效率 - 解构与rest模式 - 支持对象解构:`{#each items as { id, name }}` - 支持数组解构:`{#each items as [id, ...rest]}` - 无item的each块 - 语法:`{#each expression}...{/each}` 或 `{#each expression, index}...{/each}` - 用途:仅根据长度渲染n次(如棋盘格) - Else块 - 语法:`{#each expression as name}...{:else}...{/each}` - 作用:列表为空时显示替代内容

1. 基本用法

  • 作用:迭代可迭代对象(数组、类数组对象、Map、Set等,支持 Array.from的对象)并渲染内容。
  • 基础语法
    {#each expression as name}...{/each}
    
  • 示例
    <h1>Shopping list</h1>
    <ul>
      {#each items as item}
        <li>{item.name} x {item.qty}</li>
      {/each}
    </ul>
    

2. 索引使用

  • 语法:通过逗号分隔指定索引(类似 array.map的第二个参数):
    {#each expression as name, index}...{/each}
    
  • 示例
    {#each items as item, i}
      <li>{i + 1}: {item.name} x {item.qty}</li>
    {/each}
    

3. Keyed each blocks

  • 作用:通过唯一键标识列表项,Svelte会基于键diff列表,而非仅在末尾增删,优化性能。
  • 语法
    {#each expression as name (key)}...{/each}
    {#each expression as name, index (key)}...{/each}
    
  • 要求:key需唯一(推荐字符串或数字,确保对象变化时身份可识别)。
  • 示例
    {#each items as item (item.id)}
      <li>{item.name} x {item.qty}</li>
    {/each}
    

4. 解构与 rest 模式

  • 支持对象解构:直接提取对象属性
    {#each items as { id, name, qty }, i (id)}
      <li>{i + 1}: {name} x {qty}</li>
    {/each}
    
  • 支持 rest 模式:保留剩余属性
    {#each objects as { id, ...rest }}
      <li><span>{id}</span><MyComponent {...rest} /></li>
    {/each}
    
  • 支持数组解构:处理数组形式的迭代项
    {#each items as [id, ...rest]}
      <li><span>{id}</span><MyComponent values={rest} /></li>
    {/each}
    

5. 无item的each块

  • 语法:省略 as部分,仅根据长度渲染多次
    {#each expression}...{/each}
    {#each expression, index}...{/each}
    
  • 示例(渲染8x8棋盘):
    <div class="chess-board">
      {#each { length: 8 }, rank}
        {#each { length: 8 }, file}
          <div class:black={(rank + file) % 2 === 1}></div>
        {/each}
      {/each}
    </div>
    

6. Else块

  • 作用:当迭代对象为空时,显示替代内容。
  • 语法
    {#each expression as name}...{:else}...{/each}
    
  • 示例
    {#each todos as todo}
      <p>{todo.text}</p>
    {:else}
      <p>No tasks today!</p>
    {/each}
    

不同语法结构对比表

用途 语法示例 关键特点
基础迭代 {#each items as item}...{/each} 遍历items,获取item
带索引迭代 {#each items as item, i}...{/each} 同时获取item和索引i
带key优化 {#each items as item (item.id)}...{/each} 通过item.id唯一标识项
空列表处理 {#each items as item}...{:else}...{/each} 空列表时显示else内容
无item渲染n次 {#each { length: 5 }}...{/each} 仅根据长度5渲染5次

关键问题

  1. 问题:为什么要使用keyed each blocks?key的选择有什么要求?
    答案:keyed each blocks通过唯一key标识列表项,使Svelte在数据变化时能精准diff列表(而非仅在末尾增删),提升性能。key需唯一标识每个项,推荐使用字符串或数字(确保对象内容变化时,key仍能保持身份一致性)。
  2. 问题{#each}块的 {:else}子句有什么作用?适用于哪些场景?
    答案{:else}子句在迭代对象为空(如空数组)时显示内容,适用于需要提示“无数据”的场景(如空待办列表显示“今日无任务”、空购物车显示“暂无商品”等)。
  3. 问题:无item的 {#each}块(如 {#each { length: 8 }})有什么实际应用?
    答案:适用于需要根据固定长度重复渲染内容的场景,无需具体迭代项数据。例如:渲染固定行列的网格(如棋盘)、生成固定数量的占位元素、循环创建重复UI组件(如8个评分星星)等。

3. key 标签

Svelte的 {#key expression}...{/key}块的核心功能是:当表达式的值发生变化时,会销毁并重新创建其包含的内容。这一特性可用于强制组件重新实例化和初始化,也能实现在值变化时触发过渡动画,是控制内容更新和状态重置的重要工具。

思维导图

- {#key}块 - 基础语法 - 结构:`{#key expression}...{/key}` - 核心:表达式值变化触发内容重建 - 核心作用 - 销毁并重建内部内容 - 重置组件状态 - 应用场景 - 组件重新实例化 - 触发过渡动画 - 示例 - 组件场景:`{#key value}<Component />{/key}` - 过渡场景:`{#key value}<div transition:fade>{value}</div>{/key}`

1. 基本语法与核心原理

  • 语法结构
    {#key expression}
      <!-- 内容(元素或组件) -->
    {/key}
    
  • 核心机制:当 expression的值发生变化时,{#key}块会销毁内部所有内容,并重新创建这些内容,实现状态的完全重置。

2. 主要应用场景

应用场景 说明 示例代码
组件重新实例化 强制组件重新初始化,重置内部状态 svelte {#key value} <Component /> {/key}
触发过渡动画 当值变化时,让过渡效果重新执行 svelte {#key value} <div transition:fade>{value}</div> {/key}
  • 组件重新实例化:适用于需要彻底重置组件状态的场景。例如,当用户切换标签页时,通过 {#key}使标签内容组件重新初始化,避免旧状态残留。
  • 过渡动画触发:结合Svelte的过渡指令(如 transition:fade),可在值变化时强制过渡效果重新播放。例如,数据更新时让元素重新执行淡入动画,增强视觉反馈。

关键问题

  1. 问题{#key}块的核心作用是什么?与普通条件渲染有何区别?
    答案:核心作用是当表达式值变化时,销毁并重建内部内容,实现状态完全重置。与 {#if}等条件渲染的区别在于:{#if}仅根据条件显示/隐藏内容,不会销毁已存在的DOM;而 {#key}会彻底重建内容,重置所有内部状态。
  2. 问题:在什么情况下需要使用 {#key}强制组件重新实例化?
    答案:当组件内部状态依赖外部数据,且需要在外部数据变化时彻底重置组件(而非仅更新属性)时使用。例如:表单组件在切换编辑对象时,需清空输入框状态;定时器组件在时间参数变化时,需重新开始计时。
  3. 问题{#key}如何配合过渡动画使用?优势是什么?
    答案:将过渡指令(如 transition:fade)与 {#key}结合,当表达式值变化时,{#key}会销毁旧元素并创建新元素,触发过渡效果重新执行。优势是无需手动管理动画状态,即可在数据变化时自动播放过渡,提升交互体验的流畅性。

4. await标签

Svelte的 {#await}块用于处理Promise的三种状态:pending(等待中)、fulfilled(已完成)、rejected(已拒绝),通过不同分支实现条件渲染。其语法支持多种形式,可省略不需要的状态分支,还能与 import(...)结合实现组件懒加载;在服务器端渲染(SSR)时,仅会渲染pending分支,非Promise值则只渲染fulfilled分支。

- {#await}块 - 核心作用 - 处理Promise的三种状态:pending、fulfilled、rejected - 实现对应状态的条件渲染 - 语法结构 - 完整结构:`{#await expression}...{:then name}...{:catch name}...{/await}` - 省略catch:`{#await expression}...{:then name}...{/await}` - 省略pending:`{#await expression then name}...{/await}` - 仅catch:`{#await expression catch name}...{/await}` - 状态分支 - pending分支:Promise等待时渲染 - :then分支:Promise完成或非Promise值时渲染 - :catch分支:Promise拒绝时渲染 - 特殊用法 - 与`import(...)`结合实现组件懒加载 - SSR行为 - 服务器端仅渲染pending分支 - 非Promise值仅渲染:then分支

1. 基本作用

{#await}块是Svelte中用于处理Promise状态的模板指令,能够根据Promise的三种状态(pending、fulfilled、rejected)自动切换渲染内容,简化异步操作的UI展示逻辑。

2. 语法结构

{#await}支持4种语法结构,可根据需求省略不需要的状态分支:

语法结构 适用场景
{#await expression}...{:then name}...{:catch name}...{/await} 需要处理所有三种状态(pending、fulfilled、rejected)
{#await expression}...{:then name}...{/await} 无需处理rejected状态(无错误或不展示错误)
{#await expression then name}...{/await} 无需处理pending状态(不展示等待内容)
{#await expression catch name}...{/await} 仅需处理rejected状态(只关注错误)

3. 状态分支说明

  • pending分支:位于 {#await expression}后,当Promise处于等待状态时渲染,用于展示加载中提示(如“加载中...”)。
  • :then分支:通过 {:then name}定义,当Promise成功完成(fulfilled)或表达式为非Promise值时渲染,name为Promise的返回值。
  • :catch分支:通过 {:catch name}定义,当Promise拒绝(rejected)时渲染,name为错误信息。

4. 特殊用法

{#await}可与动态 import(...)结合,实现组件懒加载,示例:

{#await import('./Component.svelte') then { default: Component }}
  <Component />
{/await}

5. 服务器端渲染(SSR)行为

  • 当进行服务器端渲染时,仅pending分支会被渲染,无论Promise实际状态如何。
  • 若表达式为非Promise值,服务器端和客户端均只渲染:then分支

关键问题

  1. 问题{#await}块与传统JavaScript中使用 .then()/.catch()处理Promise相比,优势是什么?答案{#await}将异步状态与UI渲染直接绑定,无需手动管理状态变量(如 isLoadingdataerror),简化了模板逻辑;且能自动根据Promise状态切换渲染内容,使代码更简洁直观。
  2. 问题:在什么情况下应该省略 {#await}的某个状态分支?答案:- 若异步操作几乎不会失败(或无需展示错误),可省略 :catch分支;- 若无需展示加载中状态(如数据加载极快),可省略pending分支,直接使用 {#await ... then ...};- 若只关注错误状态(如监控异步操作失败),可省略 :then分支,仅用 {#await ... catch ...}
  3. 问题:为什么服务器端渲染(SSR)时 {#await}仅渲染pending分支?
    答案:因为服务器端渲染时,异步操作可能尚未完成(Promise状态不确定),为避免客户端与服务器端渲染内容不一致(hydration不匹配),Svelte默认仅渲染pending分支,确保客户端接管后能正确处理后续状态变化。

5. snippet

Svelte的 {#snippet}可重用的标记块,用于创建组件内可复用的标记片段,支持参数(含默认值和解构,不支持rest参数),通过 {@render}调用。其作用域为词法作用域,可引用外部变量且内部声明的snippet仅在同级及子级可见,还能递归调用。{#snippet}可作为props传递给组件(支持隐式声明为组件props),支持类型定义(基于 Snippet接口),Svelte 5.5.0及以上版本支持导出顶层snippet,且可通过 createRawSnippet编程式创建。相比Svelte 4的slots,{#snippet}更强大灵活,slots在Svelte 5中已弃用

- {#snippet} - 基本定义 - 可重用的标记块 - 用于组件内复用标记片段 - 语法结构 - 基础:`{#snippet name()}...{/snippet}` - 带参数:`{#snippet name(param1, param2)}...{/snippet}`(支持默认值、解构,无rest参数) - 调用:`{@render name(arguments)}` - 作用域规则 - 词法作用域:可引用外部变量 - 可见性:内部snippet仅同级及子级可见 - 支持递归调用 - 核心功能 - 组件内复用标记 - 作为props传递给组件(含隐式声明) - 支持类型定义(`Snippet`接口) - 导出:顶层snippet可从`<script module>`导出(需Svelte 5.5.0+) - 编程式创建:`createRawSnippet` API(高级场景) - 与slots对比 - 比slots更强大灵活 - Svelte 5中slots已弃用

1. 定义与核心作用

{#snippet}是Svelte中用于创建可重用标记片段的指令,旨在减少组件内的重复代码,通过将重复的标记逻辑封装为snippet,实现模板层面的复用。

2. 语法结构

语法形式 说明 示例
无参数snippet 定义无参数的可重用片段 {#snippet hello()}<p>hello</p>{/snippet}
带参数snippet 支持多个参数,可含默认值和解构,不支持rest参数 {#snippet greet(name = 'guest')}<p>hi {name}</p>{/snippet}
调用snippet 通过 {@render}调用,传入对应参数 {@render greet('Alice')}

3. 作用域规则

  • 外部变量引用:snippet可引用声明位置外部的变量(如 <script>中的变量或 {#each}块中的值)。示例:
    <script>let message = "welcome";</script>
    {#snippet hello(name)}<p>hello {name}! {message}</p>{/snippet}
    {@render hello('Bob')} <!-- 输出:hello Bob! welcome -->
    
  • 可见性限制:snippet仅在其词法作用域内可见(同级元素及子元素可访问,外部不可)。示例:
    <div>
      {#snippet x()}
        {#snippet y()}<p>y</p>{/snippet}
        {@render y()} <!-- 有效:y在x的作用域内 -->
      {/snippet}
      {@render y()} <!-- 错误:y不在外部作用域 -->
    </div>
    
  • 递归支持:snippet可引用自身或其他snippet实现递归。
    示例:
    {#snippet countdown(n)}
      {#if n > 0}<span>{n}...</span>{@render countdown(n-1)}{/if}
    {/snippet}
    {@render countdown(3)} <!-- 输出:3...2...1... -->
    

4. 传递snippets给组件

  • 作为props传递:snippet可像普通值一样作为props传递给子组件,实现“传递内容而非数据”,类似web components的slots。示例:
    <!-- 父组件 -->
    <script>import Table from './Table.svelte';</script>
    {#snippet row(data)}<td>{data.name}</td>{/snippet}
    <Table {row} />
    
    <!-- Table.svelte -->
    <script>let { row } = $props();</script>
    <tr>{@render row(item)}</tr>
    
  • 隐式声明为props:在组件标签内直接声明的snippet,会隐式成为组件的props,与显式传递效果一致。
  • children隐式snippet:组件标签内的非snippet内容,会自动成为 children snippet,可在子组件中通过 {@render children()}渲染。
    注意:若组件有内部内容,不可声明名为 children的prop,避免冲突。

5. 类型定义

snippets的类型基于 svelte导出的 Snippet接口,需在TypeScript中显式声明:

<script lang="ts">
  import type { Snippet } from 'svelte';
  interface Props {
    itemSnippet: Snippet<[string]>; // 接收一个string参数的snippet
  }
  let { itemSnippet }: Props = $props();
</script>

支持泛型,确保参数类型与数据类型一致:

<script lang="ts" generics="T">
  let { row: Snippet<[T]> } = $props(); // row的参数类型与泛型T一致
</script>

6. 导出snippets

Svelte 5.5.0及以上版本支持从 <script module>中导出顶层snippet(不引用非module脚本中的声明):

<script module>export { add };</script>
{#snippet add(a, b)}<p>{a} + {b} = {a+b}</p>{/snippet}

7. 编程式创建

通过 createRawSnippet API可编程式创建snippets,适用于高级场景(如动态生成标记)。

8. 与slots的对比

特性 {#snippet} Svelte 4 slots
灵活性 支持参数、递归、类型定义 不支持参数,灵活性有限
复用性 可在组件内多次渲染,传递给多个组件 主要用于组件间内容分发,复用性弱
状态 可引用外部状态,上下文感知 上下文依赖组件层级,灵活性低
现状 Svelte 5推荐使用 Svelte 5中已弃用

关键问题

  1. 问题{#snippet}相比直接复制粘贴重复标记代码,核心优势是什么?
    答案{#snippet}通过封装重复标记逻辑,减少代码冗余,提升可维护性;支持参数化(含默认值和解构),可根据不同输入动态生成标记;遵循词法作用域,能自然引用外部状态,且支持递归调用,比硬编码重复代码更灵活。
  2. 问题:在组件中使用 snippet时,如何处理可选的snippet props(即可能未被传递的snippet)?
    答案:有两种方式:① 使用可选链调用:{@render children?.()},若snippet不存在则不渲染;② 使用 {#if}块判断,不存在时显示 fallback 内容:{#if children}{@render children()}{:else}默认内容{/if}
  3. 问题:为什么Svelte 5弃用slots而推荐 snippet
    答案{#snippet}比slots更强大:支持参数传递,可根据数据动态生成内容;能作为值在组件间传递,复用性更高;遵循词法作用域,上下文处理更直观;还支持类型定义和递归,覆盖slots的所有使用场景且扩展性更强,因此Svelte 5选择用 snippet替代slots。