跳到主要内容

Svelte 速查表:Svelte 5 runes、snippet、props、stores,附 Svelte 4 对照写法

Svelte 5 速查表,runes ($state / $derived / $effect)、snippet、props、stores,带 Svelte 4 对照。

  • 本地处理
  • 分类 开发运维
  • 适合 格式化、校验、压缩或检查和代码相关的文本。
125
Runes (20)

$state , 响应式变量

Runes
<script lang="ts">
  let count = $state(0);
  let user = $state({ name: 'Lei', age: 30 });
</script>

<button onclick={() => count++}>{count}</button>
<input bind:value={user.name} />

说明:$state 把一个 let 变量变成响应式。读它 (模板里或别的 rune 里) 会订阅,写它会触发重渲染。基本类型和对象都能用 , 返回的对象是 Proxy,深层追踪修改,所以 `user.name = "Hong"` 不用整体重赋也能更新。Svelte 4 里所有需要响应的 `let`,在 5 里都换成 `$state`。

Svelte 4 对照写法
<script>
  let count = 0;
  let user = { name: 'Lei', age: 30 };
</script>

$derived , 带缓存的派生值

Runes
<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);
  let label = $derived(`count is ${count}`);
</script>

<p>{count} × 2 = {doubled}</p>

说明:$derived 接收一个表达式,返回一个值: 它读到的任何响应式依赖一变就重算,而且结果会缓存,同次渲染里多次读取无开销。是 Svelte 4 `$: doubled = count * 2` 在 runes mode 下的替代。表达式必须是纯函数; 多语句逻辑用 $derived.by。

Svelte 4 对照写法
$: doubled = count * 2;

$derived.by , 多语句派生

Runes
<script lang="ts">
  let items = $state<number[]>([1, 2, 3]);
  let stats = $derived.by(() => {
    let sum = 0;
    let max = -Infinity;
    for (const n of items) {
      sum += n;
      if (n > max) max = n;
    }
    return { sum, max, avg: sum / items.length };
  });
</script>

说明:$derived.by 接收一个 函数 而不是表达式,所以你能写循环、声明局部变量、一步步拼出最终值。缓存和依赖追踪和 $derived 一样。需要多条语句就用 .by; 一行能算清楚的用普通 $derived。

$effect , 带自动依赖追踪的副作用

Runes
<script lang="ts">
  let url = $state('/api/me');
  let data = $state<unknown>(null);

  $effect(() => {
    const ctrl = new AbortController();
    fetch(url, { signal: ctrl.signal })
      .then(r => r.json())
      .then(d => { data = d; });
    return () => ctrl.abort();  // cleanup
  });
</script>

说明:$effect 在组件挂载后,以及它 读到的 任何响应式值变化时执行。返回一个函数做清理 (取消定时器、abort fetch、移监听),下次执行前和 unmount 时都会调用。同时替代 Svelte 4 的 `$: { … }` 响应式块 和 onMount + onDestroy 组合。

Svelte 4 对照写法
import { onDestroy } from 'svelte';
$: {
  const ctrl = new AbortController();
  fetch(url, { signal: ctrl.signal })...
  onDestroy(() => ctrl.abort());
}

$effect.pre , DOM 更新前执行

Runes
<script lang="ts">
  let items = $state<string[]>([]);
  let listEl: HTMLDivElement;
  let shouldAutoScroll = false;

  $effect.pre(() => {
    // read items so this re-runs when they change
    items;
    if (!listEl) return;
    shouldAutoScroll =
      listEl.scrollTop + listEl.clientHeight >= listEl.scrollHeight - 5;
  });

  $effect(() => {
    items;
    if (shouldAutoScroll) listEl?.scrollTo({ top: listEl.scrollHeight });
  });
</script>

说明:$effect.pre 在 Svelte 更新 DOM 之前 执行,可以读到更新前的布局 (滚动位置、焦点、选区),配合更新后的逻辑做"只在用户原本就在底部时才自动滚到底"这类需求。普通 $effect 在 DOM 更新后跑 , 一般用 $effect 就好,需要更新前测量时才用 .pre。

$props , 强类型组件 props

Runes
<script lang="ts">
  interface Props {
    title: string;
    count?: number;
    onSelect?: (id: string) => void;
  }
  let { title, count = 0, onSelect }: Props = $props();
</script>

<h2>{title} ({count})</h2>

说明:$props 是 runes mode 下组件接收输入的方式。带默认值和类型一起解构,一行写完 , 不再需要每个 prop 写 `export let`,也不需要 `interface $$Props`。返回的是 Proxy,父组件更新时解构出来的值仍然响应。`let { ...rest } = $props()` 能接住其余所有属性,适合写包装组件。

Svelte 4 对照写法
<script>
  export let title;
  export let count = 0;
</script>

$bindable , 让 prop 支持双向绑定

Runes
<!-- Input.svelte -->
<script lang="ts">
  let { value = $bindable('') }: { value?: string } = $props();
</script>
<input bind:value />

<!-- Parent.svelte -->
<script lang="ts">
  let text = $state('');
</script>
<Input bind:value={text} />

说明:Svelte 5 里 props 默认只读。给某个 prop 套一层 $bindable 才允许父组件 `bind:value={…}`,子组件里改它也会反传回去。没套 $bindable 的 prop 用 `bind:` 是编译错误。$bindable 的参数是父组件没绑定时的默认值。

$inspect , 调试响应式变化

Runes
<script lang="ts">
  let count = $state(0);
  let user = $state({ name: 'Lei' });

  $inspect(count, user);

  // custom handler
  $inspect(count).with((type, val) => {
    console.log(`[${type}]`, val);
  });
</script>

说明:$inspect 是仅开发期生效的 rune,它追踪的任何值变化时都打日志,包括深层修改。回答"这个值是谁改的?"比到处撒 console.log 强得多 , 它跟得上响应式的变化。生产构建里会被剥掉。链 .with(fn) 能自定义 handler,替代 console.log。

$host , 自定义元素宿主

Runes
<svelte:options customElement="my-counter" />
<script lang="ts">
  let count = $state(0);
  function emit() {
    $host().dispatchEvent(
      new CustomEvent('change', { detail: count })
    );
  }
</script>
<button onclick={() => { count++; emit(); }}>{count}</button>

说明:$host 只在组件被编译成自定义元素 (`<svelte:options customElement="…" />`) 时才有意义。它返回宿主 HTMLElement,你可以 dispatch 真正的 DOM CustomEvent,外部用 `addEventListener` 监听。不是自定义元素的话用不上 , 普通 Svelte 组件请用回调 props。

$state.raw , 退出深层追踪

Runes
<script lang="ts">
  // huge dataset — Proxy overhead is wasteful
  let scene = $state.raw({ vertices: bigArray, faces: bigFaces });

  function loadScene(next: typeof scene) {
    scene = next;  // reassignment DOES trigger
  }
  // scene.vertices.push(…)  // mutation does NOT trigger
</script>

说明:$state.raw 不做 Proxy 包装 , 只有 整体重赋 会触发更新,嵌套修改不会。适合大型、热点、形状固定的数据结构 (Three.js 场景、大 Map、解析器 AST),每属性 Proxy 开销不容忽视。代价: 要更新就得整体替换。

$derived 可被直接重赋覆盖

Runes
<script lang="ts">
  let total = $state(100);
  // optimistic UI: show a guessed value, let the real one win later
  let display = $derived(total);

  function bump() {
    display = total + 1; // temporary override
  }
  // next time `total` changes, `display` snaps back to deriving from it
</script>

<button onclick={bump}>{display}</button>

说明:Svelte 5.25+ 里 $derived 的值可以被直接赋值。赋值会设一个临时覆盖,保持到它的某个依赖下次变化为止,届时自动回到计算值。适合乐观 UI: 先显示一个瞬时猜测,等真实数据到了再让派生值盖回去。普通 $state 加 $effect 更清晰时就别用这招。

$state.snapshot 取 proxy 的普通副本

Runes
<script lang="ts">
  let form = $state({ name: 'Lei', tags: ['a', 'b'] });

  function save() {
    // structuredClone / JSON / 3rd-party libs choke on the Proxy
    const plain = $state.snapshot(form);
    localStorage.setItem('draft', JSON.stringify(plain));
    // `plain` is a non-reactive deep clone, safe to pass anywhere
  }
</script>

说明:$state.snapshot(value) 返回 $state proxy 的静态深拷贝,去掉所有响应性。把 state 交给不认识 Proxy 的代码时用它: structuredClone、对嵌套数据 JSON.stringify、第三方库、postMessage。这些场景直接读 proxy 可能因为 Proxy 陷阱报错或行为异常。

$effect.root 手动作用域副作用

Runes
<script lang="ts">
  // create effects OUTSIDE the component lifecycle, clean up by hand
  const cleanup = $effect.root(() => {
    let n = $state(0);
    $effect(() => console.log('n is', n));
    const id = setInterval(() => n++, 1000);
    return () => clearInterval(id);
  });

  // later, when you decide:  cleanup();
</script>

说明:$effect.root(fn) 建一个不绑定组件生命周期的 effect 作用域。里面创建的 effect 一直活着,直到你自己调用返回的销毁函数。适合需要比组件活得更久的 effect,或在非组件代码 (共享模块) 里搭响应式。代价: 忘了调销毁函数就会泄漏。

$effect.tracking 判断是否在响应式上下文

Runes
<script lang="ts">
  let count = $state(0);

  // false at the top level of <script>
  console.log($effect.tracking()); // false

  $effect(() => {
    count;
    console.log($effect.tracking()); // true — inside an effect
  });
</script>

说明:$effect.tracking() 在代码跑在追踪上下文 ($effect、$derived 或模板) 里时返回 true。这是给库作者的底层助手: 想让一个函数在 effect 里读时走响应式、在外面单次调用时走廉价路径。绝大多数应用代码用不到。

untrack 读值但不订阅

Runes
<script lang="ts">
  import { untrack } from 'svelte';
  let a = $state(0);
  let b = $state(0);

  // re-runs only when `a` changes, ignores `b`
  $effect(() => {
    a;
    const snapshotB = untrack(() => b);
    console.log('a changed, b was', snapshotB);
  });
</script>

说明:untrack(fn) 在 fn 里读响应式值时不把它们登记为外层 effect 或 derived 的依赖。适合 effect 只想对部分值响应、对另一些只是窥一眼,或打断导致无限循环的意外依赖。从 `svelte` 导入,不是 rune。

.svelte.ts 模块: 组件外用响应式

Runes
// counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);
  return {
    get value() { return count; },
    increment: () => count++,
    reset: () => { count = initial; },
  };
}

// Counter.svelte
<script lang="ts">
  import { createCounter } from './counter.svelte.ts';
  const counter = createCounter();
</script>
<button onclick={counter.increment}>{counter.value}</button>

说明:以 `.svelte.ts` (或 `.svelte.js`) 结尾的文件能用 runes,所以可以把响应式逻辑写在组件外面再 import 进来。$state 要通过 getter 暴露,直接返回裸值会拷贝基本类型、丢掉响应性。这是 Svelte 5 不靠 store 写可组合共享状态的方式,类似别处的 "composable"。

$props.id() 稳定唯一 id

Runes
<script lang="ts">
  // one consistent id across SSR and hydration
  const uid = $props.id();
</script>

<label for={`${uid}-email`}>Email</label>
<input id={`${uid}-email`} type="email" />
<p id={`${uid}-hint`}>We never share it.</p>
<input aria-describedby={`${uid}-hint`} />

说明:$props.id() (Svelte 5.20+) 返回一个在服务端渲染和客户端 hydration 间保持一致的唯一字符串,不会引发 mismatch。用它给 `for`/`id` 配对和 `aria-describedby` 牵线,不用硬编码可能在组件多次渲染时撞车的 id。替代会破坏 SSR 的临时 Math.random() id。

