Svelte JS 学习笔记(二):Advanced Svelte
AI的建议,仅供参考
对于 Go + Wails 开发者,建议重点关注以下内容(按顺序):
Stores (状态库):必须学,解决复杂数据流。
bind:this (实例绑定):必须学,为了调用 Canvas 或其他 JS 库。
svelte:window:必须学,处理桌面软件的快捷键和窗口行为。
Context API:依赖注入,架构设计用(类似 Go 的
context传参)。Snippets:减少重复代码。
至于 Motion 和 Animations,那是装修工人的活儿,等房子盖好了再研究也不迟。
Svelte 进阶
进阶响应式 (Advanced Reactivity)
原生状态 (Raw State)
在之前的练习中,我们已经知道状态是深度响应式的——比如说,如果我们修改了一个对象里的某个字段,或者给数组插入了一个值,都会触发UI重绘。Svelte通过使用Proxy拦截对变量的写操作和读操作来实现状态
有时你确实不希望页面太敏感。当你想要批量修改页面资产,或者你想要维护引用相等性时,你可以转而使用原生状态 (Raw State)
<script>
import { scale } from './utils.js';
import { poll } from './data.js';
let data = poll();
let w = $state(1);
let h = $state(1);
const min = $derived(Math.min(...data) - 5);
const max = $derived(Math.max(...data) + 5);
const x = $derived(scale([0, data.length], [0, w]));
const y = $derived(scale([min, max], [h, 0]));
const ticks = $derived.by(() => {
const result = [];
let n = 10 * Math.ceil(min / 10);
while (n < max) {
result.push(n);
n += 10;
}
return result;
});
$effect(() => {
const interval = setInterval(() => {
data = poll();
}, 200);
return () => {
clearInterval(interval);
};
});
</script>
<div class="outer">
<svg width={w} height={h} bind:clientWidth={w} bind:clientHeight={h}>
<line y1={h} y2={h} x2={w} />
{#each ticks as tick}
<g class="tick" transform="translate(0,{y(tick)})">
<line x2={w} />
<text x={-5}>{tick}</text>
</g>
{/each}
<polyline points={data.map((d, i) => [x(i), y(d)]).join(' ')} />
<text x={10} y={10} font-size={36}>$SVLT</text>
</svg>
</div>
<style>
.outer {
width: 100%;
height: 100%;
padding: 2em;
box-sizing: border-box;
}
svg {
width: 100%;
height: 100%;
overflow: visible;
}
polyline {
fill: none;
stroke: #ff3e00;
stroke-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
}
line {
stroke: #aaa;
}
.tick {
stroke-dasharray: 2 2;
text {
text-anchor: end;
dominant-baseline: middle;
}
}
</style>在这个例子中,我们有一个用来绘制股票走势的Svelte图表
我们希望data(或者说一个每0.2秒更新的轮询函数)能够随时间变化,我们可以直接给poll()套个$state,但我们有时候又不需要太深的响应,这时可以套个$state.raw——虽然我不知道为什么这用不用raw,看起来都没有什么变化
提示
修改原生状态不会直接产生effect,但不管怎么说,都不建议修改非响应式的状态
Mutating raw state will have no direct effect. In general, mutating non-reactive state is strongly discouraged.
响应式类 (Reactive Classes)
我们不仅能让变量动起来 ,也能让类的属性动起来
让我们把Box类的width和height属性也变成一个状态:
还可以进一步使用$derived,从width和height状态导出area状态:
提示
除了$state+$derived的组合,还可以使用$state.raw+$derived.by来获取响应式字段
In addition to
$stateand$derived, you can use$state.rawand$derived.byto define reactive fields.
Getter与Setter
classes通常需要校验数据——拿上题的Box类举例,Box的长宽值不应该在点击embiggen按钮后突破滑条的值……但这确实发生了:
我们可以通过给width和height属性加上setters和getters方法来解决这个问题。首先要把width和height变成私有属性:
然后书写getters和setters:
然后用Math库的max、min函数修正长宽的值:
set width(value) {
this.#width = Math.max(0, Math.min(value, MAX_SIZE));
}
set height(value) {
this.#height = Math.max(0, Math.min(value, MAX_SIZE));
}
MAX_SIZE在这里定义的
然后长宽就不会突破上限了
响应式内置类 (Reactive Built-ins)
Svelte预设了一些响应式类,可以用来替换一些JS内置对象——比如键值对Map、集合Set、时间Date、URL、URL请求参数URLSearchParams
<script>
let date = new Date();
const pad = (n) => n < 10 ? '0' + n : n;
$effect(() => {
const interval = setInterval(() => {
date.setTime(Date.now());
}, 1000);
return () => {
clearInterval(interval);
};
});
</script>
<p>The time is {date.getHours()}:{pad(date.getMinutes())}:{pad(date.getSeconds())}</p>在这一关中,我们需要声明一个响应式的Date对象——可以直接给new Date套上$state,也可以直接使用svelte/reactivity库的SvelteDate对象
Svelte 5之前的状态 (Stores)
在Svelte 5引入rune之前,stores是处理组件外部的响应式状态时惯用的手段之一。尽管物是人非,我们仍会在某些情况下遇到stores(包括使用SvelteKit的情形),所以还是有必要知道怎么用的
提示
我们不会介绍如何自定义stores,有需要的话请自行查阅文档
现在请回顾全局响应(Universal Reactivity)这一节的例子,但这一次,使用stores实现状态共享
在shared.js中,我们export了一个整型变量count,现在我们要把它变成一个可写的store:
import { writable } from "svelte/store"
export const count = writable(0);
在Counter.svelte中,我们要在count变量前面加个$符号,这样就不会渲染个object Object出来了
最后添加事件监听器。因为这是一个可写的store ,所以我们可以调用count状态的set或update方法来更新它的值:
但鉴于我们还在Counter组件内,所以可以继续使用$前缀:
<script>
import { count } from './shared.js';
</script>
<button onclick={() => $count += 1}>
clicks: {$count}
</button>【Gemini】Store VS $state
1. Store (Svelte 3/4 的遗产):基于“订阅”的观察者模式
Store 本质上就是一个实现了 subscribe 方法的 JavaScript 对象。
- Go 类比: Store 就像是一个 Go Channel 或者一个带有 回调函数列表 的 Struct。
- 工作原理 (Pub/Sub):
- 你创建一个 Store:
const count = writable(0)。 - 组件 订阅 它:
count.subscribe(value => ...)。 - 当你修改它
count.set(1)时,它遍历所有订阅者并调用回调。
- 你创建一个 Store:
- 那个
$是什么?- 在 Svelte 3/4 里,
$count确实是一个编译时语法糖。 - 编译器看到
$count,就自动把它翻译成:“帮我写一行代码count.subscribe(...),并且记得在组件销毁时unsubscribe”。 - 本质: 它是运行时 (Runtime) 的对象操作。
- 在 Svelte 3/4 里,
2. Runes (Svelte 5 的核心):基于“信号”的细粒度响应式
Runes ($state) 不是对象,它们是编译指令(就像 Go 的 Build Tags 或者 //go:embed)。
- Go 类比: Runes 更像是**“魔改版”的变量**,或者是带有钩子(Hooks)的内存地址。
- 工作原理 (Signals):
- 声明:
let count = $state(0); - Svelte 编译器介入:它不会生成一个
writable对象。它会把这个变量变成一个 Signal(信号)。 - 依赖收集 (Dependency Tracking):当你读取
count时,Svelte 并不需要显式的subscribe。当前的执行上下文(比如正在渲染的 HTML 片段)会自动记录:“哦,我依赖这个变量”。 - 细粒度更新:当
count变了,Svelte 只更新 依赖它的那个具体的 Text Node,而不是重新运行整个组件。
- 声明:
- 那个
$是什么?- 这里的
$只是告诉编译器:“嘿,这个变量不普通,给它加特技”。 - 关键区别: 使用时不需要加
$。- Store:
console.log($count)(要加 $ 才能读值) - Runes:
console.log(count)(直接读,像普通变量一样)
- Store:
- 这里的
3. 为什么官方强调 "Rune 不是函数"?
这就像 Go 里的 make 或 new 或 len。虽然看起来像函数调用 len(slice),但 len 是编译器内置原本语(Primitive),编译器会直接把它替换成汇编指令,而不是真正的函数跳转。
let x = $state(0):
在编译后的 JS 代码里,这一行可能会变成类似let x = $.source(0)的底层调用。$state这个符号在运行时根本不存在。
- 核心差异对比表 (Go 视角)
| 特性 | Store (旧) | Runes (新 - $state) |
|---|---|---|
| 底层模型 | Pub/Sub (发布订阅) 类似 Go 的 Observer 模式 | Signals (信号/依赖追踪) 类似电子表格 Excel 的公式自动计算 |
| 数据类型 | 是一个 对象 ({ subscribe: ... }) | 看起来是 普通变量 (int, string, struct) |
| 读取方式 | 必须加 $ ($store) 或调用 .subscribe() | 直接读取 (count) |
| 写入方式 | store.set(1) 或 store.update(n => n+1) | 直接赋值 count = 1 |
| 更新粒度 | 组件级 (通常会触发组件 Update 周期) | DOM 节点级 (直接修改 innerHTML,不跑组件逻辑) |
| 复杂度 | 重 (创建对象、闭包、回调) | 轻 (基本类型包装) |
5. 结论
Runes 不是 Store 的封装,而是 Store 的“掘墓人”。
Svelte 5 引入 Runes 的目的,就是为了在 90% 的场景下取代 Store。
- 以前为了跨组件共享状态,或者为了响应式,你被迫要把简单的
let count = 0变成复杂的writable(0)。 - 现在,你只需要把
let改成$state,其他代码逻辑完全不用变(赋值还是赋值,读取还是读取)。
对于你的学习路径:
你在 Advanced Svelte 里看到的 store,在 Svelte 5 的世界观里,主要退化为两个用途:
- 兼容旧代码。
- 处理极端复杂的异步流(RxJS 风格的操作),这时候 Store 的 subscribe 模式可能还有点用。
一句话总结:
Store 是我要监听你 (手动挡);
Runes 是我用到了你,所以你变的时候通知我(自动挡,且是引擎级别的集成)。
内容复用/UI复用 (Reusing Content)
代码段与渲染标签 (Snippets and Render Tags)
Snippets允许我们在.svelte组件内复用内容,而不需要物理抽离到另一个文件中
在这一题中,我们需要创建包含三只马楼以及它们的Unicode转义序列与HTML实体的表格。现在,我们只有一只马楼
我们可以手动复制粘贴<tbody>元素,可以把emoji和description放到一个map里,让{#each ...}遍历块遍历它们。但更优雅的解决方案是封装HTML元素到一个可复用的块中
首先需要声明这样一个代码段 :
{#snippet monkey()}
<tr>
<td>{emoji}</td>
<td>{description}</td>
<td>\u{emoji.charCodeAt(0).toString(16)}\u{emoji.charCodeAt(1).toString(16)}</td>
<td>&#{emoji.codePointAt(0)}</td>
</tr>
{/snippet}我们不主动渲染 马楼,那马楼就是看不见的。现在让我们渲染一只malou
把之前的一大段换成这样一小段
代码段还可以有零个或多个参数:
提示
你想的话还可以析构参数
现在把剩下的参数加进去:
{@render monkey('🙈', 'see no evil')}
{@render monkey('🙉', 'hear no evil')}
{@render monkey('🙊', 'speak no evil')}
然后就可以去把<script>删掉了……吗?
提示
代码段可以在组件的任何地方声明,但就和函数一样,只对当前作用域及子作用域的@render标签有效
将代码段传递给组件
既然代码段和函数一样,都是个value,那么代码段也可以作为props被传递给组件
以<FilteredList>元素为例,它的任务就是过滤传递给它的data
但没有预留参数来指定如何渲染数据——这是父组件应该做的事
<script>
import FilteredList from './FilteredList.svelte';
import { colors } from './data.js';
</script>
<FilteredList
data={colors}
field="name"
></FilteredList>在App组件里,我们已经定义了一些代码段:
{#snippet header()}
<header>
<span class="color"></span>
<span class="name">name</span>
<span class="hex">hex</span>
<span class="rgb">rgb</span>
<span class="hsl">hsl</span>
</header>
{/snippet}
{#snippet row(d)}
<div class="row">
<span class="color" style="background-color: {d.hex}"></span>
<span class="name">{d.name}</span>
<span class="hex">{d.hex}</span>
<span class="rgb">{d.rgb}</span>
<span class="hsl">{d.hsl}</span>
</div>
{/snippet}现在让我们把这两个代码段传递给<FilteredList>组件:
<FilteredList
data={colors}
field="name"
{header}
{row}
></FilteredList>然后到FilteredList组件那边,把header和row声明成组件级别的共享资产:
let { data, field, header, row } = $props();
最后用代码段和渲染标签替换占位的TODO,就得了
Never again will you have to memorize the hex code for MistyRose or PeachPuff.
隐式代码段资产 (Implict Snippet Props)
官方地说,组件内直接声明的代码段就是在 这些组件上的props
现在把上一题的header和row从App组件正文移动到App组件引用FilteredList组件的地方:
然后从FilteredList组件引用参数里移除header和row(d):
<script>
import FilteredList from './FilteredList.svelte';
import { colors } from './data.js';
</script>
<FilteredList
data={colors}
field="name"
>
{#snippet header()}
<header>
<span class="color"></span>
<span class="name">name</span>
<span class="hex">hex</span>
<span class="rgb">rgb</span>
<span class="hsl">hsl</span>
</header>
{/snippet}
{#snippet row(d)}
<div class="row">
<span class="color" style="background-color: {d.hex}"></span>
<span class="name">{d.name}</span>
<span class="hex">{d.hex}</span>
<span class="rgb">{d.rgb}</span>
<span class="hsl">{d.hsl}</span>
</div>
{/snippet}
</FilteredList>
<style>
header, .row {
display: grid;
align-items: center;
grid-template-columns: 2em 4fr 3fr;
gap: 1em;
padding: 0.1em;
background: var(--bg-1);
border-radius: 0.2em;
}
header {
font-weight: bold;
}
.row:hover {
background: var(--bg-2);
}
.color {
aspect-ratio: 1;
height: 100%;
border-radius: 0.1em;
}
.rgb, .hsl {
display: none;
}
@media (min-width: 40rem) {
header, .row {
grid-template-columns: 2em 4fr 3fr 3fr;
}
.rgb {
display: block;
}
}
@media (min-width: 60rem) {
header, .row {
grid-template-columns: 2em 4fr 3fr 3fr 3fr;
}
.hsl {
display: block;
}
}
</style>组件内任何不在已声明代码段的内容都会变成children代码段。显然header没有参数,我们可以直接把代码块标签去了,直接把header变成children组件:
然后去FilteredList组件源代码里把header改成children:

字面意义地改名成children:
<script>
let { data, field, children, row } = $props();
let search = $state('');
let filtered = $derived.by(() => {
if (search === '') return data;
const regex = new RegExp(search, 'i');
return data.filter((d) => regex.test(d[field]));
});
</script>
<div class="list">
<label>
Filter: <input bind:value={search} />
</label>
<div class="header">
{@render children()}
</div>
<div class="content">
{#each filtered as d}
{@render row(d)}
{/each}
</div>
</div>
<style>
.list {
display: flex;
flex-direction: column;
height: 100%;
}
.header {
border-top: 1px solid var(--bg-2);
padding: 0.2em 0;
}
.content {
flex: 1;
overflow: auto;
padding-top: 0.5em;
border-top: 1px solid var(--bg-2);
}
</style>
【跳过】运动 (Motion)
插值(Tweened Values)
泉涌 (Springs)
进阶双向绑定
绑定带有Contenteditable属性的元素 (Contenteditable Bindings)
带有Contenteditable属性的元素支持textContent和innerHTML的绑定:

绑定each块中的元素 (Each Block Bindings)
可以绑定each块中的元素属性

绑定媒体元素 (Media Elements)
可以给<audio>和<video>元素绑定属性,使其更易于自定义播放器UI(比如说本题给出的AudioPlay.svelte组件)
首先,给audio元素绑好属性(鉴于本题预先声明了同名变量,所以可以使用短绑定语法):
然后给<button>元素添加事件监听器,用于翻转paused的布尔值:
现在播放器已经具备基本的功能,现在添加 拖动滑条自由选择播放进度 的功能:
在滑动条的pointerdown监听器内有个seek函数,我们要在seek里更新播放时长time:
当进度条到达尽头时,我们要让进度条回到起点:
<audio>和<video>所有可绑定属性如下:
- 七个只读 绑定:
duration— the total duration, in secondsbuffered— an array of{start, end}objectsseekable— dittoplayed— dittoseeking— booleanended— booleanreadyState— number between (and including) 0 and 4
- 五个 双向 绑定:
currentTime— the current position of the playhead, in secondsplaybackRate— speed up or slow down (1is ‘normal’)paused— this one should be self-explanatoryvolume— a value between 0 and 1muted— a boolean value where true is mutedvideo元素额外还有两个只读绑定:videoWidth和videoHeight
绑定只读的图片宽高属性 (Dimensions)
我们可以给任何元素都绑定上clientWidth、clientHeight、offsetWidth和offsetHeight属性,并且Svelte会借助缩放监视器 ResizeObserver更新绑定值
这些绑定项是只读的——修改w和h的值不会影响元素的任何更新逻辑
提示
display: inline elements do not have a width or height (except for elements with ‘固有的/内在的’ dimensions, like <img> and <canvas>), and cannot be observed with a ResizeObserver. You will need to change the display style of these elements to something else, such as inline-block.

实例自指向 (This):bind:this
可以使用bind:this助记符获取对组件中当前元素的只读绑定
<script>
import { paint } from './gradient.js';
$effect(() => {
const context = canvas.getContext('2d');
let frame = requestAnimationFrame(function loop(t) {
frame = requestAnimationFrame(loop);
paint(context, t);
});
return () => {
cancelAnimationFrame(frame);
};
});
</script>
<canvas width={32} height={32}></canvas>
<style>
canvas {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #666;
mask: url(./svelte-logo-mask.svg) 50% 50% no-repeat;
mask-size: 60vmin;
-webkit-mask: url(./svelte-logo-mask.svg) 50% 50% no-repeat;
-webkit-mask-size: 60vmin;
}
</style>在本题中,$effect尝试创建一个canvas上下文,但canvas尚未被定义。我们首先要在组件顶部定义这个canvas变量:
然后到canvas元素里把canvas元素绑定到canvas变量上
这样,canvas变量的值会在组件挂载前保持undefined——换句话说,在$effect执行前不能访问canvas变量
绑定组件 (Component Bindings)
你可以给DOM元素绑定属性,也可以给组件绑定props参数。比方说,我们可以给<keypad>组件绑定value参数,让它看上去就像是一个表单元素
<script>
import Keypad from './Keypad.svelte';
let pin = $state('');
let view = $derived(pin
? pin.replace(/\d(?!$)/g, '•')
: 'enter your pin');
function onsubmit() {
alert(`submitted ${pin}`);
}
</script>
<h1 style="opacity: {pin ? 1 : 0.4}">
{view}
</h1>
<Keypad {onsubmit} />首先,我们需要标记value变量为可绑定的 。在keypad.svelte组件内,更新value的声明让它不只是一个props参数,还是被$bindable符文修饰了的参数:
<script>
let { value = $bindable(''), onsubmit } = $props();
const select = (num) => () => (value += num);
const clear = () => (value = '');
</script>然后到App.svelte组件里,给Keypad组件绑定value参数:
现在,当用户与键盘交互时,父组件中pin的值会被即时更新
提示
不要滥用组件props绑定。如果我们在程序中有很多要这样绑定的参数,就会使得我们难以追踪数据流,特别是there is no ‘single source of truth’的时候
绑定到组件实例上 (Bindings to Component Instances)
用bind:this不止能获取DOM元素自身,也可以获取组件实例自身
这在极少数情况下,当我们需要和组件交互(而不是向其传递最新的props参数)。回顾动作 (Actions)中的例子,加个清空画板的按钮会很棒
首先从Canvas.svelte中导出一个函数:
export function clear() {
context.clearRect(0, 0, canvas.width, canvas.height);
}然后回到App组件声明一下canvas实例,在引用Canvas组件的地方把Canvas组件本身绑定到canvas变量上:
最后添加一个按钮,触发canvas.clear操作:
【跳过】进阶过渡动画
延迟过渡 (Deferred Transitions)
动画 (Animations)
API上下文 (Context API)
setContext与getContext
Context API允许组件之间互相交换数据,而不需要开发者把数据和函数变成props在组件间传递,抑或是费心思调度一大堆事件。这个机制很高级也很有用。在本节中,我们将使用Context API重制Schotter by George Nees——生成式艺术的先锋之一
在Canvas.svelte组件中,有一个函数是用来向canvas添加item的:
function addItem(fn) {
$effect(() => {
items.add(fn);
return () => items.delete(fn);
});
}我们可以使用setContext让这个函数在<Canvas>的组件内起效
而在App组件的子组件(Square.svelte)内,我们使用getContext从canvas中取出并调用函数:

这样,感觉还是有点无聊了,让我们给网格加一些小惊喜:

setContext和getContext只能在组件初始化时被调用,这样context才能正确响应。本题用到的键canvas可以为任何对象,而不只是字符串,不只是'canvas',这有助于控制能够访问该context的角色
提示
Context是个包罗万象的东西,甚至可以包含响应式状态(reactive state)。这允许我们给子组件传递随时可能变化的值:
特殊元素
监听窗口级事件 <svelte:window>
就像给任意DOM元素添加事件监听器一样,我们也可以给窗口对象(<svelte:window)添加事件监听器
<script>
let key = $state();
let keyCode = $state();
function onkeydown(event) {
key = event.key;
keyCode = event.keyCode;
}
</script>
<svelte:window />
<div style="text-align: center">
{#if key}
<kbd>{key === ' ' ? 'Space' : key}</kbd>
<p>{keyCode}</p>
{:else}
<p>Focus this window and press any key</p>
{/if}
</div>
<style>
div {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
flex-direction: column;
}
kbd {
border-radius: 4px;
font-size: 6em;
padding: 0.2em 0.5em;
background-color: #eeeeee;
border-top: 5px solid #f9f9f9;
border-left: 5px solid #f9f9f9;
border-right: 5px solid #aaaaaa;
border-bottom: 5px solid #aaaaaa;
color: #555;
}
</style>这里已经定义好了onkeydown函数(与onkeydown事件同名,所以接下来可以使用短绑定助记符),我们唯一要做的就是把函数绑定到svelte:window上:

绑定<svelte:window>
我们也可以绑定<svelte:window>的属性,比如scrollY:
可以绑定的窗口参数如下:
innerWidthinnerHeightouterWidthouterHeightscrollX(只读)scrollY(只读)online—window.navigator.onLine的别名
只有scrollX和scrollY是只读的
<svelte:document>
相关信息
fire?什么fire?
The <svelte:document> element allows you to listen for events that fire on document. This is useful with events like selectionchange, which doesn’t fire on window.
Add the onselectionchange handler to the <svelte:document> tag:
提示
Avoid mouseenter and mouseleave handlers on this element, as these events are not fired on document in all browsers. Use <svelte:body> instead.
<svelte:body>
相关信息
不知道fire到底指的是什么,所以选择不翻译
Similar to <svelte:window> and <svelte:document>, the <svelte:body> element allows you to listen for events that fire on document.body. This is useful with the mouseenter and mouseleave events, which don’t fire on window.
Add onmouseenter and onmouseleave handlers to the <svelte:body> tag...
...and hover over the <body>.
修改<head>标签中的内容 `svelte:head
<svelte:head>元素允许我们给<head>标签里插入元素。这可以用来动态化<title>和<meta>元素,用于SEO优化
由于这部分内容很难在交互式教程里演示出来,因此这里我们将展示另一个用途——加载样式表
<svelte:head>
<link ref="stylesheet" href="/tutorial/stylesheets/{selected}.css" />
</svelte:head>
<svelte:head>
<link rel="stylesheet" href="/tutorial/stylesheets/{selected}.css" />
</svelte:head>
选完直接回不去了
提示
SSR模式下<svelte:head>中的内容是与其余HTML内容分开发送的
动态组件 <svelte:element>
有时候我们无法预知哪些元素需要被加载,与其手写一大堆{#if ...}块
<script>
const options = ['h1', 'h2', 'h3', 'p', 'marquee'];
// 有几个元素就得写几个if块
let selected = $state(options[0]);
</script>
<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
{#if selected === 'h1'}
<h1>I'm a <code><h1></code> element</h1>
{:else}
<p>TODO others</p>
{/if}不如使用<svelte:element>:
this={selected}中绑定上去的值可为任意字符串,也可以是一个为假的值——这时不会渲染元素
<svelte:boundary>
相关信息
为防止不小心给程序留下一个残缺的状态,我们需要把这部分程序置于使用error boundry的<svelte:boundry>元素中
在本题的例子中,<FlakyComponent>组件有一个BUG——点击按钮会将mouse元素设置为null,导致{mouse.x}和{mouse.y}无法渲染
理想情况下我们当然可以轻松修复这个BUG,但现实是骨感的——有时候这个组件是另一个组件的子组件,有时候就是得和预料之外的清空斗智斗勇。
先给<FlakyComponent />套上<svelte:boundary>:
现在还没有什么用,因为我们还没有给<svelte:boundary>指定一个监听器。添加一个failed代码段,提供错误消息可视化:
failed函数
<svelte:boundary> 在其子组件抛出错误时会自动触发failed函数,并传入两个参数:
error:错误对象(通常包含message等属性)reset:一个函数,调用它可以重置错误边界,重新尝试渲染子组件
【Kimi K2】svelte:boundary元素的默认行为
在 Svelte 中,<svelte:boundary> 目前(Svelte 5 最新实现)只暴露了两个“用户可感知”的入口:
onerror回调
类型:(error: unknown, errorInfo: { component: SvelteComponent }) => void
触发时机:子树第一次抛出错误时同步调用;同一子树再次出错不会重复触发(除非先 reset)。
用途:打日志、上报监控、触发副作用,不能阻止错误继续传播到边界内部。{#snippet failed(error, reset)}
触发时机:一旦onerror执行完,边界立即卸载原来的子树,把占位内容替换成该 snippet;
入参:error—— 被捕获的那个错误对象reset—— 无参函数,调用后边界会重新挂载子树,整个错误状态清零;如果子树再次崩溃,上面两个入口会再次被执行。
其实还有个
pending无参函数会被调用,但这个函数是在boundary被首次创建时才会被调用的
不会触发的(官方实现里根本没有):
- 没有
onreset回调 - 没有
componentDidCatch之类的生命周期 - 没有
beforeError/afterError钩子 - 错误不会冒泡到父级边界(Svelte 目前只支持单层边界,子树出错就停在最近的那一层;父级边界收不到同一次错误)
- 也不会把错误抛到全局
window.error(除非你在onerror里自己手动throw或console.error)
小结
| 入口 | 触发次数 | 参数 | 典型用途 |
|---|---|---|---|
onerror | 每轮错误一次 | error, { component } | 日志/监控 |
failed 片段 | 每轮错误一次 | error, reset() | 展示 UI 与恢复按钮 |
除此之外,<svelte:boundary> 不会再调用任何其它用户提供的函数。
现在点击按钮后,boundary内的按钮会被替换成错误消息文本
接下来可以通过failed的第二个参数尝试重置mouse属性:
接下来还可以给<svelte:boundary>添加一个onerror监听器,利用failed函数的第二个参数调用事件:
<svelte:boundary onerror={(e) => console.error(e)}>
<FlakyComponent />
{#snippet failed(error, reset)}
<p>Oops! {error.message}</p>
<button onclick={reset}>Reset</button>
{/snippet}
</svelte:boundary>这可以用来给日志服务发送错误消息,也可以用来在<boundary>之外添加一些UI
This is useful for sending information about the error to a reporting service, or adding UI outside the error boundary itself.
模块脚本 (<script module>)
代码复用 (Sharing Code)
在我们目前练习过的所有demo中,每个组件实例被初始化时都会执行<script>块中的内容。对于组件的大多数应用情景,这就是我们需要的了
在极罕见的情况下,我们需要在单个组件实例外执行一些代码。回顾绑定媒体元素 (Media Elements)中的例子,我们可以同时播放四个音频,但如果能做到播放一个音频同时自动暂停其他播放的话会更好
这种情况下,我们可以声明一个<script module>块,其中的代码只会被执行一次,只在代码块被首次执行时执行其中的代码。将下面的代码置于AudioPlayer.svelte的顶部(注意这是单独的一个脚本标签):
现在组件内部就可以自己交流,而不需要额外管理一个状态了:
导出 (export)
从<script module>块中导出的内容都会变成源自该<module>的一个导出(原文莫名绕口)
Anything exported from a
modulescript block becomes an export from the module itself.
让我们导出一个pauseAll函数
然后到App组件里导入pauseAll函数:
相关信息
这里补习一下ESBuild 6的import语法:
import { name } from './module.js'
➜ 按名字导入,对应源文件里必须export const name = …(具名导出)import name from './module.js'
➜ 导入的是该模块的默认导出(export default …)
然后在按钮的事件监听器中应用这个函数:

提示
我们不能设置默认导出symbol ,因为svelte组件就是那个默认导出
You can’t have a default export, because the component is the default export.
下一步 (Next Steps)
<script>
let characters = ['🥳', '🎉', '✨'];
let confetti = $state(new Array(100)
.fill()
.map((_, i) => {
return {
character:
characters[i % characters.length],
x: Math.random() * 100,
y: -20 - Math.random() * 100,
r: 0.1 + Math.random() * 1
};
})
.sort((a, b) => a.r - b.r));
$effect(() => {
let frame = requestAnimationFrame(function loop() {
frame = requestAnimationFrame(loop);
for (const confetto of confetti) {
confetto.y += 0.3 * confetto.r;
if (confetto.y > 120) confetto.y = -20;
}
});
return () => {
cancelAnimationFrame(frame);
}
});
</script>
{#each confetti as c}
<span
style:left="{c.x}%"
style:top="{c.y}%"
style:scale={c.r}
>
{c.character}
</span>
{/each}
<style>
span {
position: absolute;
font-size: 5vw;
user-select: none;
}
:global(body) {
overflow: hidden;
}
</style>完结撒花 (Congratulations!)
你已经完成了Svelte交互式教程,准备好了开始构建成体系的前端网站
接下来的两部分将聚焦于SvelteKit,一个用于构建全尺寸页面的集成框架
如果你还不能消化完全目前的信息,不必担心!你现在就可以写页面了,不需要SvelteKit。在终端里运行这个,并跟随指示:
npx sv create然后尝试改造src/routes/+page.svelte。当你准备好了,就点击下面的链接继续你的旅程