DevToolBox免费
博客

Svelte指南:响应式、Store、SvelteKit和Svelte 5 Runes

13 分钟阅读作者 DevToolBox

Svelte 是一种构建用户界面的创新方法。与 React 或 Vue 不同,Svelte 将工作从浏览器转移到编译步骤,生成高度优化的原生 JavaScript,没有虚拟 DOM 开销。随着 Svelte 5 引入 runes 和 SvelteKit 提供全栈框架,Svelte 已成为 2026 年现代 Web 开发的引人注目的选择。

TL;DR

Svelte 在构建时将组件编译为高效的命令式代码,消除虚拟 DOM diff。其响应式系统内置于语言中,包括响应式声明($:)和 Svelte 5 runes($state、$derived、$effect)。SvelteKit 提供基于文件的路由、SSR、表单 actions 和渐进增强。

Key Takeaways
  • Svelte 编译为原生 JS,没有运行时框架开销
  • 响应式是内置于语言的,而非库 API
  • Svelte 5 runes 提供细粒度的通用响应式
  • SvelteKit 提供 SSR、基于文件的路由和表单 actions
  • 过渡和动画是内置指令的一等公民
  • Store 提供简单的、框架集成的状态管理

Svelte 响应式系统

Svelte 在编译时跟踪响应式依赖。当你给 let 声明的变量赋值时,Svelte 会自动更新 DOM。响应式声明($:)让你定义计算值和副作用。

响应式声明与语句

<script>
  let count = 0;

  // Reactive declaration: recomputes when count changes
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;

  // Reactive statement: runs side effect
  $: if (count >= 10) {
    console.log("Count reached 10!");
    count = 0;
  }

  // Reactive block
  $: {
    console.log("count is", count);
    console.log("doubled is", doubled);
  }

  function increment() {
    count += 1; // assignment triggers reactivity
  }
</script>

<button on:click={increment}>
  Clicked {count} times
</button>
<p>{count} x 2 = {doubled}</p>
<p>{count} x 4 = {quadrupled}</p>

响应式赋值

在 Svelte 中,赋值触发响应式。编译器会对每个赋值进行插桩以调度 DOM 更新。数组和对象变更需要重新赋值才能触发更新。

<script>
  let items = ["apple", "banana"];

  function addItem() {
    // Reassignment triggers reactivity
    items = [...items, "cherry"];

    // This will NOT trigger reactivity:
    // items.push("cherry");

    // Idiomatic: push then reassign
    items.push("date");
    items = items; // trigger update
  }

  let user = { name: "Alice", age: 30 };

  function birthday() {
    user.age += 1; // property assignment works
  }
</script>

组件:Props、事件、Slots 与生命周期

Svelte 组件写在 .svelte 文件中,将 HTML、CSS 和 JavaScript 组合在一个文件中。Props 向下传递,事件向上冒泡,slots 允许内容组合。

组件 Props

<!-- UserCard.svelte -->
<script>
  export let name;         // required prop
  export let age = 25;     // optional with default
  export let role = "user";
</script>

<div class="card">
  <h3>{name}</h3>
  <p>Age: {age} | Role: {role}</p>
</div>

