mcpField
S
svelte-development

svelte-development

github.com/manutej开发62435 次安装39 周前更新
代码仓库

关于 svelte-development

面向 Svelte 开发者的应用开发技能,涵盖 Svelte 5 及常见开发模式,内容包括响应式、组件、状态管理和界面效果。

主要功能

  1. 01Svelte 5 runes 与响应式
  2. 02组件结构与组合
  3. 03Store 与共享状态
  4. 04生命周期钩子与绑定
  5. 05过渡效果与动画
  6. 06表单、数据获取与实践建议

适用场景

  1. 01构建响应式 Svelte 界面
  2. 02管理组件间共享状态
  3. 03添加过渡效果和列表动画
  4. 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 开发技能涵盖:

  1. 使用 Runes 实现响应性:$state、$derived、$effect、$props
  2. 组件:结构、props、事件、插槽
  3. Stores:可写、可读、派生、自定义 stores
  4. 生命周期:onMount、onDestroy、beforeUpdate、afterUpdate、tick
  5. 过渡:内置和自定义过渡
  6. 动画:FLIP 动画、crossfade
  7. 绑定:输入、组件、元素绑定
  8. 工作流模式:组件组合、状态管理、表单、数据获取
  9. 最佳实践:性能、无障碍访问、TypeScript、CSS 作用域
  10. 实际应用模式:待办事项应用、模态框、带验证的表单

所有模式均基于使用 runes 的 Svelte 5,体现了现代 Svelte 开发实践,侧重于编译时优化和响应式编程。

评论

支持 Markdown