SuperAPI AI 工具集

svelte-core-bestpractices

前端与建站

查看项目

更新于 10-16 14:44

技能介绍

用途

该技能面向 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: 指令