深层响应式: 嵌套数组和对象

Runes
<script lang="ts">
  let board = $state({
    rows: [
      { id: 1, cells: [0, 0, 0] },
      { id: 2, cells: [0, 0, 0] },
    ],
  });

  // every level is reactive through the Proxy:
  board.rows[0].cells[1] = 5;     // tracked
  board.rows.push({ id: 3, cells: [] }); // tracked
  board.rows[0].cells.splice(0, 1);      // tracked
</script>

说明:普通 $state 把值包成深 Proxy,所以任意嵌套层级的修改都被追踪: 给某个格子赋值、往嵌套数组 push、splice、原地 sort。几乎从不需要重赋整棵树来触发更新。例外是 $state.raw (浅) 和非普通对象/数组的值 (Map、Set、类实例),后者需要 svelte/reactivity 的助手。

SvelteMap / SvelteSet 响应式集合

Runes
<script lang="ts">
  import { SvelteMap, SvelteSet } from 'svelte/reactivity';

  let seen = new SvelteSet<string>();
  let cache = new SvelteMap<string, number>();

  function visit(id: string) {
    seen.add(id);        // triggers re-render
    cache.set(id, Date.now());
  }
</script>

<p>{seen.size} visited</p>

说明:$state 里放裸 `new Map()` / `new Set()` 不是深响应的,.set/.add 这类修改不触发更新。从 `svelte/reactivity` 导入 SvelteMap、SvelteSet,API 一样、可直接替换、但带响应性。它们的 `.size`、迭代、`.has()` 都参与响应。SvelteDate、SvelteURL 同理。

在 effect 里安全写状态

Runes
<script lang="ts">
  let query = $state('');
  let results = $state<string[]>([]);

  // effect that writes a DIFFERENT state it does not read — safe
  $effect(() => {
    const q = query.trim();
    fetchResults(q).then((r) => {
      results = r;       // writes results, never reads it → no loop
    });
  });
</script>

说明:$effect 只有在 读 和 写 同一个响应式值时才循环。写一个 effect 不读的值 (这里的 `results`) 是安全且常见的: `query` 变就 fetch,结果存进 `results`。心法: 列出你读的 (依赖) 和你写的 (输出),两个集合相交才会循环。

Svelte 4 → 5 (12)

let → $state

Svelte 4 → 5
// Svelte 4 (legacy)
let count = 0;

// Svelte 5 (runes)
let count = $state(0);

说明:Svelte 4 里 `<script>` 顶层每个 `let` 都自动响应 (编译器魔法)。Svelte 5 runes mode 下 `let` 是普通 JS,要响应就显式用 $state。好处: 显式、不踩雷、`.svelte.ts` 模块里行为一致。迁移: 每个需要响应的 `let` 套上 $state(…); 不响应的局部变量原样保留。

$: doubled = … → $derived

Svelte 4 → 5
// Svelte 4 (legacy)
$: doubled = count * 2;
$: console.log(doubled);

// Svelte 5 (runes)
let doubled = $derived(count * 2);
$effect(() => console.log(doubled));

说明:Svelte 4 一个 `$:` 语法同时表示"派生值" (赋值时) 和"副作用" (不赋值时)。Svelte 5 一分为二: $derived 算值并缓存,$effect 跑副作用。这个拆分本身就是重点 , 调用处一眼看出是哪种,两种都能抽成普通函数。

on:click → onclick

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<button on:click={handle}>Click</button>
<input on:input={(e) => value = e.currentTarget.value} />

<!-- Svelte 5 -->
<button onclick={handle}>Click</button>
<input oninput={(e) => value = e.currentTarget.value} />

说明:Svelte 5 用原生 HTML 属性名挂事件: `onclick`、`oninput`、`onsubmit`、`onkeydown`。Svelte 4 的 `on:` 前缀在 legacy mode 还能用,但新代码已弃用。handler 签名一样,event 对象一样 , 只改了属性名。和 React 同样思路: 贴近 DOM,少一个框架专属语法。

<slot /> → {@render children()}

Svelte 4 → 5
<!-- Svelte 4 (legacy) Modal.svelte -->
<div class="modal">
  <slot />
</div>

<!-- Svelte 5 Modal.svelte -->
<script lang="ts">
  let { children } = $props();
</script>
<div class="modal">
  {@render children()}
</div>

说明:传给组件的 children 自动成为一个叫 `children` 的 snippet。要渲染就从 $props 解构出 children,然后 `{@render children()}`。具名 slot 变具名 snippet props: `let { header, children } = $props()`。snippet 能带参数 , `{@render row(item)}` 是关键 , 所以严格比 slot 更强。

export let → $props()

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<script lang="ts">
  export let title: string;
  export let count = 0;
</script>

<!-- Svelte 5 -->
<script lang="ts">
  let { title, count = 0 }: { title: string; count?: number } =
    $props();
</script>

说明:Svelte 4 每个 prop 一行 `export let`。Svelte 5 一次 $props() 解构搞定,带类型注解。默认值从名字后面的 `= 0` 挪到解构里的 `= 0`。纯类型 prop 写在类型注解里; 运行时默认值还是用解构语法。原来每个 prop 一行,现在每个组件一行。

createEventDispatcher → 回调 prop

Svelte 4 → 5
<!-- Svelte 4 (legacy) Picker.svelte -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher<{ select: string }>();
</script>
<button on:click={() => dispatch('select', 'a')}>A</button>

<!-- Parent: <Picker on:select={(e) => console.log(e.detail)} /> -->

<!-- Svelte 5 Picker.svelte -->
<script lang="ts">
  let { onSelect }: { onSelect: (id: string) => void } = $props();
</script>
<button onclick={() => onSelect('a')}>A</button>

<!-- Parent: <Picker onSelect={(id) => console.log(id)} /> -->

说明:Svelte 5 里不再 dispatch 自定义事件 , 接收一个 callback 函数作为 prop 直接调。回调签名就是契约,类型自然流过去,不用 `CustomEvent<…>` 那套体操。父组件传普通 handler `onSelect={(id) => …}`,不再是 `on:select={(e) => …e.detail}`。createEventDispatcher 还能用,但已弃用。

beforeUpdate / afterUpdate → $effect.pre / $effect

Svelte 4 → 5
// Svelte 4 (legacy)
import { beforeUpdate, afterUpdate } from 'svelte';
beforeUpdate(() => { /* before DOM */ });
afterUpdate(() => { /* after DOM */ });

// Svelte 5 (runes)
$effect.pre(() => { items; /* before DOM */ });
$effect(() => { items; /* after DOM */ });

说明:Svelte 4 的 beforeUpdate/afterUpdate 在 每次 更新都跑,没法限定是哪个变化触发的。Svelte 5 用 $effect.pre (DOM 前) 和 $effect (DOM 后) 替代,而且只在你里面 读到 的那些响应式值变化时重跑。把你关心的值引用一下,effect 才会追踪它们。

Svelte 4 对照写法
import { beforeUpdate, afterUpdate } from 'svelte';

$$props / $$restProps → $props() 剩余

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<script>
  export let href;
</script>
<a {href} {...$$restProps}><slot /></a>

<!-- Svelte 5 -->
<script lang="ts">
  let { href, children, ...rest } = $props();
</script>
<a {href} {...rest}>{@render children()}</a>

说明:Svelte 4 有 `$$props` (全部 props) 和 `$$restProps` (没声明的 props)。Svelte 5 两个都没了,改成解构出具名 props,再用 $props() 的 `...rest` 接住其余的,展开到元素上。rest 对象带类型、有响应性,而且你能看清到底接了哪些。

Svelte 4 对照写法
<a {href} {...$$restProps}><slot /></a>

