svelte
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSvelte 5 - Compiler-First Reactive Framework
Svelte 5 - 编译器优先的响应式框架
Overview
概述
Svelte is a compiler-based reactive UI framework that shifts work from runtime to build time. Unlike React/Vue, Svelte compiles components to highly optimized vanilla JavaScript with minimal overhead. Svelte 5 introduces Runes API for explicit, fine-grained reactivity.
Key Features:
- Runes API: $state, $derived, $effect for explicit reactivity
- Zero runtime overhead: Compiles to vanilla JS
- Built-in state management: No external libraries needed
- SvelteKit: Full-stack framework with SSR/SSG/SPA
- Write less code: Simple, readable component syntax
- Exceptional performance: Small bundles, fast runtime
Installation:
bash
undefinedSvelte是一款基于编译器的响应式UI框架,它将工作从运行时转移到构建时。与React/Vue不同,Svelte会将组件编译为高度优化的原生JavaScript,运行时开销极小。Svelte 5引入了Runes API,实现显式、细粒度的响应式能力。
Create new SvelteKit project
核心特性
npm create svelte@latest my-app
cd my-app
npm install
npm run dev
- Runes API:$state、$derived、$effect实现显式响应式
- 零运行时开销:编译为原生JS
- 内置状态管理:无需外部库
- SvelteKit:支持SSR/SSG/SPA的全栈框架
- 代码量更少:简洁易读的组件语法
- 卓越性能:包体积小,运行速度快
Or Svelte only (no SvelteKit)
安装步骤
npm create vite@latest my-app -- --template svelte-ts
undefinedbash
undefinedSvelte 5 Runes API (Modern Approach)
创建新的SvelteKit项目
State Management with $state
—
svelte
<script lang="ts">
// Reactive state - automatically tracks changes
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
// Arrays and objects are deeply reactive
let todos = $state<Todo[]>([]);
function addTodo(text: string) {
todos.push({ id: Date.now(), text, done: false });
// No need for todos = [...todos] like React!
}
function increment() {
count++; // Triggers reactivity
}
</script>
<button onclick={increment}>
Clicked {count} times
</button>npm create svelte@latest my-app
cd my-app
npm install
npm run dev
Computed Values with $derived
或仅安装Svelte(不含SvelteKit)
svelte
<script lang="ts">
let firstName = $state('John');
let lastName = $state('Doe');
// Automatically updates when dependencies change
let fullName = $derived(`${firstName} ${lastName}`);
let greeting = $derived(`Hello, ${fullName}`);
// Complex derivations
let items = $state([1, 2, 3, 4, 5]);
let total = $derived(items.reduce((sum, n) => sum + n, 0));
let average = $derived(total / items.length);
</script>
<p>{greeting}</p>
<p>Average: {average.toFixed(2)}</p>npm create vite@latest my-app -- --template svelte-ts
undefinedSide Effects with $effect
Svelte 5 Runes API(现代方案)
—
使用$state管理状态
svelte
<script lang="ts">
let count = $state(0);
let logs = $state<string[]>([]);
// Runs when dependencies change
$effect(() => {
console.log(`Count is now: ${count}`);
logs.push(`Count changed to ${count}`);
// Optional cleanup function
return () => {
console.log('Cleaning up previous effect');
};
});
// Effect with external subscriptions
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
return () => clearInterval(interval);
});
</script>svelte
<script lang="ts">
// 响应式状态 - 自动追踪变化
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
// 数组和对象支持深度响应式
let todos = $state<Todo[]>([]);
function addTodo(text: string) {
todos.push({ id: Date.now(), text, done: false });
// 无需像React那样写todos = [...todos]!
}
function increment() {
count++; // 触发响应式更新
}
</script>
<button onclick={increment}>
已点击 {count} 次
</button>Component Props with $props
使用$derived计算值
svelte
<script lang="ts">
interface Props {
title: string;
count?: number;
onUpdate?: (value: number) => void;
}
// Type-safe props with defaults
let { title, count = 0, onUpdate } = $props<Props>();
// Props are read-only, but can derive from them
let displayTitle = $derived(title.toUpperCase());
</script>
<h1>{displayTitle}</h1>
<p>Count: {count}</p>
<button onclick={() => onUpdate?.(count + 1)}>
Increment
</button>svelte
<script lang="ts">
let firstName = $state('John');
let lastName = $state('Doe');
// 依赖项变化时自动更新
let fullName = $derived(`${firstName} ${lastName}`);
let greeting = $derived(`Hello, ${fullName}`);
// 复杂计算
let items = $state([1, 2, 3, 4, 5]);
let total = $derived(items.reduce((sum, n) => sum + n, 0));
let average = $derived(total / items.length);
</script>
<p>{greeting}</p>
<p>平均值: {average.toFixed(2)}</p>Two-Way Binding with $bindable
使用$effect处理副作用
svelte
<!-- Child: SearchInput.svelte -->
<script lang="ts">
interface Props {
value: string;
placeholder?: string;
}
// Allows parent to bind to this prop
let { value = $bindable(''), placeholder = 'Search...' } = $props<Props>();
</script>
<input bind:value {placeholder} type="search" />
<!-- Parent.svelte -->
<script lang="ts">
import SearchInput from './SearchInput.svelte';
let query = $state('');
let results = $derived(query ? search(query) : []);
</script>
<SearchInput bind:value={query} />
<p>Found {results.length} results for "{query}"</p>svelte
<script lang="ts">
let count = $state(0);
let logs = $state<string[]>([]);
// 依赖项变化时执行
$effect(() => {
console.log(`当前Count值: ${count}`);
logs.push(`Count已变更为 ${count}`);
// 可选的清理函数
return () => {
console.log('清理上一次的副作用');
};
});
// 处理外部订阅的副作用
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
return () => clearInterval(interval);
});
</script>Component Patterns
使用$props定义组件属性
Basic Component Structure
—
svelte
<!-- Counter.svelte -->
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
function decrement() {
count--;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>Count: {count} (Doubled: {doubled})</span>
<button onclick={increment}>+</button>
</div>
<style>
.counter {
display: flex;
gap: 1rem;
align-items: center;
}
button {
padding: 0.5rem 1rem;
font-size: 1.2rem;
}
</style>svelte
<script lang="ts">
interface Props {
title: string;
count?: number;
onUpdate?: (value: number) => void;
}
// 带默认值的类型安全属性
let { title, count = 0, onUpdate } = $props<Props>();
// 属性为只读,但可以基于属性派生新值
let displayTitle = $derived(title.toUpperCase());
</script>
<h1>{displayTitle}</h1>
<p>计数: {count}</p>
<button onclick={() => onUpdate?.(count + 1)}>
增加
</button>Conditional Rendering
使用$bindable实现双向绑定
svelte
<script lang="ts">
let loggedIn = $state(false);
let user = $state<User | null>(null);
let loading = $state(true);
</script>
{#if loading}
<p>Loading...</p>
{:else if loggedIn && user}
<p>Welcome, {user.name}!</p>
{:else}
<p>Please log in</p>
{/if}svelte
<!-- 子组件: SearchInput.svelte -->
<script lang="ts">
interface Props {
value: string;
placeholder?: string;
}
// 允许父组件绑定该属性
let { value = $bindable(''), placeholder = '搜索...' } = $props<Props>();
</script>
<input bind:value {placeholder} type="search" />
<!-- 父组件: Parent.svelte -->
<script lang="ts">
import SearchInput from './SearchInput.svelte';
let query = $state('');
let results = $derived(query ? search(query) : []);
</script>
<SearchInput bind:value={query} />
<p>为 "{query}" 找到 {results.length} 条结果</p>Lists and Keyed Each Blocks
组件模式
—
基础组件结构
svelte
<script lang="ts">
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos = $state<Todo[]>([
{ id: 1, text: 'Learn Svelte', done: true },
{ id: 2, text: 'Build app', done: false }
]);
function toggle(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
</script>
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input
type="checkbox"
checked={todo.done}
onchange={() => toggle(todo.id)}
/>
{todo.text}
</li>
{/each}
</ul>
<style>
.done {
text-decoration: line-through;
opacity: 0.6;
}
</style>svelte
<!-- Counter.svelte -->
<script lang="ts">
let count = $state(0);
let doubled = $derived(count * 2);
function increment() {
count++;
}
function decrement() {
count--;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>计数: {count} (翻倍值: {doubled})</span>
<button onclick={increment}>+</button>
</div>
<style>
.counter {
display: flex;
gap: 1rem;
align-items: center;
}
button {
padding: 0.5rem 1rem;
font-size: 1.2rem;
}
</style>SvelteKit Framework
条件渲染
Project Structure
—
my-app/
├── src/
│ ├── routes/
│ │ ├── +page.svelte # Home page
│ │ ├── +page.ts # Universal load
│ │ ├── +page.server.ts # Server load
│ │ ├── +layout.svelte # Shared layout
│ │ ├── about/
│ │ │ └── +page.svelte # /about
│ │ └── blog/
│ │ ├── +page.svelte # /blog
│ │ └── [slug]/
│ │ └── +page.svelte # /blog/my-post
│ ├── lib/
│ │ ├── components/
│ │ ├── stores/
│ │ └── utils/
│ └── app.html
├── static/ # Static assets
└── svelte.config.jssvelte
<script lang="ts">
let loggedIn = $state(false);
let user = $state<User | null>(null);
let loading = $state(true);
</script>
{#if loading}
<p>加载中...</p>
{:else if loggedIn && user}
<p>欢迎您, {user.name}!</p>
{:else}
<p>请登录</p>
{/if}Load Functions (Data Fetching)
列表与带键的Each块
typescript
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
const post = await response.json();
return {
post
};
};svelte
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
</article>svelte
<script lang="ts">
interface Todo {
id: number;
text: string;
done: boolean;
}
let todos = $state<Todo[]>([
{ id: 1, text: '学习Svelte', done: true },
{ id: 2, text: '开发应用', done: false }
]);
function toggle(id: number) {
const todo = todos.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
</script>
<ul>
{#each todos as todo (todo.id)}
<li class:done={todo.done}>
<input
type="checkbox"
checked={todo.done}
onchange={() => toggle(todo.id)}
/>
{todo.text}
</li>
{/each}
</ul>
<style>
.done {
text-decoration: line-through;
opacity: 0.6;
}
</style>Server-Only Load Functions
SvelteKit框架
—
项目结构
typescript
// src/routes/admin/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.isAdmin) {
throw redirect(303, '/login');
}
const users = await db.users.findMany();
return {
users
};
};my-app/
├── src/
│ ├── routes/
│ │ ├── +page.svelte # 首页
│ │ ├── +page.ts # 通用加载逻辑
│ │ ├── +page.server.ts # 服务端加载逻辑
│ │ ├── +layout.svelte # 共享布局
│ │ ├── about/
│ │ │ └── +page.svelte # /about页面
│ │ └── blog/
│ │ ├── +page.svelte # /blog页面
│ │ └── [slug]/
│ │ └── +page.svelte # /blog/my-post页面
│ ├── lib/
│ │ ├── components/
│ │ ├── stores/
│ │ └── utils/
│ └── app.html
├── static/ # 静态资源
└── svelte.config.jsForm Actions
加载函数(数据获取)
typescript
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
export const actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const data = Object.fromEntries(formData);
const result = schema.safeParse(data);
if (!result.success) {
return fail(400, {
errors: result.error.flatten().fieldErrors
});
}
const user = await authenticateUser(result.data);
if (!user) {
return fail(401, { message: 'Invalid credentials' });
}
cookies.set('session', user.sessionToken, { path: '/' });
throw redirect(303, '/dashboard');
}
} satisfies Actions;svelte
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form } = $props<{ form?: ActionData }>();
</script>
<form method="POST">
<input name="email" type="email" required />
{#if form?.errors?.email}
<span class="error">{form.errors.email[0]}</span>
{/if}
<input name="password" type="password" required />
{#if form?.errors?.password}
<span class="error">{form.errors.password[0]}</span>
{/if}
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Log In</button>
</form>typescript
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
const post = await response.json();
return {
post
};
};svelte
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
</article>State Management Patterns
服务端专属加载函数
Runes-Based Store (Modern)
—
typescript
// src/lib/stores/cart.svelte.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function createCart() {
let items = $state<CartItem[]>([]);
let total = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
let itemCount = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
return {
get items() { return items; },
get total() { return total; },
get itemCount() { return itemCount; },
addItem(item: Omit<CartItem, 'quantity'>) {
const existing = items.find(i => i.id === item.id);
if (existing) {
existing.quantity++;
} else {
items.push({ ...item, quantity: 1 });
}
},
removeItem(id: number) {
items = items.filter(i => i.id !== id);
},
clear() {
items = [];
}
};
}
export const cart = createCart();svelte
<!-- Usage -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
<div>
<p>Items: {cart.itemCount}</p>
<p>Total: ${cart.total.toFixed(2)}</p>
<button onclick={() => cart.addItem({ id: 1, name: 'Widget', price: 9.99 })}>
Add Widget
</button>
</div>typescript
// src/routes/admin/+page.server.ts
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.isAdmin) {
throw redirect(303, '/login');
}
const users = await db.users.findMany();
return {
users
};
};Legacy Svelte Store (Svelte 4 Style)
表单操作
typescript
// src/lib/stores/user.ts
import { writable, derived } from 'svelte/store';
interface User {
id: number;
name: string;
email: string;
}
function createUserStore() {
const { subscribe, set, update } = writable<User | null>(null);
return {
subscribe,
login: (user: User) => set(user),
logout: () => set(null),
updateName: (name: string) => update(u => u ? { ...u, name } : null)
};
}
export const user = createUserStore();
// Derived store
export const isLoggedIn = derived(user, $user => $user !== null);svelte
<!-- Usage with $ prefix -->
<script lang="ts">
import { user, isLoggedIn } from '$lib/stores/user';
</script>
{#if $isLoggedIn}
<p>Welcome, {$user?.name}!</p>
{:else}
<p>Please log in</p>
{/if}typescript
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
export const actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const data = Object.fromEntries(formData);
const result = schema.safeParse(data);
if (!result.success) {
return fail(400, {
errors: result.error.flatten().fieldErrors
});
}
const user = await authenticateUser(result.data);
if (!user) {
return fail(401, { message: '凭据无效' });
}
cookies.set('session', user.sessionToken, { path: '/' });
throw redirect(303, '/dashboard');
}
} satisfies Actions;svelte
<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types';
let { form } = $props<{ form?: ActionData }>();
</script>
<form method="POST">
<input name="email" type="email" required />
{#if form?.errors?.email}
<span class="error">{form.errors.email[0]}</span>
{/if}
<input name="password" type="password" required />
{#if form?.errors?.password}
<span class="error">{form.errors.password[0]}</span>
{/if}
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">登录</button>
</form>Animations and Transitions
状态管理模式
Built-in Transitions
基于Runes的Store(现代方案)
svelte
<script lang="ts">
import { fade, slide, fly, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = $state(true);
</script>
<button onclick={() => visible = !visible}>
Toggle
</button>
{#if visible}
<div transition:fade={{ duration: 300 }}>
Fades in and out
</div>
<div transition:slide={{ duration: 300 }}>
Slides in and out
</div>
<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}>
Flies in from bottom
</div>
{/if}typescript
// src/lib/stores/cart.svelte.ts
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
function createCart() {
let items = $state<CartItem[]>([]);
let total = $derived(
items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
let itemCount = $derived(
items.reduce((sum, item) => sum + item.quantity, 0)
);
return {
get items() { return items; },
get total() { return total; },
get itemCount() { return itemCount; },
addItem(item: Omit<CartItem, 'quantity'>) {
const existing = items.find(i => i.id === item.id);
if (existing) {
existing.quantity++;
} else {
items.push({ ...item, quantity: 1 });
}
},
removeItem(id: number) {
items = items.filter(i => i.id !== id);
},
clear() {
items = [];
}
};
}
export const cart = createCart();svelte
<!-- 使用示例 -->
<script lang="ts">
import { cart } from '$lib/stores/cart.svelte';
</script>
<div>
<p>商品数量: {cart.itemCount}</p>
<p>总价: ${cart.total.toFixed(2)}</p>
<button onclick={() => cart.addItem({ id: 1, name: 'Widget', price: 9.99 })}>
添加Widget
</button>
</div>Custom Transitions
传统Svelte Store(Svelte 4风格)
typescript
// src/lib/transitions.ts
export function blur(node: HTMLElement, { duration = 300 }) {
return {
duration,
css: (t: number) => `
opacity: ${t};
filter: blur(${(1 - t) * 10}px);
`
};
}svelte
<script lang="ts">
import { blur } from '$lib/transitions';
let show = $state(true);
</script>
{#if show}
<div transition:blur={{ duration: 500 }}>
Custom blur transition
</div>
{/if}typescript
// src/lib/stores/user.ts
import { writable, derived } from 'svelte/store';
interface User {
id: number;
name: string;
email: string;
}
function createUserStore() {
const { subscribe, set, update } = writable<User | null>(null);
return {
subscribe,
login: (user: User) => set(user),
logout: () => set(null),
updateName: (name: string) => update(u => u ? { ...u, name } : null)
};
}
export const user = createUserStore();
// 派生Store
export const isLoggedIn = derived(user, $user => $user !== null);svelte
<!-- 使用$前缀访问 -->
<script lang="ts">
import { user, isLoggedIn } from '$lib/stores/user';
</script>
{#if $isLoggedIn}
<p>欢迎您, {$user?.name}!</p>
{:else}
<p>请登录</p>
{/if}Animate Directive (FLIP Animations)
动画与过渡效果
—
内置过渡效果
svelte
<script lang="ts">
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
let items = $state([1, 2, 3, 4, 5]);
function shuffle() {
items = items.sort(() => Math.random() - 0.5);
}
</script>
<button onclick={shuffle}>Shuffle</button>
<ul>
{#each items as item (item)}
<li animate:flip={{ duration: 500, easing: quintOut }}>
{item}
</li>
{/each}
</ul>svelte
<script lang="ts">
import { fade, slide, fly, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = $state(true);
</script>
<button onclick={() => visible = !visible}>
切换显示
</button>
{#if visible}
<div transition:fade={{ duration: 300 }}>
淡入淡出效果
</div>
<div transition:slide={{ duration: 300 }}>
滑入滑出效果
</div>
<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}>
从底部飞入效果
</div>
{/if}Advanced Features
自定义过渡效果
Actions (Element Behaviors)
—
typescript
// src/lib/actions.ts
export function clickOutside(node: HTMLElement, callback: () => void) {
const handleClick = (event: MouseEvent) => {
if (!node.contains(event.target as Node)) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}svelte
<script lang="ts">
import { clickOutside } from '$lib/actions';
let showMenu = $state(false);
</script>
<button onclick={() => showMenu = !showMenu}>
Menu
</button>
{#if showMenu}
<div
class="menu"
use:clickOutside={() => showMenu = false}
>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
{/if}typescript
// src/lib/transitions.ts
export function blur(node: HTMLElement, { duration = 300 }) {
return {
duration,
css: (t: number) => `
opacity: ${t};
filter: blur(${(1 - t) * 10}px);
`
};
}svelte
<script lang="ts">
import { blur } from '$lib/transitions';
let show = $state(true);
</script>
{#if show}
<div transition:blur={{ duration: 500 }}>
自定义模糊过渡效果
</div>
{/if}Slots and Component Composition
Animate指令(FLIP动画)
svelte
<!-- Card.svelte -->
<script lang="ts">
let { title } = $props<{ title?: string }>();
</script>
<div class="card">
<header>
{#if title}
<h2>{title}</h2>
{:else}
<slot name="header">Default Header</slot>
{/if}
</header>
<main>
<slot>Default content</slot>
</main>
<footer>
<slot name="footer" />
</footer>
</div>
<!-- Usage -->
<Card title="My Card">
<p>This is the main content</p>
<div slot="footer">
<button>Action</button>
</div>
</Card>svelte
<script lang="ts">
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
let items = $state([1, 2, 3, 4, 5]);
function shuffle() {
items = items.sort(() => Math.random() - 0.5);
}
</script>
<button onclick={shuffle}>随机排序</button>
<ul>
{#each items as item (item)}
<li animate:flip={{ duration: 500, easing: quintOut }}>
{item}
</li>
{/each}
</ul>Special Elements
高级特性
—
Actions(元素行为)
svelte
<script lang="ts">
let Component = $state<typeof import('./A.svelte').default | typeof import('./B.svelte').default>(A);
let tag = $state<'div' | 'span'>('div');
</script>
<!-- Dynamic component -->
<svelte:component this={Component} />
<!-- Dynamic HTML element -->
<svelte:element this={tag}>
Content
</svelte:element>
<!-- Window events -->
<svelte:window
onresize={() => console.log('Window resized')}
onkeydown={(e) => console.log(e.key)}
/>
<!-- Document head -->
<svelte:head>
<title>My Page</title>
<meta name="description" content="Description" />
</svelte:head>typescript
// src/lib/actions.ts
export function clickOutside(node: HTMLElement, callback: () => void) {
const handleClick = (event: MouseEvent) => {
if (!node.contains(event.target as Node)) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}svelte
<script lang="ts">
import { clickOutside } from '$lib/actions';
let showMenu = $state(false);
</script>
<button onclick={() => showMenu = !showMenu}>
菜单
</button>
{#if showMenu}
<div
class="menu"
use:clickOutside={() => showMenu = false}
>
<ul>
<li>选项1</li>
<li>选项2</li>
</ul>
</div>
{/if}Migration from React/Vue
插槽与组件组合
React to Svelte 5
—
| React | Svelte 5 | Notes |
|---|---|---|
| | Direct state |
| | Auto-tracked |
| | Auto deps |
| | Destructure |
| | Explicit |
svelte
<!-- Card.svelte -->
<script lang="ts">
let { title } = $props<{ title?: string }>();
</script>
<div class="card">
<header>
{#if title}
<h2>{title}</h2>
{:else}
<slot name="header">默认头部</slot>
{/if}
</header>
<main>
<slot>默认内容</slot>
</main>
<footer>
<slot name="footer" />
</footer>
</div>
<!-- 使用示例 -->
<Card title="我的卡片">
<p>这是主要内容</p>
<div slot="footer">
<button>操作按钮</button>
</div>
</Card>Vue 3 to Svelte 5
特殊元素
| Vue 3 | Svelte 5 | Notes |
|---|---|---|
| | Direct state |
| | Similar |
| | Auto-tracked |
| | Type-safe |
| | Block syntax |
svelte
<script lang="ts">
let Component = $state<typeof import('./A.svelte').default | typeof import('./B.svelte').default>(A);
let tag = $state<'div' | 'span'>('div');
</script>
<!-- 动态组件 -->
<svelte:component this={Component} />
<!-- 动态HTML元素 -->
<svelte:element this={tag}>
内容
</svelte:element>
<!-- 窗口事件 -->
<svelte:window
onresize={() => console.log('窗口已调整大小')}
onkeydown={(e) => console.log(e.key)}
/>
<!-- 文档头部 -->
<svelte:head>
<title>我的页面</title>
<meta name="description" content="页面描述" />
</svelte:head>Performance Best Practices
从React/Vue迁移
—
React迁移到Svelte 5
- Use $derived over $effect when only computed values are needed
- Avoid unnecessary $effect - only for side effects, not computations
- Leverage compiler optimizations - Svelte does most work at build time
- Use keyed each blocks -
{#each items as item (item.id)} - Lazy load components -
const Component = await import('./Heavy.svelte') - SSR for initial load - Use SvelteKit's SSR capabilities
- Optimize bundles - Use adapters for deployment targets
| React | Svelte 5 | 说明 |
|---|---|---|
| | 直接定义状态 |
| | 自动追踪依赖 |
| | 自动识别依赖 |
| | 解构获取属性 |
| | 显式条件语法 |
Testing
Vue 3迁移到Svelte 5
Unit Tests with Vitest
—
typescript
// Counter.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments counter on click', async () => {
const { getByText } = render(Counter);
const button = getByText('+');
await fireEvent.click(button);
expect(getByText(/Count: 1/)).toBeInTheDocument();
});| Vue 3 | Svelte 5 | 说明 |
|---|---|---|
| | 直接定义状态 |
| | 用法类似 |
| | 自动追踪依赖 |
| | 类型安全 |
| | 块级语法 |
E2E with Playwright
性能最佳实践
typescript
// tests/login.test.ts
import { expect, test } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('text=Welcome')).toBeVisible();
});- 优先使用$derived而非$effect:仅需计算值时选择$derived
- 避免不必要的$effect:仅用于处理副作用,不用于计算
- 利用编译器优化:Svelte会在构建时完成大部分工作
- 使用带键的Each块:
{#each items as item (item.id)} - 懒加载组件:
const Component = await import('./Heavy.svelte') - 使用SSR优化初始加载:借助SvelteKit的SSR能力
- 优化包体积:针对部署目标选择合适的适配器
Deployment
测试
Adapters
使用Vitest进行单元测试
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel'; // or adapter-node, adapter-static
export default {
kit: {
adapter: adapter()
}
};Available Adapters:
- : Auto-detect platform
adapter-auto - : Vercel deployment
adapter-vercel - : Node.js server
adapter-node - : Static site generation
adapter-static - : Cloudflare Pages/Workers
adapter-cloudflare - : Netlify deployment
adapter-netlify
typescript
// Counter.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('点击按钮增加计数', async () => {
const { getByText } = render(Counter);
const button = getByText('+');
await fireEvent.click(button);
expect(getByText(/计数: 1/)).toBeInTheDocument();
});Resources
使用Playwright进行端到端测试
- Svelte Docs: https://svelte.dev/docs
- SvelteKit Docs: https://kit.svelte.dev/docs
- Svelte 5 Runes: https://svelte-5-preview.vercel.app/docs/runes
- Tutorial: https://learn.svelte.dev
- REPL: https://svelte.dev/repl
typescript
// tests/login.test.ts
import { expect, test } from '@playwright/test';
test('用户可以登录', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('text=欢迎')).toBeVisible();
});Summary
部署
—
适配器配置
- Svelte 5 uses Runes API ($state, $derived, $effect) for explicit reactivity
- Compiler-first - shifts work to build time for minimal runtime overhead
- SvelteKit provides full-stack capabilities with SSR/SSG/SPA modes
- Write less code - simpler syntax than React/Vue
- Exceptional performance - small bundles, fast runtime
- Migration-friendly - Svelte 4 and 5 can coexist
- Type-safe - First-class TypeScript support
javascript
// svelte.config.js
import adapter from '@sveltejs/adapter-vercel'; // 或adapter-node、adapter-static
export default {
kit: {
adapter: adapter()
}
};可用适配器:
- : 自动检测部署平台
adapter-auto - : 部署到Vercel
adapter-vercel - : 部署为Node.js服务
adapter-node - : 生成静态站点
adapter-static - : 部署到Cloudflare Pages/Workers
adapter-cloudflare - : 部署到Netlify
adapter-netlify
—
资源链接
—
- Svelte官方文档: https://svelte.dev/docs
- SvelteKit官方文档: https://kit.svelte.dev/docs
- Svelte 5 Runes文档: https://svelte-5-preview.vercel.app/docs/runes
- 官方教程: https://learn.svelte.dev
- 在线REPL: https://svelte.dev/repl
—
总结
—
- Svelte 5 使用Runes API($state、$derived、$effect)实现显式响应式
- 编译器优先:将工作转移到构建时,最小化运行时开销
- SvelteKit 提供全栈能力,支持SSR/SSG/SPA模式
- 代码量更少:语法比React/Vue更简洁
- 性能卓越:包体积小,运行速度快
- 迁移友好:Svelte 4和5可以共存
- 类型安全:原生支持TypeScript