mcpField
S
svelte-runtime

svelte-runtime

github.com/full-stack-skills开发489 次安装4 周前更新
代码仓库

关于 svelte-runtime

面向使用 Svelte 5 的开发者,汇总运行时 API 与实用模式,涵盖状态、生命周期、SSR、水合及 Vitest 测试。

主要功能

  1. 01Store、Context 与 $state 状态管理
  2. 02生命周期与命令式组件 API
  3. 03SSR、水合及 CSP 配置
  4. 04Vitest 组件测试与同步刷新
  5. 05Svelte 5 运行时参考示例

适用场景

  1. 01在组件间共享响应式状态
  2. 02使用 mount、render 或 hydrate 管理组件
  3. 03处理 SSR 数据复用与 CSP
  4. 04编写并调试组件测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/full-stack-skills/svelte-skills --skill svelte-runtime

安装后在 Claude Code 中输入/svelte-runtime确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
csp-examples.md
mount-hydrate.md
onmount-tick.md
README.md
ssr-patterns.md
references/
csp-reference.md
lifecycle-runtime-reference.md
mount-unmount-reference.md
README.md
ssr-reference.md

详细介绍

Svelte Runtime Reference (Svelte 5)

本技能覆盖 Svelte 5 的运行时 API,包括跨组件状态(Stores/Context)、生命周期、命令式 API、SSR 和测试。

When to use this skill

当用户需要跨组件共享状态、处理组件生命周期、使用 mount/render 等命令式 API、或编写 Vitest 组件测试时使用本技能。

Critical: Stores (svelte/store)

Store 是一种跨组件共享响应式状态的老方式(Svelte 5 推荐用 .svelte.js 中的 $state 对象替代)。

writable
import { writable } from 'svelte/store';
const count = writable(0);

count.subscribe(v => console.log(v)); // 订阅
count.set(1);                         // 设置值
count.update(n => n + 1);            // 更新
readable(不可从外部设置)
import { readable } from 'svelte/store';
const time = readable(new Date(), (set) => {
  const interval = setInterval(() => set(new Date()), 1000);
  return () => clearInterval(interval);
});
derived
import { derived } from 'svelte/store';
const doubled = derived(count, $c => $c * 2);
const combined = derived([a, b], ([$a, $b]) => $a + $b);
readonly
import { readonly, writable } from 'svelte/store';
const readOnlyStore = readonly(writableStore);
在组件中使用
<script>
  import { count } from './store.js';
  console.log($count); // $ 前缀订阅
  $count = 2;          // 自动调用 .set()
</script>

Store 必须在组件顶层声明;不要在变量前加 $ 前缀(那是 $count,不是 count)。

Svelte 5 推荐替代方案:

// state.svelte.js
export const userState = $state({ name: '', count: 0 });
// 其他组件直接 import

Critical: Context (svelte)

跨组件层级传递数据,无需 props 层层透传(prop-drilling):

setContext / getContext
// Parent.svelte
<script>
  import { setContext } from 'svelte';
  setContext('key', 'hello');
</script>

// Child.svelte
<script>
  import { getContext } from 'svelte';
  const msg = getContext('key');
</script>
createContext(推荐,类型安全)
// context.ts
import { createContext } from 'svelte';
export const [getUserContext, setUserContext] = createContext<User>();
// Parent.svelte
<script> setUserContext({ name: 'Ada' }); </script>

// Child.svelte
<script> const user = getUserContext(); </script>
响应式 Context

将 $state 对象存入 Context:

setContext('counter', counterState);
// ❌ 不要重新赋值整个对象
// counterState = { count: 0 }
// ✅ 直接修改属性
counterState.count = 0;

Critical: Lifecycle Hooks

Mental model: Svelte 5 的生命周期只有两个阶段 —— 创建 与 销毁。中间的"状态更新"由 effect 处理,因为响应式的最小单位是 effect,不是组件。所以没有 beforeUpdate/afterUpdate 钩子。

onMount

组件挂载到 DOM 后执行(SSR 时不运行):

<script>
  import { onMount } from 'svelte';
  onMount(() => {
    console.log('mounted');
    return () => console.log('unmounted'); // 返回清理函数
  });
</script>

返回的函数在组件卸载时调用;如果传入 async 函数则清理函数不会被调用(async 函数总是返回 Promise)。需要异步任务 + 清理时,用 IIFE 包裹,外部箭头函数同步返回清理函数。

onDestroy

组件卸载前执行(SSR 时也运行):

<script>
  import { onDestroy } from 'svelte';
  onDestroy(() => cleanup());
</script>
tick

等待 DOM 更新完成后再继续:

<script>
  import { tick } from 'svelte';
  $effect.pre(() => {
    // DOM 更新前
    tick().then(() => {
      // DOM 已更新完成
    });
  });
