mcpskills.net
SkillsMCPsAgentsPrompts
mcpskills.net — A curated directory of AI agent Skills and MCP servers
TermsPrivacy
← Back to Skills
Frontend

svelte-lifecycle

Svelte 5 生命周期、Stores、Context、Testing 技能。当用户需要在 Svelte 5 中使用 onMount/onDestroy/tick 生命周期钩子、使用 writable/derived stores 管理状态、使用 createContext 共享组件树状态、编写 Vitest/Storybook/Playwright 测试时使用。

by full-stack-skillsRepository →Source →

Svelte Lifecycle, Stores, Context, Testing (Svelte 5)

本技能覆盖 Svelte 5 中"非 Runes" 但仍属于核心响应式架构的子系统:生命周期钩子(onMount / onDestroy / tick)、Stores(writable / readable / derived / 自定义)、Context API(createContext / setContext / getContext),以及测试体系(Vitest 单元与组件测试、Storybook、Playwright e2e)。也覆盖浏览器支持矩阵与例外。

When to use this skill

  • 需要在组件挂载后访问 DOM(onMount)或在组件销毁前清理(onDestroy)
  • 需要在状态变更后等待 DOM 更新(tick)
  • 需要跨组件共享反应式状态,但不想用 import 模块(用 Stores 或 Context)
  • 需要写跨组件异步流(Readable/derived store)
  • 需要用 Context 替代 prop drilling,且涉及 SSR 时不污染全局
  • 需要为 Svelte 组件写 Vitest 单元/组件测试、Storybook stories 或 Playwright e2e
  • 需要查浏览器最低版本与例外特性

Critical: onMount

onMount(fn) 在组件挂载到 DOM 后立即运行。仅在浏览器执行——SSR 时不调用。

基础用法

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    console.log('mounted');
  });
</script>

返回清理函数

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    const interval = setInterval(() => console.log('beep'), 1000);
    return () => clearInterval(interval);
  });
</script>

关键约束:onMount 必须接收同步函数才能正确返回 cleanup。async () => {} 总是返回 Promise,cleanup 不会在卸载时调用。

从外部模块调用

onMount 不必写在组件脚本顶层——它必须只在组件初始化时调用。允许从同一模块内的 helper 函数中调用。

Critical: onDestroy

onDestroy(fn) 在组件销毁前立即执行。在 SSR 组件中也会运行——这是四个生命周期钩子中唯一在服务器端执行的。

<script>
  import { onDestroy } from 'svelte';

  onDestroy(() => {
    console.log('destroyed');
  });
</script>

常见用途:清理 setInterval、取消 fetch AbortController、解绑全局事件监听。

Critical: tick

tick() 返回 Promise,在所有 pending state 变更应用到 DOM 后 resolve;若无 pending 则在下一个 microtask resolve。

<script>
  import { tick } from 'svelte';

  async function handle() {
    count = count + 1;
    await tick();
    // 此时 DOM 已更新
    element.scrollIntoView();
  }
</script>

常见场景:focus、scroll、measure DOM、在 state 变更后调用第三方命令式 API。

Deprecated: beforeUpdate / afterUpdate

Svelte 4 时代的"整个组件更新前后"钩子。Svelte 5 中被 shim 但在 runes 组件中不可用,应改用:

| Svelte 4 | Svelte 5 | |---|---| | beforeUpdate(() => {}) | $effect.pre(() => {}) | | afterUpdate(() => {}) | $effect(() => {}) |

Runes 版只在显式读取的状态变化时触发,更精确。例如聊天窗口只在 messages 变化时滚到底,主题切换不会重置滚动位置。

Critical: Stores (writable, readable, derived)

Stores 是满足"store 契约"的对象:必须有 subscribe(fn) → unsubscribe;可写 store 还需有 set(value)。

<script>
  import { writable } from 'svelte/store';

  const count = writable(0);
  $count;        // 自动订阅,读取当前值
  count.set(1);  // 写入
  $count = 2;    // 语法糖:等价于 count.set(2)
</script>

writable

import { writable } from 'svelte/store';

const count = writable(0, () => {
  // 第一个订阅者订阅时调用
  return () => {
    // 最后一个订阅者退订时调用
  };
});

第二个参数是 start/stop 函数,常用于建立外部连接(WebSocket、计时器)。

readable

不可从外部 set 的 store;初始值 + start 函数:

import { readable } from 'svelte/store';

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

derived

从一个或多个 store 派生:

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

const a = writable(1);
const b = writable(2);
const sum = derived([a, b], ([$a, $b]) => $a + $b);