$$slots → 可选 snippet props

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
{#if $$slots.footer}
  <footer><slot name="footer" /></footer>
{/if}

<!-- Svelte 5 -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { footer }: { footer?: Snippet } = $props();
</script>
{#if footer}
  <footer>{@render footer()}</footer>
{/if}

说明:Svelte 4 用 `$$slots.name` 判断某个具名 slot 是否传了。Svelte 5 里 slot 就是 snippet prop,渲染前用普通真值判断 (`{#if footer}`) 看父组件传没传。把 snippet 声明成可选 (`footer?`),"没传"自然就是 undefined。

Svelte 4 对照写法
{#if $$slots.footer}<slot name="footer" />{/if}

svelte:component → 直接 {表达式}

Svelte 4 → 5
<!-- Svelte 4 (legacy) -->
<svelte:component this={Selected} {...props} />

<!-- Svelte 5: components are values, render directly -->
<script lang="ts">
  import A from './A.svelte';
  import B from './B.svelte';
  let Selected = $state(A);
</script>
<Selected {...props} />

说明:Svelte 5 里组件就是普通值,所以持有组件的变量直接写成 `<Selected />` 就能渲染,动态组件不再需要 `<svelte:component>` (它还能用但已弃用)。标签名要大写开头或含点号,编译器才当它是组件而非 HTML 元素。

Svelte 4 对照写法
<svelte:component this={Selected} />

svelte:fragment → snippet 不再需要包裹

Svelte 4 → 5
<!-- Svelte 4 (legacy) — fragment to avoid an extra <div> -->
<svelte:fragment slot="list">
  <li>a</li>
  <li>b</li>
</svelte:fragment>

<!-- Svelte 5 — a snippet can hold multiple elements directly -->
{#snippet list()}
  <li>a</li>
  <li>b</li>
{/snippet}

说明:`<svelte:fragment>` 存在的意义是往具名 slot 里塞多个元素又不引入多余的包裹元素。snippet 本来就能装多个顶层节点,所以 Svelte 5 里这个 fragment 标签没用了。定义一个含多个元素的 snippet,在想要的位置渲染即可。

Svelte 4 对照写法
<svelte:fragment slot="list">...</svelte:fragment>

tick() 保持不变

Svelte 4 → 5
<script lang="ts">
  import { tick } from 'svelte';
  let text = $state('');
  let el: HTMLInputElement;

  async function append(s: string) {
    text += s;
    await tick();        // wait for the DOM to flush
    el.scrollLeft = el.scrollWidth; // now the width is up to date
  }
</script>

说明:tick() 返回一个 promise,在挂起的状态变更应用到 DOM 后 resolve。从 Svelte 4 到 5 原样保留,配 runes 很顺: 改 $state,`await tick()`,再读刚布局好的 DOM。需要在 Svelte 更新完页面后再动作时,用它而不是 setTimeout(…, 0)。

组件 (12)

<script lang="ts"> , 强类型组件

组件
<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    items: string[];
    row?: Snippet<[string, number]>;
  }
  let { items, row }: Props = $props();
</script>

<ul>
  {#each items as item, i}
    <li>{row ? '' : item}{#if row}{@render row(item, i)}{/if}</li>
  {/each}
</ul>

说明:`<script>` 加 `lang="ts"` 整个组件就被类型检查。从 `svelte` 导入 `Snippet<[args]>` 给 snippet props 写类型。配合 $props 解构,父组件传的每个 prop 都有补全,拼错在构建时就被抓。普通 Svelte 项目和 SvelteKit 项目一致。

<style> , 组件内作用域

组件
<div class="card">
  <h3>Hello</h3>
</div>

<style>
  .card {
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1rem;
  }
  h3 { margin: 0; }
  /* unscope with :global */
  :global(body) { font-family: system-ui; }
</style>

说明:`<style>` 块里的样式默认只在当前组件生效 , 编译器自动追加一个哈希 class,这里的 `.card` 不会和别处的 `.card` 撞。需要影响外部 (body、第三方组件) 用 `:global(…)` 跳出作用域。没用到的选择器编译时会警告,重构残留一抓一个准。

class: 指令 , 条件 class

组件
<script lang="ts">
  let active = $state(false);
  let count = $state(0);
</script>

<button
  class="btn"
  class:active
  class:has-count={count > 0}
  onclick={() => { active = !active; count++; }}
>
  {count}
</button>

说明:class:name={cond} 根据布尔值加/去 class。变量名等于 class 名时可以简写为 `class:active`。多个 class: 指令互相独立,会和静态 `class="…"` 叠加。比拼字符串 `class={"btn " + (active ? "active" : "")}` 干净,而且 Svelte 自动去重。

style: 指令 , 内联样式

组件
<script lang="ts">
  let x = $state(0);
  let color = $state('tomato');
</script>

<div
  style:transform={`translateX(${x}px)`}
  style:color
  style:--accent={color}
>
  hello
</div>

说明:style:prop={value} 声明式地设单个 inline 样式,包括 CSS 自定义属性: `style:--accent={color}` 暴露一个变量,子级 CSS 用 `var(--accent)` 读。`style:color` 是 `style:color={color}` 的简写。比拼 `style={…}` 字符串干净,Svelte 还能跳过解析直接设单个属性。

带默认值和剩余的 props

组件
<script lang="ts">
  interface Props {
    href: string;
    target?: '_blank' | '_self';
    children: import('svelte').Snippet;
  }
  let {
    href,
    target = '_self',
    children,
    ...rest
  }: Props & Record<string, unknown> = $props();
</script>

<a {href} {target} {...rest}>{@render children()}</a>

说明:解构出单独的 props 带默认值,用 interface 写类型,用 `...rest` 接住其余所有属性。把 `{...rest}` 展开到底层元素上,使用者就能传 `id`、`data-…`、`aria-…` 而你不用一个个列。包装组件的标准写法 , 一行 `...rest` 省掉几十个 prop 转发。

children snippet , 默认内容

组件
<!-- Card.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { children }: { children: Snippet } = $props();
</script>
<div class="card">{@render children()}</div>

<!-- usage -->
<Card>
  <h2>Hi</h2>
  <p>Body text</p>
</Card>

说明:父组件写 `<Card>…</Card>`,标签之间的所有内容自动绑到一个叫 `children` 的 snippet prop 上。Card 内部从 $props 解构 children,在原本 `<slot />` 的位置写 `{@render children()}`。从 `svelte` 导入的 Snippet 类型让渲染内容也带上类型检查。

Svelte 4 对照写法
<!-- Svelte 4 Card.svelte -->
<div class="card">
  <slot />
</div>

generics="T" 类型安全的泛型组件

组件
<!-- Select.svelte -->
<script lang="ts" generics="T extends { id: string }">
  import type { Snippet } from 'svelte';
  let { options, selected = $bindable(), label }:
    {
      options: T[];
      selected?: T;
      label: Snippet<[T]>;
    } = $props();
</script>

{#each options as opt}
  <button onclick={() => selected = opt}>{@render label(opt)}</button>
{/each}

说明:`<script lang="ts">` 上的 `generics` 属性给整个组件声明类型参数,写法和 TypeScript 泛型子句完全一样 (可用 `extends` 加约束)。父组件得到完整推断: 传 `options: User[]`,那 `selected`、`label` 就都是 `User` 类型。这是不靠 `any` 写可复用 Select/List/Table 组件的方式。

svelte:self → 直接 import 自身文件

组件
<!-- Tree.svelte — recursive component -->
<script lang="ts">
  import Tree from './Tree.svelte'; // import yourself
  let { node }: { node: { name: string; children?: any[] } } = $props();
</script>

<li>
  {node.name}
  {#if node.children?.length}
    <ul>
      {#each node.children as child}
        <Tree node={child} />
      {/each}
    </ul>
  {/if}
</li>

说明:Svelte 4 用 `<svelte:self>` 做递归。Svelte 5 里组件直接 import 自己的文件、用名字引用自己即可,这招在任何框架都成立。用来渲染嵌套树、评论楼层、多级菜单。递归处用 `{#if}` 加终止条件守一下,免得无限套。

Svelte 4 对照写法
<svelte:self node={child} />

svelte:window 绑窗口尺寸/滚动/按键

组件
<script lang="ts">
  let width = $state(0);
  let scrollY = $state(0);
  function onKey(e: KeyboardEvent) {
    if (e.key === 'Escape') console.log('esc');
  }
</script>

<svelte:window
  bind:innerWidth={width}
  bind:scrollY
  onkeydown={onKey}
/>

<p>{width}px wide, scrolled {scrollY}px</p>

说明:`<svelte:window>` 给全局 window 挂事件监听和双向绑定,不用手写 addEventListener。可绑: innerWidth、innerHeight、scrollX、scrollY、online、devicePixelRatio。事件用同样的 `onkeydown`/`onresize` 属性写法。Svelte 随组件挂载/卸载自动加减监听,无需自己清理。

svelte:head 设页面标题和 meta

组件
<script lang="ts">
  let { post }: { post: { title: string; excerpt: string } } = $props();
</script>

<svelte:head>
  <title>{post.title} · My Blog</title>
  <meta name="description" content={post.excerpt} />
  <meta property="og:title" content={post.title} />
</svelte:head>

说明:`<svelte:head>` 从组件内部往文档 `<head>` 注入元素,随 props 变化响应式更新。SSR 时它们渲染进首屏 HTML,正是搜索引擎和社交卡片读取的内容。设每页 `<title>`、meta description、Open Graph 标签、canonical 链接的标准位置。

svelte:boundary 捕获渲染错误

组件
<svelte:boundary onerror={(e) => report(e)}>
  <RiskyWidget />

  {#snippet failed(error, reset)}
    <div class="err">
      <p>Something broke: {error.message}</p>
      <button onclick={reset}>Try again</button>
    </div>
  {/snippet}
</svelte:boundary>

说明:`<svelte:boundary>` (Svelte 5.3+) 把它内部渲染或 effect 抛出的错误兜住,一个坏组件不会让整页白屏。提供一个 `failed` snippet 显示兜底,它收到 error 和一个用于重试的 `reset` 函数。`onerror` prop 是上报错误追踪的地方。相当于 React 的 error boundary。

svelte:element 动态标签名

组件
<script lang="ts">
  let { level = 2, children }:
    { level?: 1 | 2 | 3 | 4; children: import('svelte').Snippet } =
    $props();
  let tag = $derived(`h${level}` as const);
</script>

<svelte:element this={tag}>
  {@render children()}
</svelte:element>

说明:`<svelte:element this={tag}>` 渲染一个标签名在运行时由字符串决定的 HTML 元素。适合按 `level` prop 在 h1–h4 间选的标题组件,或能当 `<div>`、`<section>`、`<a>` 的多态组件。`this` 为空值时什么都不渲染。void 元素 (img、br) 不能有子节点。

事件 (10)

onclick , 原生 DOM 事件

事件
<script lang="ts">
  let count = $state(0);
  function handle(e: MouseEvent) {
    console.log(e.clientX, e.clientY);
    count++;
  }
</script>

<button onclick={handle}>{count}</button>
<button onclick={() => count = 0}>reset</button>

说明:Svelte 5 的事件 handler 就是普通 HTML 属性 (`onclick`、`oninput`、`onsubmit`、`onkeydown`…),接收一个函数。函数拿到真正的 DOM Event,带完整 TS 类型 , `MouseEvent`、`KeyboardEvent`、`SubmitEvent`。没有 on: 前缀、没有 event.detail、没有合成事件池。一行的用箭头,复用的抽成函数。

onkeydown , 键盘事件

事件
<script lang="ts">
  let value = $state('');
  function handle(e: KeyboardEvent) {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      console.log('submit', value);
    }
    if (e.key === 'Escape') value = '';
  }
</script>

<textarea bind:value onkeydown={handle} />

说明:Svelte 5 去掉了事件修饰符语法 (`on:keydown|preventDefault`) , 自己在 handler 里调 `e.preventDefault()` / `e.stopPropagation()`。修饰键看 `e.shiftKey`、`e.metaKey`、`e.ctrlKey`、`e.altKey`。代价是稍微啰嗦点换来显式: 没有魔法,所有效果在 handler 里一目了然。

事件修饰符替代 , preventDefault 包装

事件
// utils/events.ts
export function preventDefault<E extends Event>(fn: (e: E) => void) {
  return (e: E) => { e.preventDefault(); fn(e); };
}

// usage
<script lang="ts">
  import { preventDefault } from './utils/events';
  function submit() { /* ... */ }
</script>
<form onsubmit={preventDefault(submit)}>...</form>

说明:没有 on: 修饰符语法了,把最常用的模式抽成小工具就行。`preventDefault(fn)` 返回一个先调 preventDefault 再调 fn 的 handler。stopPropagation、once、self 同理。一个五行的小工具,既找回 Svelte 4 的手感,又不重新引入框架专属语法。

自定义事件 , 回调 prop

事件
<!-- Toggle.svelte -->
<script lang="ts">
  let { value = $bindable(false), onChange }:
    { value?: boolean; onChange?: (v: boolean) => void } = $props();
  function flip() {
    value = !value;
    onChange?.(value);
  }
</script>
<button onclick={flip}>{value ? 'on' : 'off'}</button>

<!-- Parent.svelte -->
<Toggle bind:value={enabled} onChange={(v) => log(v)} />

说明:Svelte 5 里向上抛"用户做了什么"的官方姿势是回调 prop。组件在 $props 里声明 `onChange?` 然后调它; 父组件传普通函数。配 $bindable 暴露值很自然。不再用 createEventDispatcher、不再有字符串事件名、不再解 `e.detail` , 就是带类型的函数调用。

捕获阶段 , oncapture / onclickcapture

事件
<div onclickcapture={(e) => console.log('outer capture')}>
  <button onclick={(e) => console.log('inner bubble')}>
    click me
  </button>
</div>

说明:在任何 DOM 事件属性后面加 `capture` 就在捕获阶段挂监听,而不是冒泡阶段。`onclickcapture`、`onkeydowncapture`、`onfocusincapture`。父组件想在子 handler 之前看到事件 (打点、模态框 escape 处理、焦点陷阱) 时用得上。

onsubmit 不刷新地处理表单

事件
<script lang="ts">
  let email = $state('');
  function handle(e: SubmitEvent) {
    e.preventDefault();
    if (!email.includes('@')) return;
    console.log('submit', email);
  }
</script>

<form onsubmit={handle}>
  <input type="email" bind:value={email} required />
  <button type="submit">Send</button>
</form>

说明:给 form 挂普通 `onsubmit` handler,调 `e.preventDefault()` 阻止浏览器跳转。事件是带类型的 `SubmitEvent`。按钮保留 `type="submit"`、输入保留 `required`/`type="email"`,这样原生校验仍在 handler 之前触发。SvelteKit 里要完整渐进增强,优先用 form actions + use:enhance。

oninput 与 onchange: 实时与确认

事件
<script lang="ts">
  let live = $state('');
  let committed = $state('');
</script>

<!-- fires on every keystroke -->
<input oninput={(e) => live = e.currentTarget.value} />

<!-- fires on blur / Enter for text inputs -->
<input onchange={(e) => committed = e.currentTarget.value} />

<p>live: {live} · committed: {committed}</p>

说明:`oninput` 在用户每次敲键时触发,实时搜索、字数统计、即时校验用它。`onchange` 在控件确认值时触发一次 (文本框是失焦或回车,checkbox/select 是立即)。想要每次变化用 input,只在乎最终值用 change。`e.currentTarget` 类型正确指向该元素。

事件委托: 列表点击只挂一次

事件
<script lang="ts">
  let items = $state(['a', 'b', 'c']);
  function onListClick(e: MouseEvent) {
    const btn = (e.target as HTMLElement).closest('button[data-id]');
    if (btn) console.log('clicked', btn.getAttribute('data-id'));
  }
</script>

<ul onclick={onListClick}>
  {#each items as id}
    <li><button data-id={id}>{id}</button></li>
  {/each}
</ul>

说明:在容器上挂一个 handler,用 `e.target.closest(selector)` 找出点的是哪个子元素。Svelte 5 内部已对常见事件 (click、input) 做委托优化,但行数很多、想用单个 handler 读 `data-` 属性时,显式委托仍然有用。列表增减时这个 handler 一直挂着。

pointer 事件: 统一鼠标和触摸

事件
<script lang="ts">
  let dragging = $state(false);
  let x = $state(0);
  function down(e: PointerEvent) {
    dragging = true;
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
  }
  function move(e: PointerEvent) { if (dragging) x = e.clientX; }
  function up() { dragging = false; }
</script>

<div onpointerdown={down} onpointermove={move} onpointerup={up}
     style:transform={`translateX(${x}px)`}>drag</div>

说明:pointer 事件 (onpointerdown/move/up) 用一套代码同时覆盖鼠标、触摸、手写笔,不用写两份并行的 mouse 和 touch handler。`setPointerCapture(e.pointerId)` 把后续所有事件都导到你的元素上,即使指针移出去也是,拖拽手柄和滑块离不开它。每个事件带 `pointerType` ("mouse" | "touch" | "pen") 供分支判断。

组件事件: 回调列表

事件
<!-- Stepper.svelte -->
<script lang="ts">
  let { value = $bindable(0), onIncrement, onDecrement }:
    {
      value?: number;
      onIncrement?: (v: number) => void;
      onDecrement?: (v: number) => void;
    } = $props();
</script>

<button onclick={() => { value--; onDecrement?.(value); }}>-</button>
<span>{value}</span>
<button onclick={() => { value++; onIncrement?.(value); }}>+</button>

说明:一个组件可以暴露多个带类型的回调 prop,每个按它上报的动作命名 (onIncrement、onDecrement)。用 `?.()` 调用,不关心的父组件直接不传即可。这替代了 createEventDispatcher 的多个事件名,换成可发现、有补全的 prop 列表,父组件一眼看清有哪些事件。

指令 (14)

bind:value , 双向绑定

指令
<script lang="ts">
  let name = $state('');
  let agreed = $state(false);
  let pick = $state('a');
</script>

<input bind:value={name} placeholder="name" />
<input type="checkbox" bind:checked={agreed} />
<select bind:value={pick}>
  <option value="a">A</option>
  <option value="b">B</option>
</select>

说明:bind: 在响应式变量和表单控件之间建立双向绑定。文本框和 select 用 `bind:value`,checkbox / radio 用 `bind:checked`,文件输入用 `bind:files`,单选/复选组用 `bind:group`。省掉 `value={v} oninput={(e) => v = e.target.value}` 样板,自动做类型转换 (`<input type="number" bind:value={n}>` 直接给数字)。

bind:this , 取元素引用

指令
<script lang="ts">
  let inputEl: HTMLInputElement | undefined = $state();

  $effect(() => {
    inputEl?.focus();
  });
</script>

<input bind:this={inputEl} />

说明:bind:this={ref} 在挂载之后 把 DOM 元素 (或组件实例) 写到变量里,挂载前是 undefined。配 $effect 在 ref 就绪时行动 , `$effect(() => inputEl?.focus())` 实现自动聚焦。相当于 Svelte 的 useRef / template refs。

use:action , DOM 生命周期钩子

指令
<script lang="ts">
  import type { Action } from 'svelte/action';

  const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
    function handle(e: MouseEvent) {
      if (!node.contains(e.target as Node)) callback();
    }
    document.addEventListener('click', handle, true);
    return { destroy() { document.removeEventListener('click', handle, true); } };
  };

  let open = $state(true);
</script>

{#if open}
  <div use:clickOutside={() => open = false}>menu</div>
{/if}

说明:action 是元素挂载时被调用的函数,接收元素和一个可选参数。返回对象,`destroy()` 用于清理,`update(newParam)` 用于参数变化。tooltip、click-outside、IntersectionObserver、接入第三方 DOM 库都靠它 , 任何需要把 DOM 行为绑到元素生命周期上又不想写包装组件的场景。

transition:fade , 进入和离开动画

指令
<script lang="ts">
  import { fade } from 'svelte/transition';
  let show = $state(true);
</script>

<button onclick={() => show = !show}>toggle</button>

{#if show}
  <div transition:fade={{ duration: 200 }}>hello</div>
{/if}

说明:transition:fn 在元素进入 DOM 和离开 DOM 时都跑动画。内置有 fade、fly、slide、scale、blur、draw、crossfade。参数写在指令表达式里。进出动画不一样就用 `in:` 和 `out:` (例如进入从下飞入、离开向左飞出)。

in: / out: , 进出分别配

指令
<script lang="ts">
  import { fly, fade } from 'svelte/transition';
  let show = $state(true);
</script>

{#if show}
  <div
    in:fly={{ y: 20, duration: 200 }}
    out:fade={{ duration: 150 }}
  >panel</div>
{/if}

说明:`in:` 只跑进入动画,`out:` 只跑离开动画,可以用不同的函数和不同的参数。常用搭配: 从下飞入,原地淡出 , 来时活泼,去时优雅。默认是 local,只有元素自己加入/移除时触发; 父块切换时也要跑就加 `|global`。

animate:flip , FLIP 列表重排

指令
<script lang="ts">
  import { flip } from 'svelte/animate';
  let items = $state([
    { id: 1, name: 'A' },
    { id: 2, name: 'B' },
    { id: 3, name: 'C' },
  ]);
  function shuffle() {
    items = [...items].sort(() => Math.random() - 0.5);
  }
</script>

<button onclick={shuffle}>shuffle</button>
<ul>
  {#each items as item (item.id)}
    <li animate:flip={{ duration: 250 }}>{item.name}</li>
  {/each}
</ul>

说明:animate:fn 在 keyed `{#each}` 列表的项被重新排序时执行。flip 助手 (First-Last-Invert-Play) 测量重排前后位置,自动 tween , 列表重排过渡丝滑,不用自己算 CSS。必须有 key 表达式 `(item.id)`,Svelte 才能跨渲染识别项。

bind:group , 单选/复选组

指令
<script lang="ts">
  let pick = $state<'a' | 'b' | 'c'>('a');
  let tags = $state<string[]>([]);
</script>

<!-- radio: one value -->
<label><input type="radio" bind:group={pick} value="a" /> A</label>
<label><input type="radio" bind:group={pick} value="b" /> B</label>

<!-- checkbox: array of values -->
<label><input type="checkbox" bind:group={tags} value="svelte" /> svelte</label>
<label><input type="checkbox" bind:group={tags} value="kit" /> kit</label>

说明:bind:group 把多个表单控件绑到同一个响应式变量。radio 给你被选中那个的 value; checkbox 给你被选中的所有 value 组成的数组。变量类型自动对上 , radio 是 string,checkbox 是 string[]。省掉"循环控件再聚合"的样板。

bind:value 用于 textarea 和数字

指令
<script lang="ts">
  let note = $state('');
  let age = $state(0);
  let pick = $state('a');
</script>

<textarea bind:value={note}></textarea>

<!-- number input coerces to a number automatically -->
<input type="number" bind:value={age} min="0" />

<!-- range slider -->
<input type="range" bind:value={age} min="0" max="120" />

说明:`bind:value` 在 textarea 和所有 input 类型上都好用。`type="number"` 和 `type="range"` 时 Svelte 自动把绑定值转成数字,所以 `age` 是真正的数字而非字符串,不用 parseInt。空的数字输入绑定为 `null`。同一个绑定能同时驱动指向同一 state 的滑块和数字框。

bind:clientWidth 读元素尺寸

指令
<script lang="ts">
  let w = $state(0);
  let h = $state(0);
</script>

<div bind:clientWidth={w} bind:clientHeight={h} class="box">
  {w} × {h}
</div>

说明:Svelte 提供只读的尺寸绑定: `clientWidth`、`clientHeight`、`offsetWidth`、`offsetHeight`、`contentRect`、`contentBoxSize`。元素尺寸变化时它们响应式更新 (底层是 ResizeObserver),依赖布局的逻辑无需手写 observer 就能重跑。它们是单向的: 只能读尺寸,不能设。

use:action 带 update() 响应参数

指令
<script lang="ts">
  import type { Action } from 'svelte/action';

  const tooltip: Action<HTMLElement, string> = (node, text) => {
    node.title = text ?? '';
    return {
      update(next) { node.title = next ?? ''; }, // param changed
      destroy() { node.removeAttribute('title'); },
    };
  };

  let label = $state('hello');
</script>

<button use:tooltip={label}>hover me</button>

说明:action 收到的参数是响应式值时,它变化 Svelte 就调返回的 `update(newParam)`,不会重建 action。实现 `update` 应用新参数 (这里设新 title),`destroy` 在卸载时清理。用 `svelte/action` 的 `Action<元素类型, 参数类型>` 给 action 写类型。

自定义 CSS transition 函数

指令
<script lang="ts">
  import { cubicOut } from 'svelte/easing';
  import type { TransitionConfig } from 'svelte/transition';

  function spin(node: Element, { duration = 400 } = {}): TransitionConfig {
    return {
      duration,
      easing: cubicOut,
      css: (t) => `transform: scale(${t}) rotate(${t * 360}deg); opacity: ${t}`,
    };
  }
  let show = $state(true);
</script>

{#if show}<div transition:spin>hi</div>{/if}

说明:自定义 transition 是一个返回 `{ duration, easing, css }` 的函数。`css(t, u)` 回调里 `t` 进入时从 0 到 1 (离开时 1 到 0),返回一段 CSS 字符串; Svelte 据此生成关键帧动画,跑在主线程之外。性能上优先用 `css` 而非 `tick`,只有纯 CSS 表达不了的效果才用 `tick(t)`。

crossfade 共享元素过渡

指令
<script lang="ts">
  import { crossfade } from 'svelte/transition';
  const [send, receive] = crossfade({ duration: 300 });
  let todos = $state([{ id: 1, t: 'a' }]);
  let done = $state<{ id: number; t: string }[]>([]);
</script>

{#each todos as item (item.id)}
  <li in:receive={{ key: item.id }} out:send={{ key: item.id }}>{item.t}</li>
{/each}
{#each done as item (item.id)}
  <li in:receive={{ key: item.id }} out:send={{ key: item.id }}>{item.t}</li>
{/each}

说明:crossfade() 返回一个 `[send, receive]` 对。一个元素带 `out:send={{ key }}` 离开某列表,另一个相同 key 的元素带 `in:receive={{ key }}` 进入时,Svelte 让它从旧位置飞到新位置。经典用法是 todo 在 "进行中" 和 "已完成" 列表间移动。两个列表的 key 要对上,形变才能接起来。

transition |global 修饰符

指令
<script lang="ts">
  import { fade } from 'svelte/transition';
  let outer = $state(true);
  let inner = $state(true);
</script>

{#if outer}
  {#if inner}
    <!-- plays only when `inner` itself toggles (local, default) -->
    <p transition:fade>local</p>
    <!-- plays even when `outer` is what mounted/unmounted -->
    <p transition:fade|global>global</p>
  {/if}
{/if}

说明:默认 transition 是 局部 的: 只在元素自身被加入/移除时播放,父块切换时不播。加 `|global` 让 transition 在祖先块挂载/卸载它时也播放。这个默认值在 Svelte 3 和 4 之间翻过,Svelte 5 里局部是默认,所以想要页面级进入动画时要刻意写 `|global`。

bind:open 绑定 details 展开

指令
<script lang="ts">
  let open = $state(false);
</script>

<button onclick={() => open = !open}>toggle from outside</button>

<details bind:open>
  <summary>More info</summary>
  <p>Content shown when open is {String(open)}</p>
</details>

说明:`bind:open` 把原生 `<details>` 的展开状态双向绑到响应式变量,既能读它是否展开,也能从页面别处用代码切换它。因为用的是真正的 `<details>` 而不是 div 重造,元素保持完整的可访问性和键盘可操作性。

流程 (10)

{#if} {:else if} {:else}

流程
<script lang="ts">
  let status = $state<'loading' | 'ok' | 'error'>('loading');
</script>

{#if status === 'loading'}
  <p>Loading…</p>
{:else if status === 'error'}
  <p class="err">Failed</p>
{:else}
  <p>Ready</p>
{/if}

说明:条件渲染。条件翻转时块被挂载/卸载 , 不是虚拟 DOM diff,元素真的进出。配 `transition:` 做进出动画。如果只是切换可见性又想保留 DOM (滚动位置、视频播放、焦点),应该改 class 而不是 #if。

{#each items as item (item.id)}

流程
<script lang="ts">
  let items = $state([
    { id: 1, name: 'apple' },
    { id: 2, name: 'banana' },
  ]);
</script>

<ul>
  {#each items as item, i (item.id)}
    <li>{i + 1}. {item.name}</li>
  {:else}
    <li>no items</li>
  {/each}
</ul>

说明:列表渲染。可选的 `(item.id)` 是 key , Svelte 用它在多次渲染间匹配项,所以重排时是移动现有 DOM 而不是销毁重建。第二个绑定 `, i` 是索引。`{:else}` 在列表为空时渲染。没 key 的话重排时动画和表单状态都会断。

{#await promise}

流程
<script lang="ts">
  let promise = $state<Promise<unknown>>(fetch('/api/me').then(r => r.json()));
</script>

{#await promise}
  <p>Loading…</p>
{:then data}
  <pre>{JSON.stringify(data, null, 2)}</pre>
{:catch err}
  <p class="err">{err.message}</p>
{/await}

说明:一个 promise 的 pending / fulfilled / rejected 三种状态分别渲染不同内容 , 不用手写状态机。重新赋 promise 会从 pending 状态重跑这个块。简写 `{#await promise then data}` 跳过 loading,适合已知会很快 resolve (有缓存) 的情况。

{#key expr} , 强制重建

流程
<script lang="ts">
  let userId = $state('alice');
</script>

<button onclick={() => userId = userId === 'alice' ? 'bob' : 'alice'}>
  switch
</button>

{#key userId}
  <UserProfile id={userId} />
{/key}

说明:{#key value} 在值变化时卸载并重新挂载里面的内容。适合子组件持有内部状态 (表单草稿、动画) 需要在上下文切换时重置,或第三方组件对 prop 变更处理不好的情况。相当于 React 给组件加 `key={…}`。

{@html} , 原始 HTML (XSS 警告)

流程
<script lang="ts">
  import DOMPurify from 'dompurify';
  let untrusted = $state('<p>hi <img src=x onerror="alert(1)"></p>');
  let safe = $derived(DOMPurify.sanitize(untrusted));
</script>

<!-- NEVER do this with user input: -->
<!-- {@html untrusted} -->

<!-- SAFE: sanitize first -->
{@html safe}

说明:{@html expr} 把值作为 原始 HTML 插入 , 不转义。表达式只要含来自用户、API 或任何外部源的内容,就是一个 XSS 漏洞。先用 DOMPurify (或同类) 清洗再传给 {@html}。安全用法: 构建时编译好的可信 markdown、自己端到端控制的服务端 HTML。

{#each} 解构与索引

流程
<script lang="ts">
  let pairs = $state<[string, number][]>([['a', 1], ['b', 2]]);
  let users = $state([{ id: 1, name: 'Lei', role: 'admin' }]);
</script>

{#each pairs as [name, value], i}
  <li>{i}: {name} = {value}</li>
{/each}

{#each users as { id, name, role } (id)}
  <li>{name} ({role})</li>
{/each}

说明:each 的项绑定支持完整解构: 数组模式 `[a, b]`、对象模式 `{ id, name }`,后面可跟索引 `, i`,再后面是 `(key)`。解构出你真正要渲染的字段。即使解构了也保留 `(key)`,它仍负责重排和动画的身份匹配。

{#each n} 按数量循环

流程
<script lang="ts">
  let rating = $state(3);
</script>

<!-- render `rating` filled stars out of 5 -->
{#each { length: 5 }, i}
  <span class:filled={i < rating}>★</span>
{/each}

说明:Svelte 5 允许 `{#each}` 遍历带 `length` 属性的类数组,所以 `{#each { length: 5 }, i}` 跑五次、`i` 走 0..4,不用临时造 `Array.from({ length: 5 })`。星级评分、分页圆点、固定网格都方便。`i` 索引才是你真正用到的值。

{#await} 简写: 只写 then

流程
<script lang="ts">
  let ready = $state<Promise<string>>(Promise.resolve('cached'));
</script>

<!-- no pending block: assumes it resolves fast -->
{#await ready then value}
  <p>{value}</p>
{/await}

<!-- catch-only: ignore the value, only show errors -->
{#await ready catch err}
  <p class="err">{err.message}</p>
{/await}

说明:promise 已经 resolve 或你不需要 loading 态时,用 `{#await promise then value}` 跳过 pending 块。对称的 `{#await promise catch err}` 只在 reject 时渲染。两者都是完整三分支形式的语法糖,某个分支无关紧要时用它们减少视觉噪音。

{@const} 块内常量

流程
<script lang="ts">
  let rows = $state([{ price: 100, qty: 3 }, { price: 50, qty: 2 }]);
</script>

{#each rows as row}
  {@const subtotal = row.price * row.qty}
  <li>
    {row.qty} × {row.price} = <strong>{subtotal}</strong>
    {#if subtotal > 200}<span class="hot">big</span>{/if}
  </li>
{/each}

说明:{@const name = expr} 在当前块 (#each、#if、#snippet、#await 分支) 作用域内声明一个局部常量。用它在这一次迭代里算一次值、在标记中复用,而不用重复写表达式,也不用为每一行在 script 里加 derived。它只能作为块的直接子节点,不能放在模板顶层。

{@debug} 暂停并打印值

流程
<script lang="ts">
  let user = $state({ name: 'Lei', score: 0 });
</script>

<!-- with devtools open, execution pauses here when `user` changes -->
{@debug user}

<p>{user.name}: {user.score}</p>

说明:{@debug var1, var2} 在列出的值变化时打印它们,并且若 devtools 打开会触发一个 `debugger` 断点,让你在上下文里检查。不带参数的 `{@debug}` 在每次状态变化都断。比在模板里乱撒 console.log 更聚焦的替代方案,上线前删掉。

Snippet (8)

{#snippet} , 定义 snippet

Snippet
<script lang="ts">
  let items = $state(['a', 'b', 'c']);
</script>

{#snippet row(item: string, i: number)}
  <li><strong>{i + 1}.</strong> {item}</li>
{/snippet}

<ul>
  {#each items as item, i}
    {@render row(item, i)}
  {/each}
</ul>

说明:snippet 是模板里定义的一段可复用标记。像函数一样能接带类型的参数。用 {@render name(args)} 渲染任意次。snippet 看得到外层作用域的变量 (词法作用域),抽重复标记又不用做包装组件的时候特别合适。它是 严格 局部的 , 跨组件共享要作为 prop 传过去。

{@render} , 调用 snippet

Snippet
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { header, children, footer }:
    { header?: Snippet; children: Snippet; footer?: Snippet } = $props();
</script>

<section>
  {#if header}<header>{@render header()}</header>{/if}
  <main>{@render children()}</main>
  {#if footer}<footer>{@render footer()}</footer>{/if}
</section>

说明:{@render snippet(...args)} 在标记的这个位置执行一个 snippet。snippet props 用 `svelte` 的 `Snippet<[arg1, arg2]>` 写类型。可选 snippet 渲染前用 #if 守护。这就是 headless / 包装组件的新写法: 每个"slot"都是一个 snippet prop,父组件决定标记。

snippet 作为 prop , headless 组件

Snippet
<!-- List.svelte -->
<script lang="ts" generics="T">
  import type { Snippet } from 'svelte';
  let { items, row }:
    { items: T[]; row: Snippet<[T, number]> } = $props();
</script>
<ul>
  {#each items as item, i}<li>{@render row(item, i)}</li>{/each}
</ul>

<!-- Parent.svelte -->
<List items={users} row={userRow} />
{#snippet userRow(u: User, i: number)}
  <strong>{i + 1}.</strong> {u.name} — {u.email}
{/snippet}

说明:snippet 相对 slot 的关键差异: 能带参数。List 组件每项调 `{@render row(item, i)}`; 父组件传自己的 snippet 决定每行长什么样。父组件既掌控了标记,又能用上 List 的迭代逻辑 , 真正的 headless 组合。配 `generics="T"` 让 item handler 也有类型。

children , 隐式的默认 snippet

Snippet
<!-- Modal.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { open, children }:
    { open: boolean; children: Snippet } = $props();
</script>
{#if open}
  <div class="modal-backdrop">
    <div class="modal">{@render children()}</div>
  </div>
{/if}

<!-- usage -->
<Modal open={true}>
  <h2>Hi</h2>
  <button>OK</button>
</Modal>

说明:组件标签之间的所有内容 , `<Modal>…</Modal>` , 自动成为一个绑到 `children` prop 的 snippet。在内容应该出现的位置写 `{@render children()}`。直接替代 `<slot />`。具名 slot 变成具名 snippet props: `<Modal>{#snippet header()}…{/snippet}…</Modal>`。

递归 snippet 渲染树

Snippet
<script lang="ts">
  type Node = { name: string; children?: Node[] };
  let root = $state<Node>({
    name: 'root',
    children: [{ name: 'a' }, { name: 'b', children: [{ name: 'c' }] }],
  });
</script>

{#snippet branch(node: Node)}
  <li>
    {node.name}
    {#if node.children}
      <ul>{#each node.children as child}{@render branch(child)}{/each}</ul>
    {/if}
  </li>
{/snippet}

<ul>{@render branch(root)}</ul>

说明:snippet 可以调用自己,递归结构 (树、嵌套评论、文件浏览器) 不用单独的递归组件就轻松实现。渲染当前节点,再对每个子节点 `{@render branch(child)}`。用 `{#if node.children}` 守一下,递归在叶子处终止。snippet 看得到它的词法作用域,所以 `<script>` 里定义的 helper 都能用。

snippet 兜底: 用 #if

Snippet
<!-- Avatar.svelte -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { name, badge }: { name: string; badge?: Snippet } = $props();
</script>

<span class="avatar">
  {name[0]}
  {#if badge}
    {@render badge()}
  {:else}
    <span class="dot" aria-hidden="true"></span>
  {/if}
</span>

说明:可选 snippet prop 实现 slot 默认值: 传了就渲染它,没传就在 `{:else}` 里用默认标记兜底。父组件只覆盖它在乎的部分。这是怎么做出"有合理默认又完全可定制"组件的方式,多数 headless UI 套件背后就是这个模式。

用 snippet 参数把数据向上传

Snippet
<!-- Resource.svelte (render-prop style) -->
<script lang="ts">
  import type { Snippet } from 'svelte';
  let { url, children }:
    { url: string; children: Snippet<[{ loading: boolean; data: unknown }]> } =
    $props();
  let loading = $state(true);
  let data = $state<unknown>(null);
  $effect(() => {
    loading = true;
    fetch(url).then(r => r.json()).then(d => { data = d; loading = false; });
  });
</script>

{@render children({ loading, data })}

说明:因为 snippet 能带参数,组件可以通过 `{@render children(payload)}` 把内部状态 传给 父组件,即 render-prop 模式。这里一个取数组件暴露 `{ loading, data }`,让父组件决定每种状态怎么显示。父组件掌控展示,组件掌控逻辑。干净的控制反转,不用层层传回调。

从 module script 导出 snippet

Snippet
<!-- icons.svelte -->
<script module lang="ts">
  export { check, cross };
</script>

{#snippet check()}<svg viewBox="0 0 16 16"><path d="M2 8l4 4 8-8"/></svg>{/snippet}
{#snippet cross()}<svg viewBox="0 0 16 16"><path d="M3 3l10 10M13 3L3 13"/></svg>{/snippet}

<!-- consumer.svelte -->
<script lang="ts">
  import { check, cross } from './icons.svelte';
</script>
{@render check()}

说明:在 `.svelte` 文件顶层声明的 snippet 可以从 `<script module>` 块导出,再被别的组件 import。这是跨文件共享标记块 (图标集、格式化徽章) 的方式。注意 `<script module>` 是旧 `<script context="module">` 在 Svelte 5 的新写法。

Stores (11)

writable , 基础 store

Stores
// stores/counter.ts
import { writable } from 'svelte/store';
export const count = writable(0);

// Counter.svelte
<script lang="ts">
  import { count } from './stores/counter';
</script>
<button onclick={() => count.update(n => n + 1)}>
  {$count}
</button>
<button onclick={() => count.set(0)}>reset</button>

说明:writable 创建一个带 `set(v)`、`update(fn)`、`subscribe(cb)` 的 store。`.svelte` 文件里加 `$` 前缀就自动订阅/取消订阅 , `$count` 响应式读到当前值。即使 Svelte 5 有 $state 处理局部状态,跨多个不相干组件共享的全局 state (鉴权、主题) 还是 store 最对。

readable , 从外部源派生

Stores
import { readable } from 'svelte/store';

export const now = readable(new Date(), (set) => {
  const id = setInterval(() => set(new Date()), 1000);
  return () => clearInterval(id);
});

// usage
<script>
  import { now } from './stores/now';
</script>
<p>{$now.toLocaleTimeString()}</p>

说明:readable 创建一个值来自外部源 (定时器、WebSocket、Subscription) 的 store。start 函数在首次有人订阅时运行,返回的 stop 函数在最后一个订阅者离开时调用。适合那种"维持成本不低"的源 , 只在有人看时才跑。

derived , 依赖其他 store 的 store

Stores
import { writable, derived } from 'svelte/store';

export const price = writable(100);
export const qty = writable(3);
export const total = derived(
  [price, qty],
  ([$p, $q]) => $p * $q
);

// usage: <p>{$total}</p>

说明:derived 接收一个或一组 store 加一个把它们当前值算出新值的函数。任一输入 store 变化时结果重算,产物本身也是一个可订阅的 store。支持异步派生: 回调第二个参数收 `set`,在异步逻辑里调 `set(v)`。

get , 一次性读 store 值

Stores
import { get } from 'svelte/store';
import { user } from './stores/user';

export async function checkout() {
  const current = get(user);  // synchronous, no subscription
  await fetch('/checkout', {
    method: 'POST',
    body: JSON.stringify({ userId: current.id }),
  });
}

说明:get(store) 同步读到当前值,不订阅 , Svelte 替你订阅、读、退订。事件 handler、fetch payload 这种"要值但不想要响应"的场景用它。`.svelte` 文件里需要响应式读还是用 `$store`,只有特意不要响应时才用 get。

$store 自动订阅

Stores
<script lang="ts">
  import { user } from './stores/user';
  // `user` is a writable<User | null>

  // Read & write reactively — no subscribe/unsubscribe code
  function logout() { $user = null; }
</script>

{#if $user}
  <p>Hi {$user.name}</p>
  <button onclick={logout}>logout</button>
{/if}

说明:仅在 `.svelte` 文件里,任何 store 加 `$` 前缀就能响应式读到当前值。编译器在挂载/卸载时自动插订阅/退订。写 `$store = v` 等价 `store.set(v)`。这是 Svelte store 最大的体验红利 , 没样板、没 `useStore()` hook。

自定义 store , 封装 API

Stores
import { writable } from 'svelte/store';

export function createCounter(initial = 0) {
  const { subscribe, set, update } = writable(initial);
  return {
    subscribe,
    inc: () => update(n => n + 1),
    dec: () => update(n => n - 1),
    reset: () => set(initial),
  };
}

export const counter = createCounter();
// usage: <button onclick={counter.inc}>{$counter}</button>

说明:自定义 store 就是任何暴露了 `subscribe(cb)` 的对象 , Svelte 不关心你怎么搭的。包一层 writable,只对外暴露想给的方法,把 set/update 藏起来。这就是怎么把 store 写得像 service (`counter.inc()` 而不是 `counter.update(n => n + 1)`),同时保留 `$counter` 响应式读。

store 还是 $state: 怎么选

Stores
// app-global, cross-route, lives in a .ts file → store
// stores/theme.ts
import { writable } from 'svelte/store';
export const theme = writable<'light' | 'dark'>('dark');

// local component state, or .svelte.ts module → $state
// Counter.svelte
<script lang="ts">
  let count = $state(0);   // not shared, just this component
</script>

说明:Svelte 5 的经验法则: 组件或 `.svelte.ts` 模块拥有的状态用 $state; 需要在普通 `.ts`/`.js` 文件里用值、或要给第三方代码一个基于 subscribe 的契约时用 store。store 没被弃用,它仍是互操作原语。别为追新把好好的 store 改成 runes。

derived store 异步: 用 set

Stores
import { writable, derived } from 'svelte/store';

export const query = writable('');

// async derived: second arg is `set`, return a cleanup
export const results = derived(
  query,
  ($query, set) => {
    const ctrl = new AbortController();
    fetch(`/api/search?q=${$query}`, { signal: ctrl.signal })
      .then((r) => r.json())
      .then(set);
    return () => ctrl.abort();
  },
  [] as string[], // initial value while first fetch is pending
);

说明:derived 回调带第二个 `set` 参数时就变成异步: 新值是你 (可能稍后) 传给 `set()` 的东西。返回一个清理函数,在下次重算前执行 (abort 正在飞的 fetch)。derived 的第三个参数是首次 set 触发前显示的初始值。这就是防抖搜索的写法。

持久化 store: 同步到 localStorage

Stores
import { writable } from 'svelte/store';

export function persisted<T>(key: string, initial: T) {
  const stored =
    typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
  const store = writable<T>(stored ? JSON.parse(stored) : initial);
  store.subscribe((v) => {
    if (typeof localStorage !== 'undefined')
      localStorage.setItem(key, JSON.stringify(v));
  });
  return store;
}

export const settings = persisted('settings', { dark: true });

说明:包一层 writable: 从 localStorage 读初始值,通过 subscribe 在每次变化时写回。用 typeof 守 `localStorage`,免得 SSR 时 (那里没有它) 模块崩。返回的是普通 store: `$settings.dark` 读、`$settings = …` 写,磁盘自动保持同步。

Context API 作用域依赖注入

Stores
// Parent.svelte
<script lang="ts">
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';
  const theme = writable('dark');
  setContext('theme', theme);   // available to all descendants
</script>

// DeepChild.svelte
<script lang="ts">
  import { getContext } from 'svelte';
  import type { Writable } from 'svelte/store';
  const theme = getContext<Writable<string>>('theme');
</script>
<p class={$theme}>themed</p>

说明:setContext(key, value) 让一个值对所有后代组件通过 getContext(key) 可用,不用一层层传 prop。它作用于组件子树,在组件初始化时解析,所以本身不是响应式的; 后代需要响应变化时,把 store (或 $state 支撑的对象) 作为值传进去。主题、表单状态、i18n 都很合适。

store.update 基于旧值更新

Stores
import { writable } from 'svelte/store';

export const cart = writable<{ id: string; qty: number }[]>([]);

export function addItem(id: string) {
  cart.update((items) => {
    const found = items.find((i) => i.id === id);
    if (found) return items.map((i) =>
      i.id === id ? { ...i, qty: i.qty + 1 } : i);
    return [...items, { id, qty: 1 }];
  });
}

说明:store.update(fn) 把当前值给你、要你返回下一个值,新值依赖旧值时 (计数器、开关、列表编辑) 用它最对。返回 新的 数组/对象而不是改入参,store 才会通知订阅者。新值与旧值无关时改用 set(v)。

SvelteKit (15)

+layout.server.ts 共享布局数据

SvelteKit
// src/routes/(app)/+layout.server.ts
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';

export const load: LayoutServerLoad = async ({ locals, url }) => {
  if (!locals.user) throw redirect(302, `/login?from=${url.pathname}`);
  return { user: locals.user };
};

// any +page.svelte under this layout:
// let { data } = $props();  → data.user is available

说明:`+layout.server.ts` 的 load 对该布局下每个页面都跑,数据合并进每个页面的 `data`。用它放每个嵌套页面都要的东西 (登录用户、导航、功能开关),也能用一次 redirect 把整个区段挡在鉴权之后。子页面的 load 与布局 load 并行跑,不是排在它后面。

load: depends 配 invalidate

SvelteKit
// +page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, depends }) => {
  depends('app:feed');          // custom invalidation key
  const res = await fetch('/api/feed');
  return { items: await res.json() };
};

// elsewhere, after posting:
import { invalidate } from '$app/navigation';
await invalidate('app:feed');   // re-runs the load above

说明:`depends(id)` 给 load 函数登记一个自定义依赖。之后调 `invalidate(id)` 会重跑所有声明了该 id 的 load,URL 不变。不是真实 fetch URL 的依赖用带命名空间的字符串如 `app:feed`。这样改动后能精确刷新该刷的 load,而不是一把 `invalidateAll()`。

use:enhance 渐进增强表单

SvelteKit
<script lang="ts">
  import { enhance } from '$app/forms';
  let saving = $state(false);
</script>

<form method="POST" action="?/save" use:enhance={() => {
  saving = true;
  return async ({ update }) => {
    await update();        // apply result, reset form if invalid
    saving = false;
  };
}}>
  <input name="title" required />
  <button disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
</form>

说明:`$app/forms` 的 `use:enhance` 升级一个真实 `<form>`,提交时不刷新页面: SvelteKit 在客户端 POST 数据、应用 action 结果、失效相关 load。回调在提交前跑 (设挂起标志); 返回的函数在提交后跑,里面 `update()` 应用结果。没有 JS 时表单仍作为普通 POST 工作。

具名 form actions

SvelteKit
// +page.server.ts
import type { Actions } from './$types';
export const actions: Actions = {
  create: async ({ request }) => { /* ... */ },
  delete: async ({ request }) => { /* ... */ },
};

// +page.svelte — pick the action with action="?/name"
<form method="POST" action="?/create"><button>Create</button></form>
<form method="POST" action="?/delete"><button>Delete</button></form>

说明:一个页面可以在 `actions` 导出里暴露多个具名 action; 表单用 `action="?/name"` 选一个。单 action 用 `default`,一个页面处理多种操作 (create、update、delete) 时用具名。每个都收到同样的 `{ request, locals, cookies }`,可以 `fail(status, data)` 或 `redirect`。`button formaction="?/name"` 能让一个表单里两个按钮指向两个 action。

$app/state: page 用 rune 替代 store

SvelteKit
<script lang="ts">
  // SvelteKit 2.12+ — runes instead of $app/stores
  import { page } from '$app/state';
</script>

<!-- no $ prefix; `page` is a reactive object -->
<h1>{page.url.pathname}</h1>
<p>route id: {page.route.id}</p>
{#if page.error}<p class="err">{page.error.message}</p>{/if}

说明:`$app/state` (SvelteKit 2.12+) 把 `page`、`navigating`、`updated` 暴露成 runes 驱动的响应式对象,直接读 `page.url`、`page.params`、`page.data`,不要 `$` 前缀。runes mode 下新代码用它取代 `$app/stores`。store 版本仍向后兼容,但今后优先用 `$app/state`。

错误页与 404 页

SvelteKit
<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from '$app/state';
</script>

<h1>{page.status}</h1>
<p>{page.error?.message ?? 'Something went wrong'}</p>
<a href="/">Go home</a>

<!-- thrown from a load: -->
// import { error } from '@sveltejs/kit';
// throw error(404, 'Post not found');

说明:`+error.svelte` 在 `load` 抛错或路由找不到时渲染; 读 `page.status` 和 `page.error` 显示对应信息。放在你想兜住的路由层级: 顶层 `+error.svelte` 是兜底的 404/500 页,嵌套的只处理它子树内的错误。从 `@sveltejs/kit` 抛 `error(status, message)` 来触发它。

+server.ts JSON API 端点

SvelteKit
// src/routes/api/todos/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url }) => {
  const tag = url.searchParams.get('tag');
  const todos = await db.todos.where({ tag });
  return json(todos);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  if (!body.title) throw error(400, 'title required');
  return json(await db.todos.create(body), { status: 201 });
};

说明:`+server.ts` 文件通过导出以方法命名的函数定义 HTTP 端点: GET、POST、PUT、PATCH、DELETE。返回一个 `Response`,`@sveltejs/kit` 的 `json(data, init)` 助手构造 JSON 响应。这些只在服务端跑,是在页面所在的同一个 SvelteKit 应用里暴露 REST/JSON API 的方式。

hooks.server.ts 全局中间件

SvelteKit
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  const token = event.cookies.get('session');
  event.locals.user = token ? await lookupUser(token) : null;

  const response = await resolve(event);
  response.headers.set('x-frame-options', 'DENY');
  return response;
};

说明:`hooks.server.ts` 里的 `handle` 钩子包住每个请求: 读 cookie/header,填充 `event.locals` (随后所有 load 和 action 都能用),调 `resolve(event)` 跑路由,再后处理响应 (安全头、日志)。用 `@sveltejs/kit/hooks` 的 `sequence()` 串多个钩子。这是 SvelteKit 的服务端中间件层。

页面选项: prerender / ssr / csr

SvelteKit
// +page.ts or +page.server.ts or +layout.ts
export const prerender = true;   // build to static HTML at build time
export const ssr = true;         // server-render on first load (default)
export const csr = true;         // hydrate + client-side nav (default)

// a fully static marketing page:
// prerender = true, ssr = true, csr = false  → zero JS shipped

说明:从 `+page`/`+layout` 文件导出这些常量,按路由控制渲染。`prerender = true` 在构建时把页面烤成静态 HTML (文档、博客很适合)。`ssr = false` 让它纯客户端 (SPA 孤岛)。`csr = false` 不发客户端 JS,得到完全静态的页面。它们沿布局级联,每个路由可独立调。

load 与 action 里读写 cookie

SvelteKit
// +page.server.ts
import type { Actions } from './$types';

export const actions: Actions = {
  login: async ({ cookies, request }) => {
    const form = await request.formData();
    const token = await signIn(form.get('email'));
    cookies.set('session', token, {
      path: '/', httpOnly: true, sameSite: 'lax',
      secure: true, maxAge: 60 * 60 * 24 * 7,
    });
  },
  logout: async ({ cookies }) => { cookies.delete('session', { path: '/' }); },
};

说明:服务端 `event` 上的 `cookies` API 安全读写 cookie: `cookies.get(name)`、`cookies.set(name, value, opts)`、`cookies.delete(name, opts)`。session cookie 务必传 `path` (通常 "/") 并设 `httpOnly`、`secure`、`sameSite`,这样 JS 读不到、也不跨站发送。SvelteKit 替你把 Set-Cookie 头挂到响应上。

+page.server.ts , 服务端 load

SvelteKit
// src/routes/posts/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';

export const load: PageServerLoad = async ({ params, locals }) => {
  const post = await locals.db.posts.findUnique({
    where: { slug: params.slug },
  });
  if (!post) throw error(404, 'not found');
  return { post };
};

// +page.svelte
<script lang="ts">
  let { data } = $props();
</script>
<h1>{data.post.title}</h1>

说明:`+page.server.ts` 里的 `load` 只 在服务端跑 , 适合 DB 查询、密钥、私有 API。返回的对象成为页面组件的 `data` prop。SvelteKit 生成 `$types` 实现端到端类型安全。需要服务端和客户端都跑同一份 load 时用 `+page.ts` (SSR 时在服务端跑,导航时在客户端跑)。

form actions , 渐进增强表单

SvelteKit
// +page.server.ts
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';

export const actions: Actions = {
  default: async ({ request, locals }) => {
    const data = await request.formData();
    const email = data.get('email')?.toString();
    if (!email) return fail(400, { email, missing: true });
    await locals.auth.signup(email);
    throw redirect(303, '/welcome');
  },
};

// +page.svelte
<form method="POST">
  <input name="email" required />
  <button>Sign up</button>
</form>

说明:form actions 在服务端处理 POST 提交,返回 JSON 或重定向。表单 不用 JS 也能用 , 真正的 `<form method="POST">` 是基线。加 `$app/forms` 的 `use:enhance` 做客户端增强: 不整页刷新、自动失效数据、提供乐观更新钩子。这就是渐进增强的标准姿势。

+page.ts , 通用 load

SvelteKit
// src/routes/feed/+page.ts
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, url }) => {
  const tag = url.searchParams.get('tag') ?? 'all';
  const res = await fetch(`/api/feed?tag=${tag}`);
  if (!res.ok) throw new Error('feed failed');
  const items = await res.json();
  return { items, tag };
};

说明:`+page.ts` 的 load 在 SSR 时在服务端跑,导航时在客户端跑。能在任何地方跑的 load 用它 , 调自己的 `/api/*`、公共 API、客户端本来就能拉的内容。需要服务端独有访问 (DB、env、session) 的用 `+page.server.ts`。两个能共存,结果会被合并。

$app/stores , page、navigating、updated

SvelteKit
<script lang="ts">
  import { page, navigating } from '$app/stores';
</script>

<h1>You are at {$page.url.pathname}</h1>
<p>params: {JSON.stringify($page.params)}</p>

{#if $navigating}
  <p>Loading {$navigating.to?.url.pathname}…</p>
{/if}

说明:SvelteKit 从 `$app/stores` 暴露内置 store。`page` 保存当前 url、params、data、route 信息 , 面包屑、激活链接高亮都用它。`navigating` 在路由切换中非空 (含 `to` 和 `from` URL) , 用来做加载条。`updated` 在部署了新版本时变为 true,可以提示用户刷新。

goto / invalidate , 编程式导航

SvelteKit
<script lang="ts">
  import { goto, invalidate, invalidateAll } from '$app/navigation';

  async function refresh() {
    await invalidate('/api/feed');  // re-run loads that fetched this URL
  }

  async function reset() {
    await invalidateAll();           // re-run every load on the page
    await goto('/?tab=home', { replaceState: true });
  }
</script>

说明:`goto(url, opts)` 编程式导航 , 等价用户点 `<a href>` 的效果。`invalidate(urlOrFn)` 告诉 SvelteKit "依赖过这个的 load 都已经过期,重跑",URL 不变。`invalidateAll()` 重跑当前页面所有 load。改动后调 invalidate 就能让 UI 显示最新数据,不用整页刷新。

常见坑 (13)

坑: 解构出的 prop 失去响应性

常见坑
<script lang="ts">
  let { count } = $props();

  // BAD — `double` is computed once, never updates
  // let double = count * 2;

  // GOOD — derive so it tracks `count`
  let double = $derived(count * 2);
</script>

<p>{count} → {double}</p>

说明:解构 `{ count } = $props()` 让 `count` 在模板里仍响应,但你若算一个普通的 `let double = count * 2`,那只跑一次、永不更新,你抓到的是值,不是响应式源。把派生包进 $derived,prop 变它才重跑。任何 `let x = 某响应式值` 的快照都踩同一个坑。

坑: 闭包里捕获的响应式值会过期

常见坑
<script lang="ts">
  let count = $state(0);

  // BAD — captures count=0 forever
  // setTimeout(() => console.log(count), 0) at module level

  // GOOD — read count when the callback actually runs
  function later() {
    setTimeout(() => console.log(count), 1000); // reads current value
  }
</script>

<button onclick={() => count++}>{count}</button>
<button onclick={later}>log in 1s</button>

说明:在回调里读 $state 值是在 调用那一刻 读,通常正合你意。坑在于把值一次性抓进变量 (模块加载时或一个过期闭包里) 再复用那个快照,它永不更新。尽量晚地读响应式变量,放在真正需要值时执行的函数里,而不是提前存进一个 const。

坑: 没 key 导致重排时重建 DOM

常见坑
<script lang="ts">
  let items = $state([{ id: 1, t: 'a' }, { id: 2, t: 'b' }]);
</script>

<!-- BAD — no key: inputs lose focus/value when list reorders -->
{#each items as item}
  <input value={item.t} />
{/each}

<!-- GOOD — keyed by stable id: DOM nodes move, state survives -->
{#each items as item (item.id)}
  <input value={item.t} />
{/each}

说明:没有 `(key)` 时 Svelte 按索引匹配 each 块的项,所以重排或删除会打乱哪个 DOM 节点持有哪份数据: 输入失焦、过渡断裂、组件状态挂到错误的行上。永远用 稳定唯一 id 做 key (别用数组索引,那等于白做)。key 是 Svelte 跨渲染保持身份的依据。

坑: 用 $effect 算派生值

常见坑
<script lang="ts">
  let first = $state('Li');
  let last = $state('Lei');

  // BAD — extra state, runs after render, can flash stale
  // let full = $state('');
  // $effect(() => { full = first + ' ' + last; });

  // GOOD — derived computes synchronously, no extra state
  let full = $derived(`${first} ${last}`);
</script>

<p>{full}</p>

说明:常见反模式是用 $effect 从别的状态写一个状态。改用 $derived: 它在渲染前同步计算 (不会闪过期值)、不需要额外 $state,也不会有 effect 那种读后写的循环。规则: 你若是在用其他响应式值给一个变量赋值,要的是 $derived 而不是 $effect。effect 是给图外的活 (DOM、网络、存储) 用的。

坑: legacy 与 runes 模式混用

常见坑
<script lang="ts">
  // Once you use ANY rune, the component is in runes mode.
  let count = $state(0);

  // BAD — $: is legacy reactivity, illegal in runes mode
  // $: doubled = count * 2;   // compile error

  // GOOD — use the rune equivalent
  let doubled = $derived(count * 2);
</script>

说明:组件一旦用了任何 rune 就进入 runes mode,而 runes mode 下旧的 `$:` 响应式语句和自动响应的顶层 `let` 都失效,混用是编译错误。一次性迁移整个组件: 每个 `$:` 改成 $derived 或 $effect,每个响应式 `let` 改成 $state。单个文件不能改一半。

坑: $derived 必须是纯函数

常见坑
<script lang="ts">
  let items = $state([3, 1, 2]);

  // BAD — sort() mutates in place, side effect inside derived
  // let sorted = $derived(items.sort());

  // GOOD — copy first, then sort the copy
  let sorted = $derived([...items].sort((a, b) => a - b));
</script>

<p>{sorted.join(', ')}</p>

说明:$derived 表达式必须纯: 它该算出一个值,不该改任何东西。`items.sort()` 原地排序,作为副作用改了源数组,这里是错的,也是隐蔽 bug 的来源。先拷贝 (`[...items]`、`structuredClone`) 再变换副本。`.reverse()`、`.splice()` 同样会改原值,一样要小心。

坑: 索引仅作占位,别当 key

常见坑
<script lang="ts">
  let rows = $state<string[]>(['a', 'b', 'c']);
</script>

<!-- the `, i` index is fine to DISPLAY -->
{#each rows as row, i (row)}
  <li>{i + 1}. {row}</li>
{/each}

<!-- but never key BY the index: (i) defeats identity tracking -->
<!-- {#each rows as row, i (i)}  ← bad -->

说明:第二个绑定 `, i` 给你索引用于显示 (行号、"首/末"判断),用它完全没问题。错误是 用索引做 key (写成 `(i)`),这会让 key 恰好在项移动时改变,Svelte 没法追踪身份,于是出现和没 key 一样的 DOM 重建 bug。用稳定数据做 key,用索引做显示。

坑: $state 对象 , 改字段还是重赋

常见坑
let user = $state({ name: 'Lei', tags: ['a'] });

// BOTH WORK — $state returns a deep Proxy:
user.name = 'Hong';            // tracked
user.tags.push('b');           // tracked
user = { name: 'X', tags: [] }; // tracked (reassignment)

// BUT $state.raw is shallow — only reassignment triggers:
let big = $state.raw({ items: [] });
big.items.push(1);              // NOT tracked
big = { items: [...big.items, 1] }; // tracked

说明:普通 $state 是深 Proxy,改字段 (`user.name = "…"`) 和整体重赋 (`user = {…}`) 都触发更新。$state.raw 是浅的 , 只有重赋有效。Svelte 5 里"state 没更新"的 bug 多半是不小心写成了 $state.raw,或者把基本类型抓到局部变量 (那是 copy,不是 binding)。默认就用普通 $state,只有大型形状固定的数据才考虑 .raw。

坑: runes 只能在顶层

常见坑
// WRONG — runes cannot be inside if / for / try
if (someCondition) {
  let count = $state(0);      // compile error
}
for (let i = 0; i < 3; i++) {
  let item = $state(0);        // compile error
}

// RIGHT — declare at top, gate the USE
let count = $state(0);
if (someCondition) {
  count++;                     // fine
}

// For per-item state, use an object array:
let items = $state(initial.map(x => ({ value: x, selected: false })));

说明:runes 是编译时变换,不是运行时函数调用 , 只能出现在 `<script>` 或 `.svelte.ts` 模块的顶层,不能写在 if、for、try/catch 里。解决: rune 在顶层声明,用的地方再加判断; 或者把"每项各自的 state"挪到对象数组,字段通过 proxy 自动响应。

坑: $effect 无限循环

常见坑
let count = $state(0);

// BAD — reads count, then writes count → re-runs → infinite loop
$effect(() => {
  count = count + 1;
});

// GOOD — write inside an event handler, not an effect
function inc() { count++; }

// GOOD — use untrack() to read without subscribing
import { untrack } from 'svelte';
$effect(() => {
  const c = untrack(() => count);
  console.log('component re-rendered, count was', c);
});

说明:$effect 里读一个响应式值又写它 (或它依赖的另一个响应式值),就是无限循环。两个解法: (1) 把写挪到事件 handler , effect 是把数据 同步出去 (DOM、网络、localStorage) 用的,不是用来转 state 的; (2) 真的需要当前值又不想因它重跑,就 `untrack(() => …)` 读。

坑: class 绑定冲突

常见坑
<!-- BAD — both class= and class: are present, behavior is confusing -->
<div class={dynamic} class:active>...</div>

<!-- BETTER — pick ONE style: -->
<!-- option A: pure class:* directives -->
<div
  class="base"
  class:active
  class:large={size === 'lg'}
></div>

<!-- option B: build the string yourself -->
<div class="base {active ? 'active' : ''} {size === 'lg' ? 'large' : ''}"></div>

说明:一个元素上同时用动态 `class={…}` 表达式和 `class:foo` 指令能跑,但优先级坑过不少人: 静态 `class="…"` 会合并,但 `class={dynamic}` 会 替换 静态部分。每个元素只用一种风格: Svelte 5 里全用指令最干净。一定要混着用,把 `class:*` 写在 `class={…}` 后面,这样切换能赢。

坑: SSR 和 client-only 代码

常见坑
<script lang="ts">
  import { browser } from '$app/environment';

  // BAD — window does not exist on the server
  // const w = window.innerWidth;

  // GOOD option 1: guard with browser flag
  let w = $state(0);
  $effect(() => {
    if (browser) w = window.innerWidth;
  });

  // GOOD option 2: read in onMount / $effect (only runs on client)
  $effect(() => {
    const onResize = () => w = window.innerWidth;
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  });
</script>

说明:SvelteKit 组件 服务端 (SSR 时) 和客户端都跑。`window`、`document`、`localStorage` 只在客户端有 , 在模块顶层读会让 SSR 崩。解决: 放进 $effect 里读 (只在客户端跑),或用 `import { browser } from "$app/environment"` 守护。`svelte` 的 `onMount` 也只在客户端跑,可以放心用。

坑: $ 前缀只在 .svelte 文件里有效

常见坑
// stores/count.ts
import { writable, get } from 'svelte/store';
export const count = writable(0);

// WRONG — `$count` is invalid in a plain .ts file
// export function double() { return $count * 2; }

// RIGHT — use get(), or subscribe manually
export function double() { return get(count) * 2; }

// In Counter.svelte:
<script>
  import { count } from './stores/count';
</script>
<p>{$count}</p>          <!-- works -->

说明:`$store` 自动订阅是编译器变换,只在 `.svelte` 和 `.svelte.ts` 文件里生效。普通 `.ts` / `.js` 模块里 `$` 语法不存在 , 一次性读用 `get(store)`,持续读用 `store.subscribe(cb)`。对称地,$state 写在 `.svelte` / `.svelte.ts` 之外的文件也编译不过。文件扩展名是关键。

这个工具能做什么

可搜索的 Svelte 5 速查表,60+ 条真实代码片段,覆盖十大类: runes (Svelte 5 全新响应式): $state 给变量加响应性、$derived 和 $derived.by 做带缓存的派生值、$effect 和 $effect.pre 跑带自动 依赖追踪的副作用、$props 和 $bindable 接收强类型 props、 $inspect 调试响应式、$host 在自定义元素里用; Svelte 4 → 5 迁 移: `let` → `$state`、`$:` 响应式块 → `$derived` / `$effect`、 `on:click` → `onclick`、`<slot />` → `{@render children()}`、 `export let` → `$props()`; 组件结构: `<script lang="ts">`、 scoped `<style>`、props 解构带默认值和剩余、class: 与 style: 指令; 事件: 原生 DOM 监听、键盘修饰符、回调式自定义事件 (Svelte 5 的官方姿势,替代 `createEventDispatcher`); 指令: `bind:value` 双向绑定、`bind:this` 取元素、`use:action` DOM 生命周期钩子、 `transition:fade`、`in:fly` / `out:slide` 进出动画、 `animate:flip` 列表重排动画; 流程: `{#if}`、`{#each}` 带 keyed `(item.id)`、`{#await}` 处理 promise、`{#key}` 强制重建; snippet (新的插槽体系): `{#snippet}` 定义、`{@render}` 调用、把 snippet 作为 prop 实现 headless 组件; stores (Svelte 5 里依然有用): `writable`、`readable`、`derived`、`get`,以及 `$` 前缀的自动订阅; SvelteKit: `+page.server.ts` 和 `+page.ts` 里的 `load`、form actions、page data、`$app/stores` 与 `$app/navigation`; 常见坑: 深层对象响应性 (改字段 vs 整体重赋)、class 绑定冲突、SSR 和 client-only 代码、store 自动订阅的坑、runes 只能在顶层 (不能写 在 if / for 里)。每条都有中英文 独立 撰写 (非机翻) 的说明、可 拷贝的代码,有 Svelte 4 对照的都并排展示,从 Svelte 4 过来的人 能直接看到两种写法。完全在浏览器里跑,不上传。

工具细节

输入
文本 + 数值
页面会根据工具类型展示文本框、数值控件、文件选择或结构化输入。
输出
即时结果 + 复制 + 预览
结果区优先给出可操作结果,支持项会显示复制、下载或可视化预览。
隐私
可能使用网络查询
组件源码里检测到网络调用,页面会按工具逻辑处理;敏感内容建议先脱敏。
保存 / 分享
本地保存偏好
偏好、历史或草稿保存在本机浏览器,不需要账号。
性能预算
首屏 JS ≤ 28 KB
没有声明 WASM 依赖,适合快速打开和移动端使用。
适用场景
开发运维 · 程序员
分类和职业标签用于推荐相关工具、组织内链,并帮助用户快速判断是否适合当前任务。

怎么用

  1. 1. 输入

    把内容粘贴或拖入工具面板。

  2. 2. 处理

    点击按钮,在浏览器内本地处理,文件不上传。

  3. 3. 复制 / 下载

    一键复制结果或下载到本地。

Svelte 速查表 适合怎么用

适合穿插在写代码、查问题、做 Review、上线前的小任务里。

适合开发场景

  • 格式化、校验、压缩或检查和代码相关的文本。
  • 把片段整理好再放进文档、工单、提交或交接材料。
  • 不切换工具,快速检查一个小 payload。

开发检查项

  • 压缩、混淆这类不可逆处理,先对副本操作。
  • 除非确认工具本地处理,不要粘贴密钥和敏感片段。
  • 转换后的代码上线前,仍要跑自己的测试或 lint。

下一步可以接着做

这些入口会把当前任务接到更完整的工具链里。

  1. 1 JSON 格式化与校验 浏览器内即时格式化、校验、压缩 JSON,数据不离开本地。 打开
  2. 2 React Hooks 速查表 React Hooks 速查表,17 个内置 hook (useState / useEffect / useMemo / useTransition / useFormStatus...) 含真实例子和常见坑。 打开
  3. 3 Vue 3 速查表 Vue 3 速查表,Composition API、响应式、组件、指令、Pinia,带 Options API 对照。 打开

真实使用场景

  • 真实升级里把一个 Svelte 4 组件迁到 runes

    你接手一个 200 行的组件,用的是 `export let`、`$:` 块和 `createEventDispatcher`。打开迁移那一类,每个旧写法旁边就是 runes 等价写法,照着原地改: `export let value` 变成 `let { value } = $props()`,三行 `$:` 拆成两个 `$derived` 加 一个 `$effect`,dispatcher 换成 `onChange` 回调 prop。本来要一 小时,二十分钟搞定。

  • 在 code review 里定下"改字段到底响不响应"的争论

    组里有人说 `state.items.push(x)` 不可能响应,非要整体重赋一个 新数组。你把 `$state` 的 Proxy 那条和深层响应性的坑翻出来: push 会触发更新,因为 `$state` 返回的是 Proxy。两段代码贴进 PR,审的 人看到真正的退出口是 `$state.raw`,那个多余的 spread 重写就被砍掉了。

  • 用 snippet props 搭一个 headless 的 List 组件

    你想要一个 `List`,键盘导航和选中逻辑它自己管,但每一行由父组件 渲染。搜 「snippet」,复制 `{#snippet row(item)}` 定义和 `{@render row(item)}` 调用,再接一个 snippet prop 让父组件传每项 的标记。速查给了准确的 `let { row } = $props()` 签名,你不用再靠 试错猜 snippet prop 的类型怎么写。

  • 一屏让一个 React 开发者上手 Svelte 5

    周五来了个 React 开发者,周一就得能干活。把回调 props 那条 FAQ (「React 一直是这个模式」) 和 stores 对 $state 那条发给他: store 对应 React Context,$state 对应 useState。旁边那些 Svelte 4 老写法 对他没用,他只看 runes 那一列,第一天就能交一个真组件。

常见踩坑

  • 把 rune 写进 `if` 或 `for` 块里。runes 只能在 `<script>` 或 `.svelte.ts` 模块的顶层用,`if (cond) let x = $state(0)` 编译就报错。

  • 习惯性地在新代码里掏 `createEventDispatcher`。Svelte 5 要的是回调 props,写 `let { onSave } = $props()` 然后调 `onSave(value)`,别再 dispatch 字符串 key 的事件。

  • 以为 `$state.raw(obj)` 能追踪字段改动。`.raw` 是故意退出深层追踪的,`raw.count++` 不会触发任何更新,要改嵌套字段就用普通 `$state`。

常见问题

类似工具组合

做你这行的人, 还会一起用这些。

Made by Toolora · 100% client-side · Updated 2026-06-13