</script>
生命周期与 mount/unmount/hydrate 的配合
阶段mount()hydrate()render() (SSR)unmount()
同步:组件 script、state、模板✓✓✓—
同步:插入 DOM / 输出字符串✓ 替换 target✓ 复用 SSR DOM✓ 返回 head+body—
微任务:$effect / onMount 触发✓✓✗—
同步:清理 effect + onDestroy——✓(render 完成后)✓
异步:过渡 (outro)———outro:true 时等待

mount() 返回时 effect 和 onMount 都还没跑;要立刻让它们跑就 flushSync()。hydrate() 同理。 tick() 和 flushSync() 在服务端是 no-op(没有响应式调度)。

Critical: Imperative Component API

mount(客户端挂载)
import { mount } from 'svelte';
import App from './App.svelte';

const app = mount(App, {
  target: document.querySelector('#app'),
  props: { name: 'world' }
});
unmount(卸载)
import { mount, unmount } from 'svelte';
unmount(app, { outro: true }); // 播放过渡后卸载
render(服务端渲染)
import { render } from 'svelte/server';
const { head, body } = await render(App, { props: { data } });
hydrate(客户端水合)

复用 SSR HTML 并激活为交互式:

import { hydrate } from 'svelte';
const app = hydrate(App, { target: document.querySelector('#app') });

Critical: Hydratable Data

SSR 时序列化数据,客户端水合时复用(避免重复请求):

<script>
  import { hydratable } from 'svelte';
  const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
CSP 支持

hydratable() 会在 render() 返回的 head 中嵌入一段 inline <script>。CSP 默认会拦截它,需要用 nonce 或 hash 放行:

// 动态 SSR:每请求一个 nonce
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
  csp: { nonce }
});
// 响应头同步加上:
// Content-Security-Policy: script-src 'self' 'nonce-${nonce}'
// 静态 SSG:使用 hash
const { head, body, hashes } = await render(App, {
  csp: { hash: true }
});
// Content-Security-Policy: script-src ${hashes.script.map(h => `'${h}'`).join(' ')}

重要:

  • 没用 hydratable() 时 render() 不输出 inline <script>,CSP 严格模式不会拦截任何东西。
  • inline 事件处理器(<button onclick={...}>)不是 <script> 标签,不需要 nonce。
  • 推荐 nonce 模式,hash 模式将来会和 streaming SSR 冲突。
  • 详见 references/csp-reference.md。

Critical: Best Practices

$state
// ✅ 只对需要响应式的变量用 $state
let count = $state(0);

// ✅ 大数组/不可变数据用 $state.raw
let apiData = $state.raw([]);
apiData = await fetchData(); // 重新赋值触发更新
$derived
// ✅ 派生值用 $derived
let doubled = $derived(count * 2);

// ❌ 不要用 $effect 同步派生
let square;
$effect(() => { square = num * num; }); // ❌
let square = $derived(num * num);         // ✅
$effect
// ✅ 仅用于副作用
$effect(() => { myChart.resize(); });

// ❌ 不要同步状态
$effect(() => { left = total - spent; }); // ❌
Props
// ✅ 依赖 props 的值用 $derived
let { type } = $props();
let color = $derived(type === 'danger' ? 'red' : 'green');
事件

<button onclick={handleClick}>click</button>


<button on:click={handleClick}>click</button>


<svelte:window onkeydown={handleKey} />
Snippets