异步版本:接受 (values, set, update),允许在异步回调里调用 set/update;可返回清理函数;可传第三个参数作为初始值。

readonly / get

  • readonly(store):包装为只读视图(无 set/update)
  • get(store):同步读一次(内部建立订阅 → 读 → 退订,不建议在热路径用)

When to use stores vs $state

Svelte 5 推荐优先使用 runes($state、.svelte.js 模块):

| 场景 | 推荐 | |---|---| | 提取可复用逻辑 | .svelte.js 文件 + $state | | 跨组件共享状态 | 模块级 $state 对象 | | 复杂异步数据流 | Stores(readable + 计时器/订阅) | | 与 RxJS 互操作 | Stores($ 自动订阅) | | 跨组件简单计数器 | 两者皆可 |

简单规则:能用 runes 就用 runes,stores 用于"事件流/可观察序列"。

Critical: Context API

Context 让父组件向任意深度的后代组件传值,无需 prop drilling。

createContext(推荐,Svelte 5.40+)

// context.ts
import { createContext } from 'svelte';
interface User { name: string; }
export const [getUser, setUser] = createContext<User>();
<!-- Parent.svelte -->
<script>
  import { setUser } from './context';
  setUser({ name: 'world' });
</script>
<!-- Child.svelte -->
<script>
  import { getUser } from './context';
  const user = getUser();
</script>

<h1>hello {user.name}</h1>

setContext / getContext(备选)

<!-- Parent -->
<script>
  import { setContext } from 'svelte';
  setContext('my-key', value);
</script>

<!-- Child -->
<script>
  import { getContext } from 'svelte';
  const value = getContext('my-key');
</script>

键和值可以是任意 JS 值。createContext 提供类型安全与无需 key。

hasContext / getAllContexts

判断某个 key 是否存在于当前组件上下文层级;getAllContexts() 返回所有当前上下文的 Map。常用于库的内部实现。

与 state 组合

将 $state 对象 set 到 context,不要重新赋值——否则破坏响应式链接:

<!-- 错误 -->
<button onclick={() => counter = { count: 0 } }>reset</button>

<!-- 正确:原地修改 -->
<button onclick={() => counter.count = 0}>reset</button>

Svelte 会发出警告。

替代全局 state(SSR 关键)

模块级 $state 在 SSR 下会在请求间共享(数据泄漏)。Context 是请求隔离的,因此涉及用户特定数据时优先用 Context。

Critical: Testing

Vitest 单元测试

// counter.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';

test('Counter', () => {
  let count = $state(0);
  count = 1;
  flushSync();
  expect(count).toBe(1);
});

组件测试

vite.config.js:

import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: { environment: 'jsdom' },
  resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined
});
import { mount, unmount, flushSync } from 'svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';

test('Component', () => {
  const c = mount(Component, { target: document.body, props: { n: 0 } });
  expect(document.body.innerHTML).toBe('<button>0</button>');
  document.body.querySelector('button').click();
  flushSync();
  expect(document.body.innerHTML).toBe('<button>1</button>');
  unmount(c);
});

$effect 在 mount 时不会自动运行——测试中用 flushSync() 强制同步触发。

使用 $effect 的测试

import { flushSync } from 'svelte';
import { expect, test } from 'vitest';

test('Effect', () => {
  const cleanup = $effect.root(() => {
    let count = $state(0);
    let log = [];
    $effect(() => log.push(count));
    flushSync();
    expect(log).toEqual([0]);
    count = 1;
    flushSync();
    expect(log).toEqual([0, 1]);
  });
  cleanup();
});

Storybook

通过 npx sv add storybook 配置;用 play 函数模拟用户交互并断言:

<Story name="Filled" play={async ({ canvas, userEvent }) => {
  await userEvent.type(canvas.getByTestId('email'), '[email protected]');
  await userEvent.click(canvas.getByRole('button'));
  await expect(canvas.getByText("You're in!")).toBeInTheDocument();
}} />

Playwright e2e

