Svelte 模板基础语法详解:从 HTML++ 标记、属性规则到事件委托实现
本文基于 Svelte 官方文档 Basic markup 展开,系统讲解 Svelte 组件中"HTML++"模板体系的六大核心能力:元素与组件标签的区分规则、静态/动态属性及布尔属性的取值语义、组件 props 与展开属性、on* 事件属性及其底层的事件委托机制、花括号文本表达式与 HTML 转义,以及注释的多种形态。读完后,你将不仅会写 Svelte 模板,还能结合仓库源码(如 utils.js、events.js)理解这些语法在编译期与运行期的真实行为。
Svelte 模板:HTML++ 心智模型
Svelte 组件中的标记(markup)可以被视为 HTML++:它在标准 HTML 之上叠加了 JavaScript 表达式、组件引用、展开属性和响应式能力,但不改变 HTML 的基本形态。掌握这套模板语法的关键在于分清三类标记——普通元素、组件、表达式——以及它们各自遵循的规则。
标签:元素还是组件
标签的大小写和命名方式决定了它的语义:
- 小写标签(如
<div>)表示普通的 HTML 元素; - 首字母大写的标签(如
<Widget>)或使用点号分隔的标签(如<my.stuff>)表示一个 组件(component)。
<script>
import Widget from './Widget.svelte';
</script>
<div>
<Widget />
</div>
组件必须先在 <script> 中通过 import 引入才能在模板中使用。编译器会根据标签名判断目标类型,进而走不同的转换路径——元素走 DOM 属性/事件逻辑,组件走 props 传参与实例化逻辑。
元素属性(Element Attributes)
静态属性与无引号值
默认情况下,元素属性与标准 HTML 行为完全一致:
<div class="foo">
<button disabled>can't touch this</button>
</div>
和 HTML 一样,属性值可以不加引号:
<input type=checkbox />
表达式:在值中,或就是值
属性值中可以用花括号内嵌任意 JavaScript 表达式:
<a href="page/{p}">page {p}</a>
整个属性值也可以直接 是 一个表达式:
<button disabled={!clickable}>...</button>
这两种写法在 客户端属性编译逻辑 中都会被统一处理:静态字符串走字符串字面量路径,含表达式的属性则被编译为可在响应式更新时重新求值的动态调用。
布尔属性与 nullish 规则
这是 Svelte 属性语义中最容易被误解的一点,文档明确区分了两类规则:
- 布尔属性:值为 truthy 时属性出现在元素上,值为 falsy 时属性被移除;
- 其他所有属性:只要值不是 nullish(即
null或undefined)就会出现。
<input required={false} placeholder="This input field is not required" />
<div title={null}>This div has no title attribute</div>
[!NOTE] 给单个表达式加引号不影响值的解析方式,但在 Svelte 6 中会导致值被强制转换为字符串:
<button disabled="{number !== 42}">...</button>
哪些属性算"布尔属性"由源码中的白名单决定。在 utils.js 中,DOM_BOOLEAN_ATTRIBUTES 列出了 allowfullscreen、autofocus、checked、disabled、muted、required、readonly 等 28 个属性,is_boolean_attribute() 据此判断——所以 <button disabled={!clickable}> 中的 !clickable 走的是"truthy 则保留、falsy 则移除"的分支,而不是把 "false" 写进 DOM。
属性简写(Shorthand)
当属性名与值相同(name={name})时,可简写为 {name}:
<button {disabled}>...</button>
<!-- 等价于
<button disabled={disabled}>...</button>
-->
组件 Props
按惯例,传给组件的值称为 properties / props,而 attributes 是 DOM 的概念。规则与元素属性一致,同样支持 {name} 简写:
<Widget foo={bar} answer={42} text="hello" />
组件内部如何接收这些值(传统 export let 或 runic 的 $props())属于另一主题,可参考 props 文档。
展开属性(Spread Attributes)
展开属性允许一次性把一组属性或 props 传给元素/组件,并且可以与普通属性交错出现。顺序很重要——后出现的属性覆盖先出现的同名项:
<Widget a="b" {...things} c="d" />
- 若
things.a存在,它优先于a="b"(因为展开在a="b"之后); - 而
c="d"优先于things.c(因为c="d"在展开之后)。
即:同一位置上的后写者胜,这一语义对元素和组件同样成立。
事件(Events)
on* 事件属性
通过在元素上添加以 on 开头的属性来监听 DOM 事件。例如监听 click 事件,就给按钮加 onclick 属性:
<button onclick={() => console.log('clicked')}>click me</button>
事件属性是大小写敏感的
onclick 监听的是 click 事件,而 onClick 监听的是 Click 事件——两者是不同的事件。这一设计保证了你可以监听名称中含大写字母的自定义事件(例如组件通过 createEventDispatcher 派发的自定义事件)。
事件属性也是属性
因为事件本质上就是属性,所以同样适用属性规则:
- 简写:
<button {onclick}>click me</button> - 展开:
<button {...thisSpreadContainsEventAttributes}>click me</button>
在时序上,事件属性总是在绑定(bindings)触发的事件之后执行——例如 oninput 总是在 bind:value 更新之后触发。底层实现上,一部分事件处理器通过 addEventListener 直接挂到元素上,另一部分则通过事件委托处理(见下文)。
触摸事件的 passive 优化
使用 ontouchstart 与 ontouchmove 事件属性时,处理器会以 passive 模式注册,从而允许浏览器立即滚动文档,而不是等待看事件处理函数是否调用 event.preventDefault(),大幅提升移动端响应性。
从源码看,这一行为由 utils.js 中的 PASSIVE_EVENTS = ['touchstart', 'touchmove'] 决定,源码注释解释了原因:这两个事件触发频繁、多发生在性能更弱的移动设备上,且默认经委托路径处理,因此显式标记为 passive。
极少数确实需要 preventDefault 阻止默认行为的场景,应改用从 svelte/events 导入的 on 函数(例如在 action 内部)。
事件委托(Event Delegation)
为降低内存占用、提升性能,Svelte 对特定事件使用事件委托:在应用根部只挂一个监听器,由它负责执行事件传播路径上各元素的处理器。以下 23 个事件会走委托:
beforeinput、click、change、dblclick、contextmenu、focusin、focusout、input、keydown、keyup、mousedown、mousemove、mouseout、mouseover、mouseup、pointerdown、pointermove、pointerout、pointerover、pointerup、touchend、touchmove、touchstart
这份清单与 utils.js 中的 DELEGATED_EVENTS 常量逐条一致,并由 can_delegate_event() 供编译器在分析阶段(见 Attribute.js 中的调用)判断某个 on* 属性能否走委托路径。
委托模式的两个陷阱
- 手动派发事件:如果你
dispatchEvent一个使用委托监听器的事件,务必设置{ bubbles: true },否则事件到不了应用根部,处理器不会执行; - 直接使用
addEventListener:避免在其中调用stopPropagation,否则事件到不了根部、委托处理器不会触发。同时,根部手动添加的处理器会在捕获与冒泡两个阶段都先于深层声明式处理器(如onclick={...})执行。
因此官方建议:需要手动挂事件时,用从 svelte/events 导入的 on 函数而非 addEventListener,它会保证与声明式处理器的执行顺序正确、并妥善处理 stopPropagation。
源码走读:委托处理器如何工作
on 函数定义于 internal/client/dom/elements/events.js,其 JSDoc 明确说明了它存在的意义:
"Attaches an event handler to an element and returns a function that removes the handler. Using this rather than
addEventListenerwill preserve the correct order relative to handlers added declaratively (with attributes likeonclick), which use event delegation for performance reasons"
委托的运行时机制分三步(见 events.js):
- 注册期:
delegated(event_name, element, handler)不挂任何监听器,而是把处理器按事件名存到元素上的一个event_symbol符号属性中;delegate(events)则在根部登记"应用关注哪些事件"。 - 触发期:根部监听器调用 handle_event_propagation,它通过
event.composedPath()拿到完整传播路径,从目标元素起沿路径向上遍历,逐个查找element[event_symbol][event_name]并调用,遇到event.cancelBubble即停止。 - 一致性保证:遍历过程中会临时把
event.currentTarget代理为当前路径上的真实目标元素,保证委托处理器内event.currentTarget语义与原生监听器一致;多个嵌套挂载的应用之间通过event[event_symbol]记录"已在何处处理过"来避免重复触发。
也就是说,onclick 在编译产物中并不会真的调用 addEventListener,而是一次"在元素上登记 + 等根部统一分发"的操作——这正是文档提示"手动 dispatchEvent 必须 bubbles"的根本原因。
文本表达式(Text Expressions)
花括号内可写任意 JavaScript 表达式:
{expression}
求值结果为 null 或 undefined 时会被省略,其余值一律转换为字符串。
在模板中输出字面量花括号
若确实需要在模板里显示 { 或 },使用 HTML 实体:{、{、{ 表示 {,}、}、} 表示 }。
正则表达式字面量
在表达式中使用正则字面量(RegExp literal notation)时,必须用圆括号包裹,避免解析歧义:
<h1>Hello {name}!</h1>
<p>{a} + {b} = {a + b}.</p>
<div>{(/^[A-Za-z ]+$/).test(value) ? x : y}</div>
自动转义与 XSS 防护
表达式会被字符串化并转义,以防止代码注入(XSS)。转义逻辑实现在 escaping.js 的 escape_html(value, is_attr) 中:普通文本转义 & 与 <,属性模式额外转义 "。
如果确实需要渲染 HTML,使用 {@html} 标签:
{@html potentiallyUnsafeHtmlString}
[!NOTE] 务必对传入字符串做转义,或只填充你自己完全可控的值,否则会造成 XSS 攻击 风险。
{@html}的完整用法见 08-@html.md。
注释(Comments)
HTML 注释
组件内可直接使用标准 HTML 注释:
<!-- this is a comment! --><h1>Hello world</h1>
svelte-ignore
以 svelte-ignore 开头的注释会关闭其后一段标记上的警告(通常是可访问性警告)。关闭警告务必有充分理由,可用的 a11y 警告清单见 a11y.md:
<!-- svelte-ignore a11y_autofocus -->
<input bind:value={name} autofocus />
@component 文档注释
以 @component 开头的特殊注释,会在其他文件中悬停(hover)该组件名时作为文档提示显示,支持 Markdown 和代码块:
<!--
@component
- You can use markdown here.
- You can also use code blocks here.
- Usage:
```html
<Main name="Aretha">
-->
Hello, {name}
标签内的 JS 风格注释
在标签的属性之间,还可以写 JavaScript 风格的行注释:
<div
// this is a comment!
data-foo="bar"
>
foo bar
</div>
小结
Svelte 的基础标记可以概括为一张速查表:
| 语法 | 规则要点 | 源码依据 |
|---|---|---|
| 小写标签 / 大写或点号标签 | 元素 / 组件 | 编译器按标签名分流 |
| 布尔属性 | truthy 保留、falsy 移除 | DOM_BOOLEAN_ATTRIBUTES |
| 非布尔属性 | 仅 null/undefined 时移除 |
同上转换逻辑 |
{name} 简写 |
等价于 name={name},元素与组件通用 |
— |
{...spread} |
后写者优先 | — |
on* 属性 |
大小写敏感、可简写/展开、先于绑定前触发 | can_delegate_event |
| 触摸事件 | 默认 passive | PASSIVE_EVENTS |
| 委托事件(23 个) | 根部单监听器分发 | DELEGATED_EVENTS |
{expression} |
null/undefined 省略,其余转字符串并转义 | escape_html |
{@html} |
原样渲染 HTML,需自行防 XSS | @html 文档 |
<!-- svelte-ignore ... --> / <!-- @component ... --> |
关闭警告 / IDE 悬停文档 | a11y 警告清单 |
以上行为以当前仓库(Svelte 6 开发主线)的源码为准;其中"引用单个表达式会在 Svelte 6 中被强制转字符串"一条来自文档自身的版本注记,属于版本敏感行为,跨版本使用时请以对应版本发布说明核对。
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