Svelte 5 完全指南¶
为什么要学 Svelte 5¶
编译时响应式,运行时开销接近零:Svelte 在构建时将组件编译为高效的原生 JavaScript,不需要虚拟 DOM diff。生成的代码体积小、执行快,在真实性能基准测试中经常超越 React 和 Vue。
Runes 系统彻底简化响应式:Svelte 5 引入的 Runes(
$state,$derived,$effect)用显式的声明替代了 Svelte 4 隐式的$:语法,逻辑更清晰,可调试性更强,也更容易在.svelte.ts文件中复用。更少的代码量:实现同样的功能,Svelte 代码量通常比 React 少 30-40%。没有
useState/useEffect模板代码,没有useMemo/useCallback优化焦虑。SvelteKit 提供全栈开发体验:SvelteKit 是 Svelte 官方元框架,提供路由、SSR、SSG、API 路由、表单 Actions 等开箱即用功能,类似 Next.js 对 React 的关系。
社区快速增长,企业采用加速:Apple、Spotify、The New York Times 等公司已在生产中使用 Svelte。StackOverflow 调查中 Svelte 连续多年获得"最受喜爱框架"称号。
核心概念详解¶
Svelte 是什么(白话解释)¶
React 和 Vue 的工作方式是:在浏览器运行时维护一个虚拟 DOM,每次数据变化就比较新旧虚拟 DOM 的差异,再去更新真实 DOM。Svelte 的做法完全不同:它在你编译代码的时候就把"检测变化并更新 DOM"的代码直接编译好了。运行时不需要框架代码,不需要 diff 算法。
打个比方:React 像是请了一个翻译在你和外国人之间实时传话(虚拟 DOM),Svelte 像是你直接学会了对方的语言(编译生成原生更新代码)。
Svelte 5 Runes 系统¶
Runes 是 Svelte 5 最重要的变化。Rune 这个词来自古代北欧文字,在这里表示一种"魔法标记"。
$state — 响应式状态¶
<script>
// Svelte 5: 显式声明响应式状态
let count = $state(0);
let user = $state({ name: '张三', age: 25 });
let items = $state([1, 2, 3]);
// 深层响应式:修改对象属性也会触发更新
function updateName() {
user.name = '李四'; // 自动触发更新
}
// 数组方法也是响应式的
function addItem() {
items.push(items.length + 1); // 自动触发更新
}
</script>
<button onclick={() => count++}>
点击次数: {count}
</button>
<p>{user.name}, {user.age}岁</p>
<p>项目: {items.join(', ')}</p>
对比 Svelte 4:
<script>
// Svelte 4: 隐式响应式(let 声明自动响应式)
let count = 0;
// 但对象和数组需要重新赋值才能触发更新
let user = { name: '张三', age: 25 };
user = { ...user, name: '李四' }; // 必须重新赋值
</script>
$derived — 派生状态¶
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
// 复杂派生
let items = $state([1, 2, 3, 4, 5]);
let evenItems = $derived(items.filter(i => i % 2 === 0));
let total = $derived(items.reduce((a, b) => a + b, 0));
// 需要多行逻辑时用 $derived.by
let summary = $derived.by(() => {
const sum = items.reduce((a, b) => a + b, 0);
const avg = sum / items.length;
return `总和: ${sum}, 平均: ${avg.toFixed(1)}`;
});
</script>
<p>计数: {count}, 双倍: {doubled}, 四倍: {quadrupled}</p>
<p>偶数项: {evenItems.join(', ')}</p>
<p>{summary}</p>
$effect — 副作用¶
<script>
let count = $state(0);
let searchTerm = $state('');
// 基本 effect: 当依赖变化时自动运行
$effect(() => {
console.log(`count 变为: ${count}`);
// 返回清理函数(可选)
return () => {
console.log(`清理: count 曾为 ${count}`);
};
});
// 搜索防抖示例
$effect(() => {
const term = searchTerm; // 追踪这个依赖
const timer = setTimeout(() => {
if (term) {
console.log(`搜索: ${term}`);
}
}, 300);
return () => clearTimeout(timer);
});
// $effect.pre: 在 DOM 更新前运行
$effect.pre(() => {
console.log('DOM 即将更新');
});
</script>
<input bind:value={searchTerm} placeholder="搜索..." />
$props — 组件属性¶
<!-- Child.svelte -->
<script>
// Svelte 5: 使用 $props() 解构
let { name, age = 18, onUpdate, children } = $props();
// 类型安全(TypeScript)
// interface Props {
// name: string;
// age?: number;
// onUpdate: (name: string) => void;
// }
// let { name, age = 18, onUpdate }: Props = $props();
</script>
<div>
<p>{name}, {age}岁</p>
<button onclick={() => onUpdate(name)}>更新</button>
{@render children()}
</div>
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
</script>
<Child name="张三" age={25} onUpdate={(n) => console.log(n)}>
{#snippet children()}
<p>这是子内容</p>
{/snippet}
</Child>
$bindable — 双向绑定属性¶
<!-- TextInput.svelte -->
<script>
let { value = $bindable('') } = $props();
</script>
<input bind:value />
<!-- 使用 -->
<script>
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>输入的是: {name}</p>
Svelte 5 vs React vs Vue 3 对比¶
| 特性 | Svelte 5 | React 19 | Vue 3 |
|---|---|---|---|
| 响应式机制 | 编译时(Runes) | 运行时(Hooks) | 运行时(Proxy) |
| 虚拟DOM | 无 | 有 | 有 |
| 包体积 | 极小(按需编译) | ~45KB | ~33KB |
| 状态声明 | $state(value) | useState(value) | ref(value) |
| 派生计算 | $derived(expr) | useMemo(fn, deps) | computed(fn) |
| 副作用 | $effect(fn) | useEffect(fn, deps) | watchEffect(fn) |
| 依赖追踪 | 自动 | 手动(deps数组) | 自动 |
| 过时闭包问题 | 无 | 常见 | 无 |
| 模板语法 | 增强HTML | JSX | 模板/JSX |
| 双向绑定 | bind:value | 手动onChange | v-model |
| 动画支持 | 内置(transition) | 需第三方库 | 内置(Transition) |
| 全栈框架 | SvelteKit | Next.js | Nuxt |
| TypeScript | 原生支持 | 原生支持 | 原生支持 |
| 学习曲线 | 低 | 中高 | 中 |
| 生态规模 | 中等 | 极大 | 很大 |
Snippets(替代 Slots)¶
Svelte 5 用 Snippets 替代了 Slots:
<script>
let items = $state(['苹果', '香蕉', '橙子']);
</script>
<!-- 定义 snippet -->
{#snippet listItem(item, index)}
<li class="item">
<span class="index">{index + 1}.</span>
<span class="name">{item}</span>
</li>
{/snippet}
<ul>
{#each items as item, i}
{@render listItem(item, i)}
{/each}
</ul>
可以把 snippet 作为 prop 传给子组件:
<!-- List.svelte -->
<script>
let { items, renderItem } = $props();
</script>
<ul>
{#each items as item}
{@render renderItem(item)}
{/each}
</ul>
<!-- 使用 -->
<List {items}>
{#snippet renderItem(item)}
<li>{item.name}: {item.price}元</li>
{/snippet}
</List>
安装与配置¶
新建 SvelteKit 项目¶
交互式选项: - Template: SvelteKit minimal / SvelteKit demo / Svelte library - TypeScript: Yes - Add-ons: prettier, eslint, vitest, playwright, tailwindcss, drizzle, lucia, paraglide...
项目结构¶
my-app/
├── src/
│ ├── lib/ # 共享代码 ($lib 别名)
│ │ ├── components/ # 组件
│ │ ├── server/ # 仅服务端代码 ($lib/server)
│ │ └── utils.ts
│ ├── routes/ # 文件系统路由
│ │ ├── +layout.svelte
│ │ ├── +page.svelte
│ │ ├── +page.server.ts
│ │ ├── about/
│ │ │ └── +page.svelte
│ │ └── api/
│ │ └── users/
│ │ └── +server.ts
│ ├── app.html # HTML 模板
│ └── app.css # 全局样式
├── static/ # 静态资源
├── svelte.config.js
├── vite.config.ts
└── package.json
svelte.config.js¶
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
'@components': 'src/lib/components',
'@utils': 'src/lib/utils'
}
}
};
export default config;
TypeScript 配置¶
<script lang="ts">
interface User {
id: number;
name: string;
email: string;
}
let users: User[] = $state([]);
</script>
快速上手:5 分钟最小示例¶
src/routes/+page.svelte:
<script lang="ts">
let count = $state(0);
let history = $state<number[]>([]);
let lastAction = $derived(
history.length > 0 ? history[history.length - 1] : null
);
function increment() {
count++;
history.push(count);
}
function decrement() {
count--;
history.push(count);
}
function reset() {
count = 0;
history = [];
}
</script>
<main>
<h1>Svelte 5 计数器</h1>
<p class="count">{count}</p>
<div class="buttons">
<button onclick={decrement}>-1</button>
<button onclick={reset}>重置</button>
<button onclick={increment}>+1</button>
</div>
{#if lastAction !== null}
<p class="history">上次值: {lastAction}</p>
{/if}
{#if history.length > 0}
<p class="history">历史: {history.join(' → ')}</p>
{/if}
</main>
<style>
main { text-align: center; padding: 2rem; font-family: system-ui; }
.count { font-size: 4rem; font-weight: bold; margin: 1rem; }
.buttons { display: flex; gap: 1rem; justify-content: center; }
button { font-size: 1.5rem; padding: 0.5rem 1.5rem; cursor: pointer;
border: 2px solid #333; border-radius: 8px; background: white; }
button:hover { background: #f0f0f0; }
.history { color: #666; margin-top: 1rem; }
</style>
运行:
进阶用法¶
场景一:可复用状态逻辑(.svelte.ts 文件)¶
// src/lib/stores/counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
let doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment() { count++; },
decrement() { count--; },
reset() { count = initial; }
};
}
// 全局单例
export const globalCounter = createCounter(0);
<!-- 使用 -->
<script>
import { globalCounter } from '$lib/stores/counter.svelte';
// 或创建局部实例
import { createCounter } from '$lib/stores/counter.svelte';
const localCounter = createCounter(10);
</script>
<p>全局: {globalCounter.count} (x2={globalCounter.doubled})</p>
<button onclick={globalCounter.increment}>全局+1</button>
<p>局部: {localCounter.count}</p>
<button onclick={localCounter.increment}>局部+1</button>
场景二:SvelteKit 全栈 CRUD¶
// src/routes/posts/+page.server.ts
import { db } from '$lib/server/db';
export async function load() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' }
});
return { posts };
}
export const actions = {
create: async ({ request }) => {
const data = await request.formData();
const title = data.get('title') as string;
const content = data.get('content') as string;
if (!title || title.length < 3) {
return { success: false, error: '标题至少3个字符' };
}
await db.post.create({ data: { title, content } });
return { success: true };
},
delete: async ({ request }) => {
const data = await request.formData();
const id = data.get('id') as string;
await db.post.delete({ where: { id } });
return { success: true };
}
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
let { data, form } = $props();
</script>
<h1>文章列表</h1>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST" action="?/create">
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容"></textarea>
<button type="submit">发布</button>
</form>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={post.id} />
<button type="submit">删除</button>
</form>
</article>
{/each}
场景三:动画与过渡¶
<script>
import { fly, fade, slide, scale } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
let items = $state([1, 2, 3, 4, 5]);
let visible = $state(true);
function addItem() {
items.push(Math.max(...items) + 1);
}
function removeItem(item: number) {
items = items.filter(i => i !== item);
}
function shuffle() {
items = items.sort(() => Math.random() - 0.5);
}
</script>
<button onclick={() => visible = !visible}>切换可见性</button>
<button onclick={addItem}>添加</button>
<button onclick={shuffle}>打乱</button>
{#if visible}
<div transition:fade={{ duration: 300 }}>
<p>我会淡入淡出</p>
</div>
{/if}
<ul>
{#each items as item (item)}
<li
animate:flip={{ duration: 300, easing: quintOut }}
in:fly={{ y: 20, duration: 300 }}
out:fade={{ duration: 200 }}
>
{item}
<button onclick={() => removeItem(item)}>✕</button>
</li>
{/each}
</ul>
场景四:表单验证与增强¶
<script lang="ts">
import { enhance } from '$app/forms';
let loading = $state(false);
let errors = $state<Record<string, string>>({});
function validate(field: string, value: string) {
if (field === 'email' && !value.includes('@')) {
errors[field] = '请输入有效的邮箱地址';
} else if (field === 'password' && value.length < 8) {
errors[field] = '密码至少8个字符';
} else {
delete errors[field];
}
}
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ update }) => {
await update();
loading = false;
};
}}
>
<label>
邮箱
<input
name="email"
type="email"
oninput={(e) => validate('email', e.currentTarget.value)}
/>
{#if errors.email}<span class="error">{errors.email}</span>{/if}
</label>
<label>
密码
<input
name="password"
type="password"
oninput={(e) => validate('password', e.currentTarget.value)}
/>
{#if errors.password}<span class="error">{errors.password}</span>{/if}
</label>
<button type="submit" disabled={loading || Object.keys(errors).length > 0}>
{loading ? '提交中...' : '注册'}
</button>
</form>
场景五:数据获取与流式加载¶
// src/routes/dashboard/+page.server.ts
export async function load() {
// 并行加载
const [users, stats] = await Promise.all([
fetchUsers(),
fetchStats()
]);
return {
users,
stats,
// 流式加载:不阻塞页面渲染
slowData: fetchSlowData() // 返回 Promise,不 await
};
}
<!-- src/routes/dashboard/+page.svelte -->
<script>
let { data } = $props();
</script>
<!-- 立即可用的数据 -->
<h1>用户数: {data.users.length}</h1>
<p>总访问: {data.stats.visits}</p>
<!-- 流式数据 -->
{#await data.slowData}
<p>加载详细数据...</p>
{:then slowData}
<div>
{#each slowData.items as item}
<p>{item.name}</p>
{/each}
</div>
{:catch error}
<p>加载失败: {error.message}</p>
{/await}
场景六:自定义 Action¶
<script lang="ts">
// 点击外部关闭
function clickOutside(node: HTMLElement, callback: () => void) {
function handleClick(event: MouseEvent) {
if (!node.contains(event.target as Node)) {
callback();
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
// 长按
function longpress(node: HTMLElement, duration = 500) {
let timer: ReturnType<typeof setTimeout>;
function handleMousedown() {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'));
}, duration);
}
function handleMouseup() {
clearTimeout(timer);
}
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('mouseup', handleMouseup);
return {
update(newDuration: number) { duration = newDuration; },
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('mouseup', handleMouseup);
}
};
}
let menuOpen = $state(false);
</script>
{#if menuOpen}
<div class="menu" use:clickOutside={() => menuOpen = false}>
<p>菜单内容</p>
</div>
{/if}
<button use:longpress={800} onlongpress={() => alert('长按!')}>
长按我
</button>
场景七:API 路由¶
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page') ?? 1);
const limit = Number(url.searchParams.get('limit') ?? 10);
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit
});
return json({ users, page, limit });
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.name || !body.email) {
throw error(400, '缺少必填字段');
}
const user = await db.user.create({ data: body });
return json(user, { status: 201 });
};
场景八:布局与路由守卫¶
<!-- src/routes/+layout.svelte -->
<script>
import { page } from '$app/stores';
let { children, data } = $props();
</script>
<nav>
<a href="/" class:active={$page.url.pathname === '/'}>首页</a>
<a href="/about" class:active={$page.url.pathname === '/about'}>关于</a>
{#if data.user}
<a href="/dashboard">控制台</a>
<span>{data.user.name}</span>
{:else}
<a href="/login">登录</a>
{/if}
</nav>
<main>
{@render children()}
</main>
<style>
.active { font-weight: bold; }
</style>
// src/routes/dashboard/+layout.server.ts
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) {
if (!locals.user) {
throw redirect(303, '/login');
}
return { user: locals.user };
}
常见问题与排错¶
问题一:$state 对象修改不触发更新¶
原因:一般不会发生。Svelte 5 的 $state 使用 Proxy 实现深层响应式,直接修改属性就会触发更新。如果没有触发更新,检查是否遗漏了 $state() 声明。
<script>
// 错误:忘了用 $state
let user = { name: '张三' };
user.name = '李四'; // 不会触发更新
// 正确
let user2 = $state({ name: '张三' });
user2.name = '李四'; // 触发更新
</script>
问题二:$effect 无限循环¶
原因:在 $effect 中修改了自己依赖的状态。
<script>
let count = $state(0);
// 错误:无限循环
$effect(() => {
count = count + 1; // 读取 count → 修改 count → 再次触发
});
// 正确:使用 untrack 避免追踪
import { untrack } from 'svelte';
$effect(() => {
const current = untrack(() => count);
// 做一些不涉及修改 count 的事
});
</script>
问题三:从 Svelte 4 迁移到 Svelte 5¶
主要变化对照:
Svelte 4 → Svelte 5
let x = 0 → let x = $state(0)
$: doubled = x * 2 → let doubled = $derived(x * 2)
$: { console.log(x) } → $effect(() => { console.log(x) })
export let prop → let { prop } = $props()
<slot /> → {@render children()}
on:click={handler} → onclick={handler}
问题四:SvelteKit 数据加载 +page.server.ts vs +page.ts¶
+page.server.ts:只在服务端运行,可安全使用数据库、环境变量、密钥+page.ts:在服务端和客户端都运行,适合调用公共 API
// +page.server.ts(服务端专用)
export async function load() {
const users = await db.query('SELECT * FROM users');
return { users };
}
// +page.ts(通用)
export async function load({ fetch }) {
const res = await fetch('/api/users');
const users = await res.json();
return { users };
}
问题五:CSS 样式作用域问题¶
Svelte 样式默认是组件作用域的:
<style>
/* 只影响这个组件 */
p { color: red; }
/* 穿透到子组件 */
:global(p) { color: red; }
/* 部分穿透 */
.wrapper :global(p) { color: red; }
</style>
问题六:环境变量使用¶
// 客户端可用(PUBLIC_ 前缀)
import { PUBLIC_API_URL } from '$env/static/public';
// 服务端专用
import { SECRET_DB_URL } from '$env/static/private';
// 动态环境变量
import { env } from '$env/dynamic/private';
console.log(env.SECRET_DB_URL);
问题七:部署配置¶
# Node.js 服务器
npm i -D @sveltejs/adapter-node
# 静态站点
npm i -D @sveltejs/adapter-static
# Vercel
npm i -D @sveltejs/adapter-vercel
# Cloudflare Pages
npm i -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
const config = {
kit: {
adapter: adapter({
out: 'build',
precompress: true,
envPrefix: 'APP_'
})
}
};
export default config;
问题八:性能优化¶
<script>
// 1. 用 $derived 替代 $effect 中的计算
// 坏:在 effect 中计算并更新另一个状态
let items = $state([]);
let total_bad = $state(0);
$effect(() => { total_bad = items.reduce((a, b) => a + b, 0); }); // 不好
// 好:用 $derived
let total_good = $derived(items.reduce((a, b) => a + b, 0));
// 2. 大列表用 key
// {#each items as item (item.id)} ← 提供唯一 key
// 3. 条件渲染而非隐藏
// {#if visible}<HeavyComponent />{/if} ← 不渲染就没有开销
</script>
参考资源¶
- 官方文档:https://svelte.dev/docs
- 官方教程:https://svelte.dev/tutorial
- SvelteKit 文档:https://svelte.dev/docs/kit
- Svelte Playground:https://svelte.dev/playground
- GitHub:https://github.com/sveltejs/svelte
- Svelte 5 迁移指南:https://svelte.dev/docs/svelte/v5-migration-guide
- Svelte Society:https://sveltesociety.dev/
- Awesome Svelte:https://github.com/TheComputerM/awesome-svelte
- Joy of Code (博客):https://joyofcode.xyz/
- Svelte Discord:https://svelte.dev/chat