svelte-lifecycle
Svelte 5 生命周期、Stores、Context、Testing 技能。当用户需要在 Svelte 5 中使用 onMount/onDestroy/tick 生命周期钩子、使用 writable/derived stores 管理状态、使用 createContext 共享组件树状态、编写 Vitest/Storybook/Playwright 测试时使用。
Svelte 5 生命周期、Stores、Context、Testing 技能。当用户需要在 Svelte 5 中使用 onMount/onDestroy/tick 生命周期钩子、使用 writable/derived stores 管理状态、使用 createContext 共享组件树状态、编写 Vitest/Storybook/Playwright 测试时使用。
本技能覆盖 Svelte 5 中"非 Runes" 但仍属于核心响应式架构的子系统:生命周期钩子(onMount / onDestroy / tick)、Stores(writable / readable / derived / 自定义)、Context API(createContext / setContext / getContext),以及测试体系(Vitest 单元与组件测试、Storybook、Playwright e2e)。也覆盖浏览器支持矩阵与例外。
onMount)或在组件销毁前清理(onDestroy)tick)import 模块(用 Stores 或 Context)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 函数中调用。
onDestroy(fn) 在组件销毁前立即执行。在 SSR 组件中也会运行——这是四个生命周期钩子中唯一在服务器端执行的。
<script>
import { onDestroy } from 'svelte';
onDestroy(() => {
console.log('destroyed');
});
</script>
常见用途:清理 setInterval、取消 fetch AbortController、解绑全局事件监听。
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。
Svelte 4 时代的"整个组件更新前后"钩子。Svelte 5 中被 shim 但在 runes 组件中不可用,应改用:
| Svelte 4 | Svelte 5 |
|---|---|
| beforeUpdate(() => {}) | $effect.pre(() => {}) |
| afterUpdate(() => {}) | $effect(() => {}) |
Runes 版只在显式读取的状态变化时触发,更精确。例如聊天窗口只在 messages 变化时滚到底,主题切换不会重置滚动位置。
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>
import { writable } from 'svelte/store';
const count = writable(0, () => {
// 第一个订阅者订阅时调用
return () => {
// 最后一个订阅者退订时调用
};
});
第二个参数是 start/stop 函数,常用于建立外部连接(WebSocket、计时器)。
不可从外部 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);
});
从一个或多个 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(store):包装为只读视图(无 set/update)get(store):同步读一次(内部建立订阅 → 读 → 退订,不建议在热路径用)Svelte 5 推荐优先使用 runes($state、.svelte.js 模块):
| 场景 | 推荐 |
|---|---|
| 提取可复用逻辑 | .svelte.js 文件 + $state |
| 跨组件共享状态 | 模块级 $state 对象 |
| 复杂异步数据流 | Stores(readable + 计时器/订阅) |
| 与 RxJS 互操作 | Stores($ 自动订阅) |
| 跨组件简单计数器 | 两者皆可 |
简单规则:能用 runes 就用 runes,stores 用于"事件流/可观察序列"。
Context 让父组件向任意深度的后代组件传值,无需 prop drilling。
// 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>
<!-- 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。
判断某个 key 是否存在于当前组件上下文层级;getAllContexts() 返回所有当前上下文的 Map。常用于库的内部实现。
将 $state 对象 set 到 context,不要重新赋值——否则破坏响应式链接:
<!-- 错误 -->
<button onclick={() => counter = { count: 0 } }>reset</button>
<!-- 正确:原地修改 -->
<button onclick={() => counter.count = 0}>reset</button>
Svelte 会发出警告。
模块级 $state 在 SSR 下会在请求间共享(数据泄漏)。Context 是请求隔离的,因此涉及用户特定数据时优先用 Context。
// 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() 强制同步触发。
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();
});
通过 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();
}} />
// 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 启动预览服务器。
基线目标 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 | — |
async;改用同步函数并内部 await。$store 前缀读取;不要解构(const { subscribe } = store 不会自动订阅)。derived([a,b], ([$a,$b]) => ...)。$state 全局共享;改用 setContext。document is not defined → test.environment: 'jsdom';或在文件顶部加 // @vitest-environment jsdom。flushSync()。webServer.command 是否正确启动;提高 timeout。if 块或函数内。$ 前缀——会被识别为 store。tick() 等待 DOM。window / document 必须放 onMount 或 onDestroy(onDestroy 在 SSR 运行)。$state 对象或函数 getter。$state.snapshot 浏览器要求 98+ / Safari 15.4+——兼容性发布时检查。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/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/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 指南 |