<style>
  .card { padding: 1rem; border: 1px solid #ddd; }
</style>

<!-- Parent.svelte -->
<script>
  import UserCard from "./UserCard.svelte";
</script>

<UserCard name="Alice" age={30} role="admin" />
<UserCard name="Bob" />

自定义事件

组件使用 createEventDispatcher 分发自定义事件。父组件使用 on: 指令监听。

<!-- SearchInput.svelte -->
<script>
  import { createEventDispatcher } from "svelte";
  const dispatch = createEventDispatcher();
  let query = "";

  function handleInput() {
    dispatch("search", { query });
  }
</script>

<input bind:value={query} on:input={handleInput}
  placeholder="Search..." />

<!-- App.svelte -->
<script>
  import SearchInput from "./SearchInput.svelte";
  function onSearch(event) {
    console.log("Searching:", event.detail.query);
  }
</script>
<SearchInput on:search={onSearch} />

Slots 与命名 Slots

Slots 允许父组件向子组件注入内容。命名 slots 提供多个插入点。

<!-- Modal.svelte -->
<script>
  export let title = "Dialog";
</script>

<div class="modal-backdrop">
  <div class="modal">
    <header>
      <slot name="header"><h2>{title}</h2></slot>
    </header>
    <main><slot /></main>
    <footer>
      <slot name="footer">
        <button>Close</button>
      </slot>
    </footer>
  </div>
</div>

<!-- Usage -->
<Modal>
  <h2 slot="header">Confirm Delete</h2>
  <p>Are you sure?</p>
  <div slot="footer">
    <button>Cancel</button>
    <button>Delete</button>
  </div>
</Modal>

生命周期函数

Svelte 提供 onMount、onDestroy、beforeUpdate、afterUpdate 和 tick 来管理组件生命周期。

<script>
  import { onMount, onDestroy, beforeUpdate,
    afterUpdate, tick } from "svelte";
  let data = null;

  onMount(async () => {
    const res = await fetch("/api/data");
    data = await res.json();
    return () => { /* cleanup */ };
  });

  onDestroy(() => { /* remove listeners */ });
  beforeUpdate(() => { /* before DOM update */ });
  afterUpdate(() => { /* after DOM update */ });

  async function handleClick() {
    data = newValue;
    await tick(); // wait for DOM to update
  }
</script>

Svelte Stores

Store 是 Svelte 内置的状态管理方案。它们是可订阅和更新的响应式对象。自动订阅语法($store)使其使用非常流畅。

可写 Store

// stores.js
import { writable } from "svelte/store";

export const count = writable(0);
export const user = writable({ name: "", loggedIn: false });

// Component.svelte
<script>
  import { count, user } from "./stores.js";

  // $ prefix auto-subscribes and unsubscribes
  function increment() {
    $count += 1;
    // equivalent to: count.update(n => n + 1);
  }
</script>

<p>Count: {$count}</p>
<p>User: {$user.name}</p>
<button on:click={increment}>+1</button>

可读 Store 与派生 Store

import { readable, derived } from "svelte/store";

// Readable store: external data source
export const time = readable(new Date(), (set) => {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);
  return () => clearInterval(interval);
});

// Derived store: computed from other stores
export const elapsed = derived(time, ($time) =>
  Math.round(($time.getTime() - start) / 1000)
);

// Derived from multiple stores
export const summary = derived(
  [count, user],
  ([$count, $user]) =>
    `\${$user.name} clicked \${$count} times`
);

自定义 Store

自定义 store 通过包装可写 store 并暴露受控 API 来封装逻辑。

import { writable } from "svelte/store";

function createTodoStore() {
  const { subscribe, update, set } = writable([]);
  return {
    subscribe,
    add: (text) => update(todos =>
      [...todos, { id: Date.now(), text, done: false }]
    ),
    toggle: (id) => update(todos =>
      todos.map(t =>
        t.id === id ? { ...t, done: !t.done } : t
      )
    ),
    remove: (id) => update(todos =>
      todos.filter(t => t.id !== id)
    ),
    reset: () => set([])
  };
}

export const todos = createTodoStore();

SvelteKit:路由、布局与 Load 函数

SvelteKit 是 Svelte 的官方全栈框架,提供基于文件的路由、服务端渲染、API 路由和强大的 load 函数数据获取系统。

基于文件的路由

SvelteKit 使用文件系统进行路由。src/routes 中的每个 +page.svelte 文件都成为一个页面。

src/routes/
  +page.svelte          → /
  +layout.svelte        → shared layout
  about/+page.svelte    → /about
  blog/
    +page.svelte        → /blog
    [slug]/
      +page.svelte      → /blog/:slug
      +page.server.ts   → server load
  api/users/
    +server.ts          → /api/users
  (auth)/
    login/+page.svelte  → /login
    register/+page.svelte → /register

布局

布局包裹页面并在导航间持久存在。嵌套布局允许在不同路由级别共享 UI。

<!-- src/routes/+layout.svelte -->
<script>
  import Header from "$lib/Header.svelte";
  import Footer from "$lib/Footer.svelte";
</script>

<Header />
<main><slot /></main>
<Footer />

<!-- src/routes/dashboard/+layout.svelte -->
<script>
  import Sidebar from "$lib/Sidebar.svelte";
</script>
<div class="dashboard">
  <Sidebar />
  <div class="content"><slot /></div>
</div>

Load 函数

Load 函数在渲染前获取数据。它们在 SSR 期间在服务器上运行,在客户端导航时在浏览器中运行。

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from "./$types";
import { error } from "@sveltejs/kit";

export const load: PageServerLoad = async ({
  params, fetch
}) => {
  const res = await fetch(
    `/api/posts/\${params.slug}`
  );
  if (!res.ok) throw error(404, "Not found");
  return { post: await res.json() };
};

// src/routes/blog/[slug]/+page.svelte
<script>
  export let data;
