用途
该技能面向 Svelte 项目,在需要编写、编辑或分析 Svelte 组件时加载,用于提供编写快速、稳健、现代 Svelte 代码的实践指导,重点覆盖 Svelte 5 的 runes 体系与相关写法。
主要能力与实践建议
响应式状态($state)
只对需要响应式的变量使用 $state rune,即那些会触发 $effect、$derived 或模板表达式更新的变量,其余变量使用普通变量即可。
对象和数组($state({...}) 或 $state([...]))会被转换为深层响应式,意味着修改会触发更新。代价是:为了细粒度响应式,对象必须被代理,这会带来性能开销。对于只被重新赋值(而非修改)的大型对象,应改用 $state.raw,例如 API 响应常属于这种情况。
派生值($derived)
从状态计算值时应使用 $derived,而不是 $effect:
// do this
let square = $derived(num * num);
// don't do this
let square;
$effect(() => {
square = num * num;
});
注意:$derived 接收的是表达式,而不是函数。如果需要使用函数(例如表达式过于复杂),应使用 $derived.by。
派生值是可写的,可以像 $state 一样对其赋值,区别在于它们会在表达式变化时重新求值。
如果派生表达式是对象或数组,会按原样返回,不会被转换为深层响应式。在极少数需要的情况下,可以在 $derived.by 内部使用 $state。
$effect
Effect 是一种逃生舱,应尽量避免使用,尤其要避免在 effect 中更新状态。
- 如果需要将状态同步到外部库(如 D3),通常使用 {@attach ...} 更简洁。
- 如果需要在用户交互后运行代码,应把代码直接放入事件处理器,或按需使用函数绑定。
- 如果需要为调试记录值,应使用 $inspect。
- 如果需要观察 Svelte 之外的内容,应使用 createSubscriber。
切勿用 if (browser) {...} 或类似方式包裹 effect 的内容,因为 effect 不会在服务端运行。
$props
应把 props 视为会变化的值。例如依赖 props 的值通常应使用 $derived:
// @errors: 2451
let { type } = $props();
// do this
let color = $derived(type === 'danger' ? 'red' : 'green');
// don't do this — `color` will not update if `type` changes
let color = type === 'danger' ? 'red' : 'green';
$inspect.trace
$inspect.trace 是响应式的调试工具。如果某个东西没有正确更新或运行次数超出预期,可以在 $effect 或 $derived.by(或其调用的任意函数)的第一行添加 $inspect.trace(label),以追踪其依赖并找出是哪个依赖触发了更新。
#key 块
#key 块在希望某个表达式变化时运行过渡/动画时很有用,但代价可能很高,因为它每次都会卸载内部的所有元素/组件,因此应谨慎使用。例如,不要用 #key 来让子组件 <script> 块中的表达式重新运行,而应考虑把这些表达式包裹在 $derived 中。
事件
任何以 on 开头的元素属性都会被视为事件监听器:
<button onclick={() => {...}}>click me</button>
<!-- attribute shorthand also works -->
<button {onclick}>...</button>
<!-- so do spread attributes -->
<button {...props}>...</button>
如果需要给 window 或 document 附加监听器,可以使用 svelte:window 和 svelte:document:
<svelte:window onkeydown={...} />
<svelte:document onvisibilitychange={...} />
避免为此使用 onMount 或 $effect。
Snippets
Snippets 用于定义可复用的标记片段,可通过 {@render ...} 标签实例化,或作为 props 传给组件。它们必须在模板内声明。
{#snippet greeting(name)}
<p>hello {name}!</p>
{/snippet}
{@render greeting('world')}
在组件顶层(即不在元素或块内)声明的 snippet 可以在 <script> 中引用。如果 snippet 不引用组件状态,则也可在 <script module> 中使用,此时可导出供其他组件使用。
Each 块
优先使用带键的 each 块,因为这样 Svelte 可以精准插入或删除条目,而不是更新既有条目的 DOM,从而提升性能。
注意:键必须唯一标识对象,不要使用索引作为键。
如果需要对条目进行修改(例如 bind:value={item.count}),应避免解构。
在 CSS 中使用 JavaScript 变量
如果要在 CSS 中使用 JS 变量,可以用 style: 指令设置 CSS 自定义属性:
<div style:--columns={columns}>...</div>
之后即可在组件的 <style> 中引用 var(--columns)。
为子组件设置样式
组件 <style> 中的 CSS 作用域限于该组件。如果父组件需要控制子组件样式,首选方式是使用 CSS 自定义属性:
<!-- Parent.svelte -->
<Child --color="red" />
<!-- Child.svelte -->
<h1>Hello</h1>
<style>
h1 {
color: var(--color);
}
</style>
如果无法这样做(例如子组件来自某个库),可以使用 :global 覆盖样式:
<div>
<Child />
</div>
<style>
div :global {
h1 {
color: red;
}
}
</style>
Context
考虑使用 context,而不是在共享模块中声明状态。这样可以把状态限定在应用中需要它的部分,并消除服务端渲染时状态在用户之间泄漏的可能。
使用 createContext,而不是 setContext 和 getContext,因为它提供类型安全。
异步 Svelte
如果使用 5.36 或更高版本,可以在组件中直接使用 await 表达式和 hydratable 来处理 promise。注意,它们需要在 vite.config.js 的插件选项中或 svelte.config.js 中启用 experimental.async 选项,因为它们尚未被视为完全稳定。
避免旧式特性
新代码始终使用 runes 模式,并避免那些已有更现代替代方案的特性:
- 使用 $state 代替隐式响应式(例如 let count = 0; count += 1)
- 使用 $derived 和 $effect 代替 $: 赋值与语句(但只在没有更好方案时才使用 effect)
- 使用 $props 代替 export let、$$props 和 $$restProps
- 使用 onclick={...} 代替 on:click={...}
- 使用 {#snippet ...} 和 {@render ...} 代替 <slot>、$$slots 和 svelte:fragment
- 使用 <DynamicComponent> 代替 <svelte:component this={DynamicComponent}>
- 使用 import Self from './ThisComponent.svelte' 和 <Self> 代替 svelte:self
- 使用带 $state 字段的类在组件间共享响应式,而不是使用 stores
- 使用 {@attach ...} 代替 use:action
- 在 class 属性中使用 clsx 风格的数组与对象,代替 class: 指令