{#snippet footer()}
  <p>footer</p>
{/snippet}
Each blocks

{#each items as item (item.id)}
  <li>{item.name}</li>
{/each}
Context
// ✅ 用 createContext 提供类型安全
import { createContext } from 'svelte';
const [getCtx, setCtx] = createContext<MyCtx>();
Avoid Legacy Features
// ✅ Runes mode 全是新项目的默认
let count = $state(0);     // ✅
$: count++;                  // ❌ legacy
export let name;            // ❌
let { name } = $props();   // ✅
on:click={fn}              // ❌
onclick={fn}               // ✅
<slot />                   // ❌
{@render children()}        // ✅
<svelte:component this={x} /> // ❌
<svelte:element this={x} />   // ✅

Critical: Testing (Vitest)

安装配置
npm install -D vitest
// vite.config.js
export default defineConfig({
  resolve: process.env.VITEST
    ? { conditions: ['browser'] }
    : undefined
});
单元测试
// multiplier.svelte.js
export function multiplier(initial, k) {
  let count = $state(initial);
  return {
    get value() { return count * k; },
    set: (c) => { count = c; }
  };
}

// test
import { multiplier } from './multiplier.svelte.js';
test('multiplier', () => {
  let double = multiplier(0, 2);
  expect(double.value).toEqual(0);
  double.set(5);
  expect(double.value).toEqual(10);
});
组件测试
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';

test('MyComponent', () => {
  const comp = mount(MyComponent, { target: document.body, props: { name: 'Ada' } });
  expect(document.body.innerHTML).toContain('Ada');
  unmount(comp);
});
flushSync

强制同步执行所有待处理效果(测试中常用):

import { flushSync } from 'svelte';
flushSync();

Quick Fixes

问题解决方案
状态在组件间不同步用 Context(跨层级)或 .svelte.js(全局)
onMount 清理不执行确保传入同步函数(不是 async),需要异步时用 IIFE 包裹
SSR 后组件不交互用 hydrate 而非 mount
测试中状态不更新用 flushSync() 强制同步执行
Context 断开链接不要重新赋值整个 Context 对象
mount 后 effect 没运行mount() 同步返回,但 effect 在下一个 microtask 才跑;需要立刻跑就 flushSync()
CSP 阻止 hydratable 脚本用 csp: { nonce }(动态)或 csp: { hash: true }(静态),并在响应头里同步声明
异步 onMount 拿不到清理外部箭头同步 return () => controller.abort(),async 逻辑放 IIFE 里面

Gotchas

  1. Store $ 前缀是订阅语法 — $store 在模板和 script 中都是订阅,非 $ 前缀的变量是普通值
  2. Context 链接断开 — 重新赋值整个对象会断开响应式链接,改用 .count = 0 而非 obj = {count:0}
  3. mount 不运行 $effect — effects 在组件挂载后由 microtask 触发
  4. 测试中 $effect.root — 涉及 effects 的测试需要 $effect.root() 包裹
  5. render 只在 SSR 环境可用 — 客户端用 mount
  6. async onMount 的清理会被忽略 — async 函数永远返回 Promise,Svelte 拿不到清理函数;用 IIFE 模式
  7. hydrate() 不会同步运行 effect — 跟 mount() 一样,hydration 后要用 flushSync() 让 effect 跑起来
  8. CSP nonce 是一次性的 — 每请求一个新 nonce;hash 模式与未来的 streaming SSR 冲突
  9. hydration 随机值会不匹配 — Math.random() / Date.now() 在 SSR 和 CSR 不同;用 hydratable() 包裹

常见问题

**Q:什么时候用 Store,什么时候用 Context 或 .svelte.js? A:Store:跨整个应用共享的简单值;Context:在组件树局部共享;.svelte.js $state:推荐的新方式,可替代 Store。

Q:tick 和 await Promise.resolve() 有什么区别? A:tick() 保证在 DOM 更新后解析,Promise.resolve() 不会等待 Svelte 的更新周期。

Q:tick() 和 flushSync() 怎么选? A:await tick() 适用于异步用户代码;flushSync() 适用于测试,以及外部库需要立即同步 DOM 的同步场景。

Q:如何测试带有 $effect 的代码? A:使用 flushSync() 强制执行,并用 $effect.root() 包裹测试代码。

Q:SSR 后 hydration 报错怎么办? A:确保 SSR 和 CSR 的 HTML 结构一致;使用 hydrate 而非 mount。

Q:什么时候用 onMount,什么时候用 $effect? A:onMount 是“挂载后运行一次”的副作用;$effect 是“依赖变化时重新运行”的反应式副作用。

Q:hydrate 的内联脚本被 CSP 阻止了怎么办? A:使用 csp: { nonce } 在 render 时为脚本添加 nonce,并在响应头中同步声明;静态站点使用 csp: { hash: true } 获取 hashes.script。

示例

演示 Svelte 5 运行时 API 的实用示例。

文件说明
examples/README.md入门页面,附快速参考
examples/mount-hydrate.md9 个示例:mount、unmount、hydrate、render、flushSync、tick
examples/ssr-patterns.mdSSR 模式:异步 render、流式传输、hydration、CSP
examples/onmount-tick.md15 个生命周期示例:onMount(同步/异步/清理)、onDestroy、用于聚焦/测量/滚动的 tick、$effect.pre 模式
examples/csp-examples.md8 个 CSP 示例:nonce(Express/SvelteKit/Cloudflare)、hash(SSG)、诊断提示

参考资料

Svelte 5 运行时 API 的深入技术参考。

文件说明
references/README.md入门页面,附快速参考
references/mount-unmount-reference.md深入讲解:mount/unmount/hydrate 生命周期、flushSync、tick
references/ssr-reference.md深入讲解 SSR:render、异步 SSR、CSP、hydration
references/lifecycle-runtime-reference.mdmount/hydrate 运行时中的生命周期:effect 运行时机、onMount 时序、tick 与 flushSync 的区别、SSR 阶段矩阵
references/csp-reference.mdCSP nonce 与 hash、运行时配置、流式传输兼容性

评论

支持 Markdown