名称: svelte-development
描述: Svelte 5 开发,包含符文($state、$derived、$effect)、SvelteKit 全栈框架和现代响应式模式。适用于构建 Svelte 应用程序、实现细粒度响应性或处理 SvelteKit 路由和服务器功能。
Svelte 5 开发
构建现代 Svelte 应用程序的全面指南,使用符文和 SvelteKit。
技术栈概述
| 工具 |
用途 |
版本 |
| Svelte 5 |
核心框架 |
5.0+ |
| SvelteKit |
全栈框架 |
2.0+ |
| TypeScript |
类型安全 |
5.0+ |
| Vite |
构建工具 |
5.0+ |
| Vitest |
测试工具 |
1.0+ |
Svelte 5 符文(核心响应式)
符文系统
符文是带有 $ 前缀的编译时宏,提供显式、细粒度的响应式。
| 符文 |
用途 |
替代品 |
$state |
响应式状态 |
let 声明 |
$derived |
计算值 |
$: 响应式语句 |
$effect |
副作用 |
$: 副作用语句 |
$props |
组件属性 |
export let |
$bindable |
双向绑定属性 |
export let 配合 bind: |
基本组件结构
<script lang="ts">
// 使用 TypeScript 定义属性
interface Props {
title: string
count?: number
onUpdate?: (value: number) => void
}
let { title, count = 0, onUpdate }: Props = $props()
// 响应式状态
let localCount = $state(count)
let items = $state<string[]>([])
// 派生值(依赖变化时自动更新)
let doubled = $derived(localCount * 2)
let total = $derived(items.reduce((sum, i) => sum + i.length, 0))
// 副作用
$effect(() => {
console.log(`计数变为: ${localCount}`)
onUpdate?.(localCount)
})
// 方法
function increment() {
localCount++
}
</script>
<div class="component">
<h1>{title}</h1>
<p>计数: {localCount} (翻倍: {doubled})</p>
<button onclick={increment}>增加</button>
</div>
<style>
.component {
padding: 1rem;
}
</style>
状态模式
// 原始类型
let count = $state(0);
let name = $state("");
// 对象(深层响应式)
let user = $state({
name: "John",
email: "john@example.com",
preferences: { theme: "dark" },
});
// 数组
let items = $state<Item[]>([]);
// 直接修改有效!
user.name = "Jane"; // 响应式
user.preferences.theme = "light"; // 响应式(深层)
items.push({ id: 1, name: "New" }); // 响应式
// 冻结状态(浅层响应式)
let frozenList = $state.frozen([1, 2, 3]);
派生值
// 简单派生
let doubled = $derived(count * 2);
// 复杂派生(使用 $derived.by 处理多行)
let stats = $derived.by(() => {
const total = items.reduce((sum, i) => sum + i.value, 0);
const average = items.length ? total / items.length : 0;
return { total, average, count: items.length };
});
// 从多个源派生
let summary = $derived(`${user.name} 有 ${items.length} 个项目`);
副作用
// 基本副作用(依赖变化时运行)
$effect(() => {
console.log(`计数现在为: ${count}`);
});
// 带有清理的副作用
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
// 清理函数(返回)
return () => clearInterval(interval);
});
// 前置副作用(DOM 更新前运行)
$effect.pre(() => {
console.log("即将更新 DOM");
});
// 根副作用(不跟踪依赖)
$effect.root(() => {
// 手动依赖管理
});
组件模式
属性与默认值和展开
<script lang="ts">
interface Props {
variant?: 'primary' | 'secondary'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
children: import('svelte').Snippet
}
let {
variant = 'primary',
size = 'md',
disabled = false,
children,
...restProps
}: Props = $props()
</script>
<button
class="btn btn-{variant} btn-{size}"
{disabled}
{...restProps}
>
{@render children()}
</button>
使用 $bindable 进行双向绑定
<!-- Input.svelte -->
<script lang="ts">
interface Props {
value: string
}
let { value = $bindable() }: Props = $props()
</script>
<input bind:value />
<!-- Parent.svelte -->
<script lang="ts">
let name = $state('')
</script>
<Input bind:value={name} />
<p>你好, {name}!</p>
片段(替代插槽)
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
title: string
children: Snippet
footer?: Snippet
}
let { title, children, footer }: Props = $props()
</script>
<div class="card">
<header>{title}</header>
<main>{@render children()}</main>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
<!-- 用法 -->
<Card title="我的卡片">
<p>卡片内容放在这里</p>
{#snippet footer()}
<button>操作</button>
{/snippet}
</Card>
带有参数的片段
<!-- List.svelte -->
<script lang="ts" generics="T">
import type { Snippet } from 'svelte'
interface Props {
items: T[]
row: Snippet<[T, number]>
empty?: Snippet
}
let { items, row, empty }: Props = $props()
</script>
{#if items.length === 0}
{#if empty}
{@render empty()}
{:else}
<p>没有项目</p>
{/if}
{:else}
<ul>
{#each items as item, index}
<li>{@render row(item, index)}</li>
{/each}
</ul>
{/if}
<!-- 用法 -->
<List items={users}>
{#snippet row(user, i)}
<span>{i + 1}. {user.name}</span>
{/snippet}
{#snippet empty()}
<p>未找到用户</p>
{/snippet}
</List>
使用 .svelte.ts 文件的共享状态
创建共享状态
// lib/stores/counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get count() {
return count;
},
increment() {
count++;
},
decrement() {
count--;
},
reset() {
count = initial;
},
};
}
// 单例实例
export const counter = createCounter();
基于类的状态
// lib/stores/user.svelte.ts
export class UserStore {
user = $state<User | null>(null);
loading = $state(false);
error = $state<string | null>(null);
isLoggedIn = $derived(!!this.user);
async login(email: string, password: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) throw new Error("登录失败");
this.user = await response.json();
} catch (e) {
this.error = e instanceof Error ? e.message : "未知错误";
throw e;
} finally {
this.loading = false;
}
}
logout() {
this.user = null;
}
}
export const userStore = new UserStore();
使用共享状态
<script lang="ts">
import { counter } from '$lib/stores/counter.svelte'
import { userStore } from '$lib/stores/user.svelte'
</script>
<p>计数: {counter.count}</p>
<button onclick={counter.increment}>+</button>
{#if userStore.isLoggedIn}
<p>欢迎, {userStore.user?.name}</p>
<button onclick={() => userStore.logout()}>登出</button>
{:else}
<button onclick={() => userStore.login('test@test.com', 'pass')}>
登录
</button>
{/if}
SvelteKit(全栈)
项目结构
sveltekit-app/
├── src/
│ ├── routes/ # 文件式路由
│ │ ├── +page.svelte # /
│ │ ├── +page.server.ts # 服务器加载函数
│ │ ├── +layout.svelte # 根布局
│ │ ├── about/
│ │ │ └── +page.svelte # /about
│ │ ├── users/
│ │ │ ├── +page.svelte # /users
│ │ │ └── [id]/
│ │ │ ├── +page.svelte # /users/:id
│ │ │ └── +page.server.ts
│ │ └── api/
│ │ └── users/
│ │ └── +server.ts # /api/users
│ ├── lib/ # $lib 别名
│ │ ├── components/
│ │ └── stores/
│ └── app.html
├── static/
└── svelte.config.js
加载函数
// routes/users/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ fetch, params }) => {
const response = await fetch('/api/users')
if (!response.ok) {
throw error(response.status, '加载用户失败')
}
const users = await response.json()
return { users }
}
// routes/users/+page.svelte
<script lang="ts">
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
</script>
<h1>用户</h1>
{#each data.users as user}
<p>{user.name}</p>
{/each}
表单操作
// routes/login/+page.server.ts
import type { Actions } from "./$types";
import { fail, redirect } from "@sveltejs/kit";
export const actions: Actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// 验证
if (!email || !password) {
return fail(400, { email, missing: true });
}
// 认证
const user = await authenticate(email, password);
if (!user) {
return fail(401, { email, incorrect: true });
}
// 设置会话 cookie
cookies.set("session", user.token, { path: "/" });
throw redirect(303, "/dashboard");
},
};
<!-- routes/login/+page.svelte -->
<script lang="ts">
import type { ActionData } from './$types'
import { enhance } from '$app/forms'
let { form }: { form: ActionData } = $props()
</script>
<form method="POST" use:enhance>
<input name="email" value={form?.email ?? ''} />
{#if form?.missing}
<p class="error">所有字段均为必填</p>
{/if}
{#if form?.incorrect}
<p class="error">凭据无效</p>
{/if}
<input name="password" type="password" />
<button>登录</button>
</form>
API 路由
// routes/api/users/+server.ts
import type { RequestHandler } from "./$types";
import { json, error } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get("limit")) || 10;
const users = await prisma.user.findMany({ take: limit });
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.email || !body.name) {
throw error(400, "缺少必填字段");
}
const user = await prisma.user.create({ data: body });
return json(user, { status: 201 });
};
// routes/api/users/[id]/+server.ts
export const GET: RequestHandler = async ({ params }) => {
const user = await prisma.user.findUnique({
where: { id: params.id },
});
if (!user) {
throw error(404, "未找到用户");
}
return json(user);
};
中间件(钩子)
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
// 从 cookie 获取会话
const session = event.cookies.get("session");
if (session) {
const user = await validateSession(session);
event.locals.user = user;
}
// 受保护路由
if (event.url.pathname.startsWith("/dashboard") && !event.locals.user) {
return new Response("重定向", {
status: 303,
headers: { Location: "/login" },
});
}
return resolve(event);
};
事件处理(Svelte 5)
DOM 事件(新语法)
<!-- 旧: on:click -->
<!-- 新: onclick -->
<button onclick={() => count++}>点击我</button>
<input
oninput={(e) => name = e.currentTarget.value}
onkeydown={(e) => e.key === 'Enter' && submit()}
/>
<!-- 事件修饰符:使用 JavaScript -->
<button onclick={(e) => {
e.preventDefault()
e.stopPropagation()
handleClick()
}}>
提交
</button>
<!-- 一次性:使用包装器 -->
<script>
function once(fn) {
return function(e) {
if (!e.target.dataset.clicked) {
e.target.dataset.clicked = 'true'
fn(e)
}
}
}
</script>
<button onclick={once(() => console.log('一次性!'))}>
点击一次
</button>
组件事件(回调属性)
<!-- 旧: createEventDispatcher -->
<!-- 新: 回调属性 -->
<!-- Child.svelte -->
<script lang="ts">
interface Props {
onSelect?: (id: string) => void
onClose?: () => void
}
let { onSelect, onClose }: Props = $props()
</script>
<button onclick={() => onSelect?.('123')}>选择</button>
<button onclick={onClose}>关闭</button>
<!-- Parent.svelte -->
<Child
onSelect={(id) => console.log('已选择:', id)}
onClose={() => console.log('已关闭')}
/>
使用 Vitest 进行测试
设置
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { svelte } from "@sveltejs/vite-plugin-svelte";
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
globals: true,
environment: "jsdom",
include: ["src/**/*.{test,spec}.{js,ts}"],
},
});
组件测试
// tests/Counter.test.ts
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/svelte";
import Counter from "$lib/components/Counter.svelte";
describe("Counter", () => {
it("渲染初始计数", () => {
render(Counter, { props: { initial: 5 } });
expect(screen.getByText("计数: 5")).toBeInTheDocument();
});
it("点击后增加", async () => {
render(Counter);
const button = screen.getByRole("button", { name: /增加/i });
await fireEvent.click(button);
expect(screen.getByText("计数: 1")).toBeInTheDocument();
});
});
测试存储
// tests/stores/counter.test.ts
import { describe, it, expect } from "vitest";
import { counter } from "$lib/stores/counter.svelte";
describe("计数器存储", () => {
it("增加计数", () => {
counter.reset();
expect(counter.count).toBe(0);
counter.increment();
expect(counter.count).toBe(1);
counter.increment();
expect(counter.count).toBe(2);
});
it("减少计数", () => {
counter.reset();
counter.decrement();
expect(counter.count).toBe(-1);
});
});
从 Svelte 4 迁移
| Svelte 4 |
Svelte 5 |
let count = 0 (响应式) |
let count = $state(0) |
$: doubled = count * 2 |
let doubled = $derived(count * 2) |
$: console.log(count) |
$effect(() => console.log(count)) |
export let value |
let { value } = $props() |
on:click={handler} |
onclick={handler} |
<slot /> |
{@render children()} |
createEventDispatcher() |
回调属性 |
迁移脚本
npx sv migrate svelte-5
需要避免的反模式
| 反模式 |
问题 |
解决方案 |
使用 $effect 处理派生值 |
不必要的复杂性 |
使用 $derived |
直接导出 $state |
破坏响应式 |
导出 getter/setter 对象 |
| 不使用 TypeScript |
缺少类型安全 |
启用 lang="ts" |
| 使用旧插槽语法 |
在 Svelte 5 中已弃用 |
使用片段 |
使用 on:event 语法 |
在 Svelte 5 中已弃用 |
使用 onevent 属性 |
性能优势
| 指标 |
改进 |
| 包大小 |
比 React/Vue 小 40-60% |
| 运行时性能 |
无虚拟 DOM 开销 |
| 可交互时间 |
最小化 JavaScript 水合 |
| 内存使用 |
较低,因编译输出 |
相关资源
何时使用此技能
- 使用符文构建新的 Svelte 5 应用程序
- 从 Svelte 4 迁移到 Svelte 5
- 使用 SvelteKit 进行全栈开发
- 创建响应式共享状态
- 实现表单操作和 API 路由
- 使用 Vitest 测试 Svelte 组件