</script>
<article>
  <h1>{data.post.title}</h1>
  <div>{@html data.post.content}</div>
</article>

表单 Actions 与渐进增强

SvelteKit 表单 actions 在服务器上处理表单提交。它们无需 JavaScript 即可工作,并可通过 use:enhance 渐进增强。

// src/routes/login/+page.server.ts
import { fail, redirect } from "@sveltejs/kit";

export const actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = data.get("email");
    const password = data.get("password");

    if (!email)
      return fail(400, { email, missing: true });

    const user = await authenticate(email, password);
    if (!user)
      return fail(401, { email, incorrect: true });

    cookies.set("session", user.token, { path: "/" });
    throw redirect(303, "/dashboard");
  }
};

// src/routes/login/+page.svelte
<script>
  import { enhance } from "$app/forms";
  export let form;
</script>

<form method="POST" use:enhance>
  <input name="email" value={form?.email ?? ""} />
  {#if form?.missing}
    <p class="error">Email is required</p>
  {/if}
  <input name="password" type="password" />
  <button>Log In</button>
</form>

过渡与动画

Svelte 通过指令内置支持过渡和动画。元素可以用最少的代码平滑地进入和离开 DOM。

内置过渡

<script>
  import { fade, fly, slide, scale }
    from "svelte/transition";
  import { quintOut } from "svelte/easing";
  let visible = true;
</script>

<button on:click={() => visible = !visible}>
  Toggle
</button>

{#if visible}
  <div transition:fade={{ duration: 300 }}>
    Fades in and out
  </div>

  <div transition:fly={{ y: 200, duration: 500,
    easing: quintOut }}>
    Flies in from below
  </div>

  <div in:fly={{ x: -200 }} out:fade>
    Flies in, fades out
  </div>

  <div transition:slide={{ duration: 300 }}>
    Slides open and closed
  </div>
{/if}

自定义过渡

通过返回包含 css 或 tick 函数的对象来创建自定义过渡。

<script>
  function typewriter(node, { speed = 1 }) {
    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);
      }
    };
  }
</script>