// tests/home.spec.js
import { expect, test } from '@playwright/test';
test('h1 visible', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

playwright.config.js 中配置 webServer 启动预览服务器。

Critical: Browser support

基线目标 2020 (Baseline 2020):

| 浏览器 | 最低版本 | |---|---| | Chrome/Edge | 87 | | Firefox | 83 | | Safari | 14 | | Opera | 73 | | Opera (Android) | 62 | | Samsung Internet | 14.0 | | Android WebView | 87 | | Internet Explorer | 不支持 |

例外特性(需更高版本)

| 特性 | Chrome/Edge | Firefox | Safari | |---|---|---|---| | $state.snapshot | 98 | 94 | 15.4 | | bind:devicePixelContentBoxSize | — | 93 | 不支持 | | flip from svelte/animate | — | 126 | — |

Quick Fixes

  • onMount 清理不执行 → 函数是 async;改用同步函数并内部 await。
  • store 引用 UI 不更新 → 必须用 $store 前缀读取;不要解构(const { subscribe } = store 不会自动订阅)。
  • derived 不触发 → 回调里未读取任何 store 值;或数组写法 derived([a,b], ([$a,$b]) => ...)。
  • Context 在 SSR 间泄漏 → 模块级 $state 全局共享;改用 setContext。
  • Vitest 中 document is not defined → test.environment: 'jsdom';或在文件顶部加 // @vitest-environment jsdom。
  • 组件 mount 后状态未生效 → 在 mount 后调用 flushSync()。
  • Playwright 找不到元素 → 检查 webServer.command 是否正确启动;提高 timeout。

Gotchas

  • store 必须在组件顶层声明:不能在 if 块或函数内。
  • 不要给非 store 变量加 $ 前缀——会被识别为 store。
  • onMount 是唯一的"挂载后",没有"渲染后";用 tick() 等待 DOM。
  • onMount 在 SSR 不运行——访问 window / document 必须放 onMount 或 onDestroy(onDestroy 在 SSR 运行)。
  • setContext/getContext 必须同步调用,在 setup 阶段(不能在事件处理器内)。
  • Context 值若为原始值(数字/字符串),变更不触发更新——用 $state 对象或函数 getter。
  • $state.snapshot 浏览器要求 98+ / Safari 15.4+——兼容性发布时检查。
  • 绑定 onMount 的 cleanup 顺序:onMount 卸载时先调 cleanup,再调 onDestroy。

FAQ

Q: Svelte 5 还需要 stores 吗? A: 简单状态用 $state + .svelte.js 模块;stores 在复杂异步流、计时器、外部订阅、RxJS 互操作时仍有价值。

Q: onMount vs $effect 怎么选? A: 需要访问 DOM 节点、启动副作用(fetch、定时器、绑定 window 事件)用 onMount;纯响应式派生/同步副作用用 $effect。

Q: 为什么 onMount 的 async 函数清理不执行? A: async () => {} 总是返回 Promise;onMount 收到非函数就跳过清理。改用同步函数包异步逻辑:

onMount(() => {
  let cancelled = false;
  (async () => {
    const data = await fetch(...);
    if (!cancelled) state = data;
  })();
  return () => { cancelled = true; };
});

Q: Context 替代 props 的时机? A: 当一个值要穿透 3+ 层中间组件、或父组件不直接知道子组件(如 {@render children()})时。

Q: Vitest 单元测试和组件测试区别? A: 单元测试纯逻辑(runes 在 .svelte.js 文件中),无 DOM;组件测试用 jsdom 渲染完整 Svelte 组件。

Q: IE 11 还能用 Svelte 5 吗? A: 不支持。最低 Chrome 87 / Firefox 83 / Safari 14。

Q: SSR 项目用 $state.snapshot 安全吗? A: 服务端是 Node,无浏览器版本要求;仅客户端使用时检查例外表。

Q: Playwright 跑测试前要 build 吗? A: playwright.config.js 中配 webServer: { command: 'npm run build && npm run preview', port: 4173 },Playwright 自动起 preview 服务器。

Examples

| 文件 | 内容 | |---|---| | examples/lifecycle-hooks.md | onMount 清理、onDestroy、tick DOM 测量、deprecated 钩子 | | examples/stores-advanced.md | writable/readable/derived/自定义 store、异步流 | | examples/store-patterns.md | stores vs $state、持久化、跨组件模式 | | examples/context-advanced.md | createContext、setContext、与 state 组合、SSR 安全 | | examples/testing-vitest.md | 单元/组件测试、$effect.root、flushSync、context wrapper | | examples/testing-playwright.md | e2e 测试、Playwright config |

References

| 文件 | 内容 | |---|---| | references/lifecycle-hooks-reference.md | onMount/onDestroy/tick 完整签名、SSR 注意事项 | | references/stores-api-reference.md | 全部 store API、TypeScript 类型、契约 | | references/context-api-reference.md | createContext/setContext/getContext/hasContext、SSR 行为 | | references/testing-reference.md | Vitest/Storybook/Playwright 完整 setup | | references/browser-support.md | 浏览器矩阵、例外特性、polyfill 指南 |