关于 svelte-development
面向 Svelte 开发者的应用开发技能,涵盖 Svelte 5 及常见开发模式,内容包括响应式、组件、状态管理和界面效果。
主要功能
- 01Svelte 5 runes 与响应式
- 02组件结构与组合
- 03Store 与共享状态
- 04生命周期钩子与绑定
- 05过渡效果与动画
- 06表单、数据获取与实践建议
适用场景
- 01构建响应式 Svelte 界面
- 02管理组件间共享状态
- 03添加过渡效果和列表动画
- 04实现表单验证与数据获取
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ manutej/ luxor-claude-marketplace --skill svelte-development安装后在 Claude Code 中输入/svelte-development确认已加载。
附带文件EXAMPLES.mdREADME.md
详细介绍
Svelte 开发技能
本技能根据 Svelte 官方文档,提供使用响应式符文(Svelte 5)、组件、存储、生命周期钩子、过渡效果和动画构建现代 Svelte 应用的全面指导。
何时使用此技能
在以下情况下使用此技能:
- 构建 JavaScript 开销极小的高性能 Web 应用
- 创建带有响应式 UI 的单页应用(SPA)
- 开发经过编译时优化的交互式用户界面
- 构建包体积小的嵌入式小组件和组件
- 实现实时仪表盘和数据可视化
- 创建性能出色的渐进式 Web 应用(PWA)
- 开发具有原生响应性的组件库
- 使用 SvelteKit 构建服务端渲染应用
- 从采用虚拟 DOM 的框架迁移到编译式方案
- 创建易于访问且性能出色的 Web 应用
核心概念
使用符文实现响应性(Svelte 5)
Svelte 5 引入了符文,这是一种声明响应式状态的新方式,提供了更好的 TypeScript 支持和更清晰的语义。
$state 符文:
<script>
let count = $state(0);
let user = $state({ name: 'Alice', age: 30 });
function increment() {
count++;
}
function updateAge() {
user.age++;
}
</script>
<button on:click={increment}>
Count: {count}
</button>
<button on:click={updateAge}>
{user.name} is {user.age} years old
</button>
$derived 符文:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
let quadrupled = $derived(doubled * 2);
// Complex derived values
let users = $state([
{ name: 'Alice', active: true },
{ name: 'Bob', active: false },
{ name: 'Charlie', active: true }
]);
let activeUsers = $derived(users.filter(u => u.active));
let activeCount = $derived(activeUsers.length);
</script>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<p>Quadrupled: {quadrupled}</p>
<p>Active users: {activeCount}</p>
$effect 符文:
<script>
let count = $state(0);
let name = $state('Alice');
// Effect runs when dependencies change
$effect(() => {
console.log(`Count is now ${count}`);
document.title = `Count: ${count}`;
});
// Effect with cleanup
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);
return () => {
clearInterval(interval);
};
});
// Conditional effects
$effect(() => {
if (count > 10) {
console.log('Count exceeded 10!');
}
});
</script>
$props 符文:
<script>
// Type-safe props in Svelte 5
let { name, age = 18, onClick } = $props();
// With TypeScript
interface Props {
name: string;
age?: number;
onClick?: () => void;
}
let { name, age = 18, onClick }: Props = $props();
</script>
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
{#if onClick}
<button on:click={onClick}>Click me</button>
{/if}
</div>
组件
组件是 Svelte 应用的构建模块。每个组件都是一个包含脚本、标记和样式的单独文件。
基本组件结构:
<script>
// Component logic
let name = $state('World');
let count = $state(0);
function handleClick() {
count++;
}
</script>
<div class="container">
<h1>Hello {name}!</h1>
<p>Count: {count}</p>
<button on:click={handleClick}>Increment</button>
</div>
<style>
.container {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
}
h1 {
color: #ff3e00;
font-size: 2rem;
}
button {
background: #ff3e00;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #ff5722;
}
</style>
组件属性:
<script>
let { title, description, imageUrl, onClick } = $props();
</script>
<div class="card" on:click={onClick}>
{#if imageUrl}
<img src={imageUrl} alt={title} />
{/if}
<h3>{title}</h3>
<p>{description}</p>
</div>
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
img {
width: 100%;
border-radius: 4px;
}
</style>
组件事件:
<script>
import { createEventDispatcher } from 'svelte';
let { variant = 'primary', disabled = false } = $props();
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('click', { timestamp: Date.now() });
}
</script>
<button
class="btn {variant}"
{disabled}
on:click={handleClick}
>
<slot />
</button>
<style>
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.primary {
background: #ff3e00;
color: white;
}
.secondary {
background: #676778;
color: white;
}
</style>
<script>
import Button from './Button.svelte';
function handleButtonClick(event) {
console.log('Clicked at:', event.detail.timestamp);
}
</script>
<Button on:click={handleButtonClick}>Click me</Button>
<Button variant="secondary" on:click={handleButtonClick}>Secondary</Button>
插槽与组合:
<script>
let { isOpen = false, onClose } = $props();
</script>
{#if isOpen}
<div class="modal-overlay" on:click={onClose}>
<div class="modal-content" on:click|stopPropagation>
<button class="close-btn" on:click={onClose}>×</button>
<div class="modal-header">
<slot name="header">
<h2>Modal Title</h2>
</slot>
</div>
<div class="modal-body">
<slot>
<p>Modal content goes here</p>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button on:click={onClose}>Close</button>
</slot>
</div>
</div>
</div>
{/if}
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
width: 90%;
position: relative;
}
.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
}
</style>
<script>
import Modal from './Modal.svelte';
let isModalOpen = $state(false);
</script>
<button on:click={() => isModalOpen = true}>Open Modal</button>
<Modal {isModalOpen} onClose={() => isModalOpen = false}>
<svelte:fragment slot="header">
<h2>Custom Title</h2>
</svelte:fragment>
<p>This is custom modal content.</p>
<svelte:fragment slot="footer">
<button on:click={() => isModalOpen = false}>Cancel</button>
<button on:click={handleSave}>Save</button>
</svelte:fragment>
</Modal>
存储
存储是可在多个组件间共享的可观察值。
可写存储:
// stores.js
import { writable } from 'svelte/store';
export const count = writable(0);
export const user = writable({
name: 'Guest',
loggedIn: false
});
export const todos = writable([]);
// Custom store with methods
function createCounter() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
export const counter = createCounter();
在组件中使用存储:
<script>
import { count, user, counter } from './stores.js';
// Auto-subscription with $
$: console.log('Count changed:', $count);
function increment() {
count.update(n => n + 1);
}
function login() {
user.set({ name: 'Alice', loggedIn: true });
}
</script>
<p>Count: {$count}</p>
<button on:click={increment}>Increment</button>
<p>Welcome, {$user.name}!</p>
{#if !$user.loggedIn}
<button on:click={login}>Login</button>
{/if}
<p>Counter: {$counter}</p>
<button on:click={counter.increment}>+</button>
<button on:click={counter.decrement}>-</button>
<button on:click={counter.reset}>Reset</button>
可读存储:
// stores.js
import { readable } from 'svelte/store';
export const time = readable(new Date(), (set) => {
const interval = setInterval(() => {
set(new Date());
}, 1000);
return () => clearInterval(interval);
});
export const mousePosition = readable({ x: 0, y: 0 }, (set) => {
const handleMouseMove = (e) => {
set({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
});
派生存储:
// stores.js
import { writable, derived } from 'svelte/store';
export const todos = writable([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: true },
{ id: 3, text: 'Code review', done: false }
]);
export const completedTodos = derived(
todos,
$todos => $todos.filter(t => t.done)
);
export const activeTodos = derived(
todos,
$todos => $todos.filter(t => !t.done)
);
export const todoStats = derived(
todos,
$todos => ({
total: $todos.length,
completed: $todos.filter(t => t.done).length,
active: $todos.filter(t => !t.done).length
})
);
// Derived from multiple stores
export const firstName = writable('Alice');
export const lastName = writable('Smith');
export const fullName = derived(
[firstName, lastName],
([$firstName, $lastName]) => `${$firstName} ${$lastName}`
);
带复杂逻辑的自定义存储:
// stores/cart.js
import { writable, derived } from 'svelte/store';
function createCart() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
addItem: (item) => update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
);
}
return [...items, { ...item, quantity: 1 }];
}),
removeItem: (id) => update(items =>
items.filter(i => i.id !== id)
),
updateQuantity: (id, quantity) => update(items =>
items.map(i => i.id === id ? { ...i, quantity } : i)
),
clear: () => set([])
};
}
export const cart = createCart();
export const cartTotal = derived(
cart,
$cart => $cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
export const cartItemCount = derived(
cart,
$cart => $cart.reduce((count, item) => count + item.quantity, 0)
);
生命周期钩子
生命周期钩子可让你在组件生命周期的特定时机运行代码。
onMount:
<script>
import { onMount } from 'svelte';
let data = $state([]);
let loading = $state(true);
let error = $state(null);
onMount(async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
data = await response.json();
} catch (err) {
error = err.message;
} finally {
loading = false;
}
});
// onMount with cleanup
onMount(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
return () => {
clearInterval(interval);
};
});
</script>
{#if loading}
<p>Loading...</p>
{:else if error}
<p>Error: {error}</p>
{:else}
<ul>
{#each data as item}
<li>{item.name}</li>
{/each}
</ul>
{/if}
onDestroy:
<script>
import { onDestroy } from 'svelte';
const subscription = eventSource.subscribe(data => {
console.log(data);
});
onDestroy(() => {
subscription.unsubscribe();
});
// Multiple cleanup operations
onDestroy(() => {
console.log('Component is being destroyed');
});
</script>
beforeUpdate 和 afterUpdate:
<script>
import { beforeUpdate, afterUpdate } from 'svelte';
let div;
let autoscroll = $state(true);
beforeUpdate(() => {
if (div) {
const scrollableDistance = div.scrollHeight - div.offsetHeight;
autoscroll = div.scrollTop > scrollableDistance - 20;
}
});
afterUpdate(() => {
if (autoscroll) {
div.scrollTo(0, div.scrollHeight);
}
});
</script>
<div bind:this={div}>
</div>
tick:
<script>
import { tick } from 'svelte';
let text = $state('');
let textarea;
async function handleKeydown(event) {
if (event.key === 'Tab') {
event.preventDefault();
const { selectionStart, selectionEnd, value } = textarea;
text = value.slice(0, selectionStart) + '\t' + value.slice(selectionEnd);
// Wait for DOM to update
await tick();
// Set cursor position
textarea.selectionStart = textarea.selectionEnd = selectionStart + 1;
}
}
</script>
<textarea
bind:value={text}
bind:this={textarea}
on:keydown={handleKeydown}
/>
过渡效果和动画
Svelte 提供内置的过渡效果和动画,可实现流畅的 UI 效果。
内置过渡效果:
<script>
import { fade, fly, slide, scale, blur } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
let visible = $state(true);
</script>
<button on:click={() => visible = !visible}>Toggle</button>
{#if visible}
<div transition:fade>Fades in and out</div>
<div transition:fly={{ y: 200, duration: 500 }}>
Flies in and out
</div>
<div transition:slide={{ duration: 300 }}>
Slides in and out
</div>
<div transition:scale={{
duration: 500,
start: 0.5,
easing: quintOut
}}>
Scales in and out
</div>
<div transition:blur={{ duration: 300 }}>
Blurs in and out
</div>
{/if}
进入和退出过渡效果:
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(true);
</script>
{#if visible}
<div
in:fly={{ y: -200, duration: 500 }}
out:fade={{ duration: 200 }}
>
Different in/out transitions
</div>
{/if}
自定义过渡效果:
<script>
import { cubicOut } from 'svelte/easing';
function typewriter(node, { speed = 1 }) {
const valid = node.childNodes.length === 1 &&
node.childNodes[0].nodeType === Node.TEXT_NODE;
if (!valid) return {};
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: t => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
function spin(node, { duration }) {
return {
duration,
css: t => {
const eased = cubicOut(t);
return `
transform: scale(${eased}) rotate(${eased * 360}deg);
opacity: ${eased};
`;
}
};
}
let visible = $state(false);
</script>
{#if visible}
<p transition:typewriter={{ speed: 1 }}>
This text will type out character by character
</p>
<div transition:spin={{ duration: 600 }}>
Spinning!
</div>
{/if}
动画:
<script>
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
let todos = $state([
{ id: 1, text: 'Buy milk' },
{ id: 2, text: 'Walk dog' },
{ id: 3, text: 'Code review' }
]);
function shuffle() {
todos = todos.sort(() => Math.random() - 0.5);
}
</script>
<button on:click={shuffle}>Shuffle</button>
<ul>
{#each todos as todo (todo.id)}
<li animate:flip={{ duration: 300, easing: quintOut }}>
{todo.text}
</li>
{/each}
</ul>
延迟过渡效果:
<script>
import { quintOut } from 'svelte/easing';
import { crossfade } from 'svelte/transition';
const [send, receive] = crossfade({
duration: d => Math.sqrt(d * 200),
fallback(node, params) {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 600,
easing: quintOut,
css: t => `
transform: ${transform} scale(${t});
opacity: ${t}
`
};
}
});
let todos = $state([
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: true }
]);
function toggleDone(id) {
todos = todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
}
</script>
<div class="board">
<div class="column">
<h2>Todo</h2>
{#each todos.filter(t => !t.done) as todo (todo.id)}
<div
class="card"
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
on:click={() => toggleDone(todo.id)}
>
{todo.text}
</div>
{/each}
</div>
<div class="column">
<h2>Done</h2>
{#each todos.filter(t => t.done) as todo (todo.id)}
<div
class="card"
in:receive={{ key: todo.id }}
out:send={{ key: todo.id }}
on:click={() => toggleDone(todo.id)}
>
{todo.text}
</div>
{/each}
</div>
</div>
绑定
Svelte 提供强大的双向绑定功能。
输入绑定:
<script>
let name = $state('');
let age = $state(0);
let message = $state('');
let selected = $state('');
let checked = $state(false);
let group = $state([]);
</script>
<input bind:value={name} placeholder="Enter name" />
<p>Hello {name}!</p>
<input type="number" bind:value={age} />
<p>Age: {age}</p>
<textarea bind:value={message}></textarea>
<p>Message length: {message.length}</p>
<select bind:value={selected}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<p>Selected: {selected}</p>
<input type="checkbox" bind:checked={checked} />
<p>Checked: {checked}</p>
<input type="checkbox" bind:group={group} value="apple" /> Apple
<input type="checkbox" bind:group={group} value="banana" /> Banana
<input type="checkbox" bind:group={group} value="orange" /> Orange
<p>Selected: {group.join(', ')}</p>
组件绑定:
<script>
let { value = '' } = $props();
</script>
<input bind:value />
<script>
import Input from './Input.svelte';
let name = $state('');
</script>
<Input bind:value={name} />
<p>Name: {name}</p>
元素绑定:
<script>
let canvas;
let video;
let div;
let clientWidth = $state(0);
let clientHeight = $state(0);
let offsetWidth = $state(0);
onMount(() => {
const ctx = canvas.getContext('2d');
// Draw on canvas
});
</script>
<canvas bind:this={canvas} width={400} height={300}></canvas>
<video bind:this={video} bind:currentTime bind:duration bind:paused>
<source src="video.mp4" />
</video>
<div bind:clientWidth bind:clientHeight bind:offsetWidth bind:this={div}>
Size: {clientWidth} × {clientHeight}
</div>
contenteditable 绑定:
<script>
let html = $state('<p>Edit me!</p>');
</script>
<div contenteditable="true" bind:innerHTML={html}></div>
<pre>{html}</pre>
API 参考
符文(Svelte 5)
$state(initialValue)
- 创建响应式状态
- 返回一个响应式变量
- 修改状态会自动触发更新
$derived(expression)
- 创建派生的响应式值
- 自动跟踪依赖项
- 依赖项发生变化时重新计算
$effect(callback)
- 依赖项发生变化时运行副作用
- 可以返回清理函数
- 自动跟踪依赖项
$props()
- 声明组件属性
- 支持解构和默认值
- 提供 TypeScript 类型安全
存储函数
writable(initialValue, start?)
- 创建可写存储
- 返回 { subscribe, set, update }
- 可选的 start 函数用于初始化
readable(initialValue, start)
- 创建只读存储
- 返回 { subscribe }
- 必须提供 start 函数
derived(stores, callback, initialValue?)
- 创建派生存储
- 依赖一个或多个存储
- 自动更新
get(store)
- 无需订阅即可获取当前值
- 请谨慎使用(优先使用 $store 语法)
生命周期函数
onMount(callback)
- 组件首次渲染后运行
- 可以返回清理函数
- 适用于获取数据和订阅
onDestroy(callback)
- 组件销毁前运行
- 用于执行清理操作
beforeUpdate(callback)
- DOM 更新前运行
- 可访问之前的状态
afterUpdate(callback)
- DOM 更新后运行
- 适用于操作 DOM
tick()
- 返回一个在状态更改后解析的 Promise
- 确保 DOM 已更新
过渡函数
fade(node, params)
- 淡入/淡出元素
- 参数:{ delay, duration, easing }
fly(node, params)
- 让元素飞入/飞出
- 参数:{ delay, duration, easing, x, y, opacity }
slide(node, params)
- 让元素滑入/滑出
- 参数:{ delay, duration, easing }
scale(node, params)
- 放大/缩小元素
- 参数:{ delay, duration, easing, start, opacity }
blur(node, params)
- 模糊/清晰显示元素
- 参数:{ delay, duration, easing, amount, opacity }
crossfade(params)
- 创建发送/接收过渡对
- 适合在列表间移动元素
动画函数
flip(node, animation, params)
- 为位置变化添加动画
- 与 each 块搭配使用
- 参数:{ delay, duration, easing }
工作流模式
组件组合
容器/展示组件模式:
<script>
import TodoList from './TodoList.svelte';
import { todos } from './stores.js';
function addTodo(text) {
todos.update(list => [...list, {
id: Date.now(),
text,
done: false
}]);
}
function toggleTodo(id) {
todos.update(list => list.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
}
function deleteTodo(id) {
todos.update(list => list.filter(t => t.id !== id));
}
</script>
<TodoList
todos={$todos}
onAdd={addTodo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<script>
let { todos, onAdd, onToggle, onDelete } = $props();
let newTodo = $state('');
function handleSubmit() {
if (newTodo.trim()) {
onAdd(newTodo);
newTodo = '';
}
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<input bind:value={newTodo} placeholder="Add todo" />
<button type="submit">Add</button>
</form>
<ul>
{#each todos as todo}
<li>
<input
type="checkbox"
checked={todo.done}
on:change={() => onToggle(todo.id)}
/>
<span class:done={todo.done}>{todo.text}</span>
<button on:click={() => onDelete(todo.id)}>Delete</button>
</li>
{/each}
</ul>
<style>
.done {
text-decoration: line-through;
opacity: 0.6;
}
</style>
状态管理
Context API 模式:
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
const user = writable({ name: 'Alice', role: 'admin' });
const theme = writable('light');
setContext('user', user);
setContext('theme', theme);
</script>
<slot />
<script>
import { getContext } from 'svelte';
const user = getContext('user');
const theme = getContext('theme');
</script>
<div class={$theme}>
<p>Welcome, {$user.name}!</p>
<p>Role: {$user.role}</p>
</div>
表单验证
带验证的表单:
<script>
let formData = $state({
email: '',
password: '',
confirmPassword: ''
});
let errors = $state({});
let touched = $state({});
let isSubmitting = $state(false);
function validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
function validateForm() {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'Invalid email address';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
}
function handleBlur(field) {
touched[field] = true;
errors = validateForm();
}
async function handleSubmit() {
touched = { email: true, password: true, confirmPassword: true };
errors = validateForm();
if (Object.keys(errors).length === 0) {
isSubmitting = true;
try {
await submitForm(formData);
// Success
} catch (error) {
errors.submit = error.message;
} finally {
isSubmitting = false;
}
}
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<div class="field">
<label for="email">Email</label>
<input
id="email"
type="email"
bind:value={formData.email}
on:blur={() => handleBlur('email')}
class:error={touched.email && errors.email}
/>
{#if touched.email && errors.email}
<span class="error-message">{errors.email}</span>
{/if}
</div>
<div class="field">
<label for="password">Password</label>
<input
id="password"
type="password"
bind:value={formData.password}
on:blur={() => handleBlur('password')}
class:error={touched.password && errors.password}
/>
{#if touched.password && errors.password}
<span class="error-message">{errors.password}</span>
{/if}
</div>
<div class="field">
<label for="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
bind:value={formData.confirmPassword}
on:blur={() => handleBlur('confirmPassword')}
class:error={touched.confirmPassword && errors.confirmPassword}
/>
{#if touched.confirmPassword && errors.confirmPassword}
<span class="error-message">{errors.confirmPassword}</span>
{/if}
</div>
{#if errors.submit}
<div class="error-message">{errors.submit}</div>
{/if}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
<style>
.field {
margin-bottom: 1rem;
}
input.error {
border-color: red;
}
.error-message {
color: red;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
数据获取
带加载状态的数据获取:
<script>
import { onMount } from 'svelte';
let data = $state([]);
let loading = $state(true);
let error = $state(null);
async function fetchData() {
loading = true;
error = null;
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
data = await response.json();
} catch (err) {
error = err.message;
} finally {
loading = false;
}
}
onMount(fetchData);
</script>
{#if loading}
<div class="spinner">Loading...</div>
{:else if error}
<div class="error">
<p>Error: {error}</p>
<button on:click={fetchData}>Retry</button>
</div>
{:else}
<ul>
{#each data as item}
<li>{item.name}</li>
{/each}
</ul>
{/if}
最佳实践
1. 使用 Runes 实现响应性(Svelte 5)
优先使用 runes,而不是旧式响应式声明:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`Count: ${count}`);
});
</script>
<script>
let count = 0;
$: doubled = count * 2;
$: {
console.log(`Count: ${count}`);
}
</script>
2. 组件组织
让组件职责明确且专注于单一用途:
<script>
let { variant = 'primary', onClick } = $props();
</script>
<button class={variant} on:click={onClick}>
<slot />
</button>
<script>
// Button that also handles data fetching, validation, etc.
</script>
3. Store 的使用
共享状态使用 stores,组件专属数据使用局部状态:
<script>
import { user } from './stores.js'; // Shared state
let localCount = $state(0); // Component-specific
</script>
<script>
import { count } from './stores.js'; // Only used in one component
</script>
4. 无障碍访问
始终添加适当的 ARIA 属性并支持键盘操作:
<button
on:click={handleClick}
aria-label="Close dialog"
aria-pressed={isPressed}
>
Close
</button>
<input
type="text"
aria-label="Search"
aria-describedby="search-help"
/>
<span id="search-help">Enter keywords to search</span>
5. 性能优化
列表使用带键的 each 块:
{#each items as item (item.id)}
<Item {item} />
{/each}
{#each items as item}
<Item {item} />
{/each}
6. TypeScript 集成
使用 TypeScript 确保类型安全:
<script lang="ts">
interface User {
name: string;
age: number;
email?: string;
}
interface Props {
user: User;
onUpdate?: (user: User) => void;
}
let { user, onUpdate }: Props = $props();
</script>
7. CSS 作用域
充分利用 Svelte 的作用域样式:
<style>
/* Scoped to this component by default */
.container {
padding: 1rem;
}
/* Global styles when needed */
:global(body) {
margin: 0;
}
/* Combining scoped and global */
.container :global(p) {
color: blue;
}
</style>
8. 事件修饰符
使用事件修饰符让代码更简洁:
<form on:submit|preventDefault={handleSubmit}>
<div on:click|stopPropagation={handleClick}>
<button on:click|once={handleClick}>
<div on:click|capture={handleClick}>
<div on:click|self={handleClick}>
<div on:scroll|passive={handleScroll}>
<div on:wheel|nonpassive={handleWheel}>
9. 组件通信
使用事件实现从子组件到父组件的通信:
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function notify() {
dispatch('message', { text: 'Hello!' });
}
</script>
<Child on:message={handleMessage} />
10. 错误边界
妥善处理错误:
<script>
import { onDestroy } from 'svelte';
let error = $state(null);
function handleError(err) {
error = err.message;
console.error(err);
}
// Global error handler
const errorHandler = (event) => {
handleError(event.error);
};
if (typeof window !== 'undefined') {
window.addEventListener('error', errorHandler);
}
onDestroy(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('error', errorHandler);
}
});
</script>
{#if error}
<div class="error-boundary">
<h2>Something went wrong</h2>
<p>{error}</p>
<button on:click={() => error = null}>Try again</button>
</div>
{:else}
<slot />
{/if}
总结
这项 Svelte 开发技能涵盖:
- 使用 Runes 实现响应性:$state、$derived、$effect、$props
- 组件:结构、props、事件、插槽
- Stores:可写、可读、派生、自定义 stores
- 生命周期:onMount、onDestroy、beforeUpdate、afterUpdate、tick
- 过渡:内置和自定义过渡
- 动画:FLIP 动画、crossfade
- 绑定:输入、组件、元素绑定
- 工作流模式:组件组合、状态管理、表单、数据获取
- 最佳实践:性能、无障碍访问、TypeScript、CSS 作用域
- 实际应用模式:待办事项应用、模态框、带验证的表单
所有模式均基于使用 runes 的 Svelte 5,体现了现代 Svelte 开发实践,侧重于编译时优化和响应式编程。