{#if visible}
  <p transition:typewriter={{ speed: 2 }}>
    This text types itself out!
  </p>
{/if}

animate 指令

animate 指令在键控 each 块中元素位置变化时平滑移动元素。

<script>
  import { flip } from "svelte/animate";
  import { fade } from "svelte/transition";
  let list = [1, 2, 3, 4, 5];

  function shuffle() {
    list = list.sort(() => Math.random() - 0.5);
  }
</script>

<button on:click={shuffle}>Shuffle</button>
{#each list as item (item)}
  <div animate:flip={{ duration: 300 }}
    transition:fade>{item}</div>
{/each}

Actions(use: 指令)

Actions 是元素挂载到 DOM 时运行的函数。它们适用于集成第三方库、添加事件监听器或实现可复用的 DOM 行为。

<script>
  // Tooltip action
  function tooltip(node, text) {
    let tip;
    function show() {
      tip = document.createElement("div");
      tip.textContent = text;
      tip.style.cssText = `position:absolute;
        background:#333;color:#fff;padding:4px 8px;
        border-radius:4px;font-size:12px`;
      node.appendChild(tip);
    }
    function hide() { tip?.remove(); }

    node.addEventListener("mouseenter", show);
    node.addEventListener("mouseleave", hide);

    return {
      update(newText) { text = newText; },
      destroy() {
        tip?.remove();
        node.removeEventListener("mouseenter", show);
        node.removeEventListener("mouseleave", hide);
      }
    };
  }

  // Click-outside action
  function clickOutside(node, callback) {
    function onClick(e) {
      if (!node.contains(e.target)) callback();
    }
    document.addEventListener("click", onClick, true);
    return {
      destroy() {
        document.removeEventListener("click",
          onClick, true);
      }
    };
  }
</script>

<button use:tooltip={"Click me!"}>Hover</button>
<div use:clickOutside={() => open = false}>
  Dropdown content
</div>

Context API

Context API 在组件树中传递数据而无需 prop 逐层传递。与 store 不同,context 作用域限定于组件子树。

<!-- ThemeProvider.svelte -->
<script>
  import { setContext } from "svelte";
  import { writable } from "svelte/store";

  const theme = writable("light");
  setContext("theme", {
    theme,
    toggle: () => theme.update(t =>
      t === "light" ? "dark" : "light"
    )
  });
</script>
<slot />

<!-- DeepChild.svelte -->
<script>
  import { getContext } from "svelte";
  const { theme, toggle } = getContext("theme");
</script>
<p>Current theme: {$theme}</p>
<button on:click={toggle}>Toggle Theme</button>

Svelte 5 Runes

Svelte 5 引入了 runes,一组编译器级别的原语,提供通用的细粒度响应式。Runes 替代了响应式声明($:)、export let props 和许多 store 用例。

$state 与 $derived

<script>
  // $state: reactive state (replaces let)
  let count = $state(0);
  let items = $state(["apple", "banana"]);

  // $derived: computed value (replaces $:)
  let doubled = $derived(count * 2);
  let total = $derived(items.length);

  // Complex derived expression
  let summary = $derived.by(() => {
    if (count > 10) return "Many clicks";
    if (count > 5) return "Some clicks";
    return "Few clicks";
  });

  // Deep reactivity in Svelte 5
  function addItem() {
    items.push("cherry"); // works in Svelte 5!
  }
</script>

<button onclick={() => count++}>
  {count} (doubled: {doubled})
</button>
<p>{summary} — {total} items</p>

$effect

$effect rune 替代响应式语句和 onMount 用于副作用。它自动跟踪依赖并在变化时重新运行。

<script>
  let count = $state(0);
  let title = $state("My App");

  // Auto-tracks dependencies
  $effect(() => {
    document.title = `\${title} (\${count})`;
  });

  // With cleanup
  $effect(() => {
    const id = setInterval(() => count++, 1000);
    return () => clearInterval(id);
  });

  // Pre-effect (runs before DOM update)
  $effect.pre(() => {
    console.log("about to update DOM");
  });
</script>

$props

$props rune 替代 export let 来在 Svelte 5 中声明组件 props。

<!-- Svelte 5 component with $props -->
<script>
  // Destructure with defaults
  let { name, age = 25, role = "user", ...rest }
    = $props();

  // With TypeScript:
  // interface Props {
  //   name: string;
  //   age?: number;
  //   role?: "user" | "admin";
  // }
  // let { name, age = 25, role = "user" }
  //   = $props<Props>();
</script>

<div {...rest}>
  <h3>{name}</h3>
  <p>Age: {age} | Role: {role}</p>
</div>

服务端渲染与 Hydration

SvelteKit 默认在服务器端渲染页面并在客户端进行 hydration。这提供了快速的初始加载、良好的 SEO 和渐进增强。

// src/routes/+page.server.ts
export const load = async ({ fetch }) => {
  const res = await fetch("/api/products");
  return { products: await res.json() };
};

// Page options
export const prerender = true;  // static at build
export const ssr = true;        // default: SSR on
export const csr = true;        // default: hydrate

// +page.svelte
<script>
  import { browser } from "$app/environment";
  export let data;
  if (browser) {
    console.log("Client-side hydrated!");
  }
</script>

{#each data.products as product}
  <div>{product.name} — ${product.price}</div>
{/each}

组件组合模式

Svelte 支持多种组合模式来构建可复用、可维护的组件架构。

<!-- Compound Components: Tabs -->
<!-- Tabs.svelte -->
<script>
  import { setContext } from "svelte";
  import { writable } from "svelte/store";
  const activeTab = writable(0);
  setContext("tabs", { activeTab });
</script>
<div class="tabs"><slot /></div>

<!-- Tab.svelte -->
<script>
  import { getContext } from "svelte";
  export let index;
  const { activeTab } = getContext("tabs");
</script>
<button on:click={() => $activeTab = index}
  class:active={$activeTab === index}>
  <slot />
</button>

<!-- TabPanel.svelte -->
<script>
  import { getContext } from "svelte";
  export let index;
  const { activeTab } = getContext("tabs");
</script>
{#if $activeTab === index}
  <div class="panel"><slot /></div>
{/if}

<!-- Usage -->
<Tabs>
  <Tab index={0}>General</Tab>
  <Tab index={1}>Settings</Tab>
  <TabPanel index={0}>General content</TabPanel>
  <TabPanel index={1}>Settings content</TabPanel>
</Tabs>

使用 Vitest 和 Playwright 测试

Svelte 组件可以使用 Vitest 进行单元和集成测试,使用 Playwright 进行端到端测试。

// counter.test.ts (Vitest + testing-library)
import { render, fireEvent } from
  "@testing-library/svelte";
import { describe, it, expect } from "vitest";
import Counter from "./Counter.svelte";

describe("Counter", () => {
  it("increments on click", async () => {
    const { getByText } = render(Counter,
      { props: { initial: 0 } });
    const btn = getByText("Count: 0");
    await fireEvent.click(btn);
    expect(getByText("Count: 1")).toBeTruthy();
  });
});

// e2e/home.test.ts (Playwright)
import { test, expect } from "@playwright/test";

test("home page loads", async ({ page }) => {
  await page.goto("/");
  await expect(page.locator("h1"))
    .toContainText("Welcome");
  await page.click("button:text(\"Increment\")");
  await expect(page.locator("[data-count]"))
    .toHaveAttribute("data-count", "1");
});

性能对比

由于编译时方法和没有虚拟 DOM 开销,Svelte 在基准测试中始终优于 React 和 Vue。

指标SvelteReactVue
Bundle Size (min+gzip)1.6 KB44+ KB34+ KB
Runtime OverheadNone (compiled)Virtual DOMVirtual DOM
Startup Time (TTI)~50ms~120ms~100ms
Memory UsageLowMedium-HighMedium
DOM Update SpeedDirect mutationReconciliationProxy-based
JS Benchmark Score1.021.381.21
TodoMVC Lines~60~120~90
Learning CurveGentleModerateGentle-Moderate
Ecosystem SizeGrowingLargestLarge
TypeScriptFirst-classFirst-classFirst-class

最佳实践

  • 使用响应式声明($:)来计算值,而非手动更新
  • 保持组件小而聚焦于单一职责
  • 使用 store 进行跨组件状态管理,使用 context 进行子树状态管理
  • 利用 SvelteKit 表单 actions 实现渐进增强
  • 谨慎使用过渡以避免晕动症问题
  • 在 Svelte 5 中优先使用 $derived 而非 $effect 来计算值
  • 使用 load 函数获取数据,而非 onMount
  • 使用 export const prerender = true 为静态内容启用预渲染

常见问题

Svelte 比 React 好吗?

与 React 相比,Svelte 提供更小的包体积、更快的运行时性能和更简单的语法。但 React 拥有更大的生态系统和更多的就业机会。Svelte 非常适合性能关键型应用和重视开发体验的团队。

Svelte 和 SvelteKit 有什么区别?

Svelte 是组件框架和编译器。SvelteKit 是构建在 Svelte 之上的全栈应用框架,提供路由、SSR、API 路由、表单 actions 和部署适配器。SvelteKit 之于 Svelte 就像 Next.js 之于 React。

Svelte 没有虚拟 DOM 如何实现响应式?

Svelte 在构建时将组件编译为命令式 JavaScript。编译器分析代码,识别响应式依赖,并生成精确的 DOM 更新。当响应式变量改变时,只有依赖它的特定 DOM 节点会被直接更新。

什么是 Svelte 5 runes?

Runes 是 Svelte 5 引入的编译器级原语:$state 用于响应式状态,$derived 用于计算值,$effect 用于副作用,$props 用于组件 props。它们提供了在任何地方都可用的细粒度响应式。

可以在 Svelte 中使用 TypeScript 吗?

可以,Svelte 对 TypeScript 有一等支持。在 .svelte 文件的 script 标签中添加 lang="ts" 即可。SvelteKit 项目默认使用 TypeScript 脚手架。

Svelte store 与 Redux 或 Zustand 相比如何?

Svelte store 比 Redux 简单得多。$ 自动订阅语法消除了样板代码。对于大多数 Svelte 应用,内置 store 就足够了。

SvelteKit 可以用于生产环境吗?

可以,SvelteKit 于 2022 年 12 月达到 1.0 版本。Apple、Spotify、纽约时报和宜家等公司都在使用。

如何部署 SvelteKit 应用?

SvelteKit 使用适配器进行部署。adapter-auto 自动检测平台。有专门的适配器用于 Vercel、Netlify、Cloudflare Pages、Node.js 服务器和静态站点生成。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON Formatter{ }CSS Minifier / Beautifier.*Regex Tester

相关文章

React Hooks 完全指南:useState、useEffect 和自定义 Hooks

通过实际示例掌握 React Hooks。学习 useState、useEffect、useContext、useReducer、useMemo、useCallback、自定义 Hooks 和 React 18+ 并发 Hooks。

Angular指南:组件、服务、RxJS、NgRx和Angular 17+信号

掌握Angular框架。涵盖组件和数据绑定、指令、依赖注入、响应式表单、RxJS可观察对象、懒加载路由器、NgRx状态管理和Angular 17信号。

Web性能优化指南:Core Web Vitals、缓存和React/Next.js

掌握Web性能优化。涵盖Core Web Vitals(LCP、FID、CLS)、图片优化、代码分割、缓存策略、React/Next.js性能、Lighthouse评分和实际基准测试。