跳转至

Svelte 5 完全指南

为什么要学 Svelte 5

  1. 编译时响应式,运行时开销接近零:Svelte 在构建时将组件编译为高效的原生 JavaScript,不需要虚拟 DOM diff。生成的代码体积小、执行快,在真实性能基准测试中经常超越 React 和 Vue。

  2. Runes 系统彻底简化响应式:Svelte 5 引入的 Runes($state, $derived, $effect)用显式的声明替代了 Svelte 4 隐式的 $: 语法,逻辑更清晰,可调试性更强,也更容易在 .svelte.ts 文件中复用。

  3. 更少的代码量:实现同样的功能,Svelte 代码量通常比 React 少 30-40%。没有 useState/useEffect 模板代码,没有 useMemo/useCallback 优化焦虑。

  4. SvelteKit 提供全栈开发体验:SvelteKit 是 Svelte 官方元框架,提供路由、SSR、SSG、API 路由、表单 Actions 等开箱即用功能,类似 Next.js 对 React 的关系。

  5. 社区快速增长,企业采用加速:Apple、Spotify、The New York Times 等公司已在生产中使用 Svelte。StackOverflow 调查中 Svelte 连续多年获得"最受喜爱框架"称号。


核心概念详解

Svelte 是什么(白话解释)

React 和 Vue 的工作方式是:在浏览器运行时维护一个虚拟 DOM,每次数据变化就比较新旧虚拟 DOM 的差异,再去更新真实 DOM。Svelte 的做法完全不同:它在你编译代码的时候就把"检测变化并更新 DOM"的代码直接编译好了。运行时不需要框架代码,不需要 diff 算法。

打个比方:React 像是请了一个翻译在你和外国人之间实时传话(虚拟 DOM),Svelte 像是你直接学会了对方的语言(编译生成原生更新代码)。

Svelte 5 Runes 系统

Runes 是 Svelte 5 最重要的变化。Rune 这个词来自古代北欧文字,在这里表示一种"魔法标记"。

$state — 响应式状态

<script>
  // Svelte 5: 显式声明响应式状态
  let count = $state(0);
  let user = $state({ name: '张三', age: 25 });
  let items = $state([1, 2, 3]);

  // 深层响应式:修改对象属性也会触发更新
  function updateName() {
    user.name = '李四'; // 自动触发更新
  }

  // 数组方法也是响应式的
  function addItem() {
    items.push(items.length + 1); // 自动触发更新
  }
</script>

<button onclick={() => count++}>
  点击次数: {count}
</button>
<p>{user.name}, {user.age}岁</p>
<p>项目: {items.join(', ')}</p>

对比 Svelte 4:

<script>
  // Svelte 4: 隐式响应式(let 声明自动响应式)
  let count = 0;
  // 但对象和数组需要重新赋值才能触发更新
  let user = { name: '张三', age: 25 };
  user = { ...user, name: '李四' }; // 必须重新赋值
</script>

$derived — 派生状态

<script>
  let count = $state(0);
  let doubled = $derived(count * 2);
  let quadrupled = $derived(doubled * 2);

  // 复杂派生
  let items = $state([1, 2, 3, 4, 5]);
  let evenItems = $derived(items.filter(i => i % 2 === 0));
  let total = $derived(items.reduce((a, b) => a + b, 0));

  // 需要多行逻辑时用 $derived.by
  let summary = $derived.by(() => {
    const sum = items.reduce((a, b) => a + b, 0);
    const avg = sum / items.length;
    return `总和: ${sum}, 平均: ${avg.toFixed(1)}`;
  });
</script>

<p>计数: {count}, 双倍: {doubled}, 四倍: {quadrupled}</p>
<p>偶数项: {evenItems.join(', ')}</p>
<p>{summary}</p>

$effect — 副作用

<script>
  let count = $state(0);
  let searchTerm = $state('');

  // 基本 effect: 当依赖变化时自动运行
  $effect(() => {
    console.log(`count 变为: ${count}`);
    // 返回清理函数(可选)
    return () => {
      console.log(`清理: count 曾为 ${count}`);
    };
  });

  // 搜索防抖示例
  $effect(() => {
    const term = searchTerm; // 追踪这个依赖
    const timer = setTimeout(() => {
      if (term) {
        console.log(`搜索: ${term}`);
      }
    }, 300);
    return () => clearTimeout(timer);
  });

  // $effect.pre: 在 DOM 更新前运行
  $effect.pre(() => {
    console.log('DOM 即将更新');
  });
</script>

<input bind:value={searchTerm} placeholder="搜索..." />

$props — 组件属性

<!-- Child.svelte -->
<script>
  // Svelte 5: 使用 $props() 解构
  let { name, age = 18, onUpdate, children } = $props();

  // 类型安全(TypeScript)
  // interface Props {
  //   name: string;
  //   age?: number;
  //   onUpdate: (name: string) => void;
  // }
  // let { name, age = 18, onUpdate }: Props = $props();
</script>

<div>
  <p>{name}, {age}岁</p>
  <button onclick={() => onUpdate(name)}>更新</button>
  {@render children()}
</div>
<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
</script>

<Child name="张三" age={25} onUpdate={(n) => console.log(n)}>
  {#snippet children()}
    <p>这是子内容</p>
  {/snippet}
</Child>

$bindable — 双向绑定属性

<!-- TextInput.svelte -->
<script>
  let { value = $bindable('') } = $props();
</script>

<input bind:value />

<!-- 使用 -->
<script>
  import TextInput from './TextInput.svelte';
  let name = $state('');
</script>

<TextInput bind:value={name} />
<p>输入的是: {name}</p>

Svelte 5 vs React vs Vue 3 对比

特性Svelte 5React 19Vue 3
响应式机制编译时(Runes)运行时(Hooks)运行时(Proxy)
虚拟DOM
包体积极小(按需编译)~45KB~33KB
状态声明$state(value)useState(value)ref(value)
派生计算$derived(expr)useMemo(fn, deps)computed(fn)
副作用$effect(fn)useEffect(fn, deps)watchEffect(fn)
依赖追踪自动手动(deps数组)自动
过时闭包问题常见
模板语法增强HTMLJSX模板/JSX
双向绑定bind:value手动onChangev-model
动画支持内置(transition)需第三方库内置(Transition)
全栈框架SvelteKitNext.jsNuxt
TypeScript原生支持原生支持原生支持
学习曲线中高
生态规模中等极大很大

Snippets(替代 Slots)

Svelte 5 用 Snippets 替代了 Slots:

<script>
  let items = $state(['苹果', '香蕉', '橙子']);
</script>

<!-- 定义 snippet -->
{#snippet listItem(item, index)}
  <li class="item">
    <span class="index">{index + 1}.</span>
    <span class="name">{item}</span>
  </li>
{/snippet}

<ul>
  {#each items as item, i}
    {@render listItem(item, i)}
  {/each}
</ul>

可以把 snippet 作为 prop 传给子组件:

<!-- List.svelte -->
<script>
  let { items, renderItem } = $props();
</script>

<ul>
  {#each items as item}
    {@render renderItem(item)}
  {/each}
</ul>

<!-- 使用 -->
<List {items}>
  {#snippet renderItem(item)}
    <li>{item.name}: {item.price}元</li>
  {/snippet}
</List>

安装与配置

新建 SvelteKit 项目

# 使用官方脚手架
npx sv create my-app
cd my-app
npm install
npm run dev

交互式选项: - Template: SvelteKit minimal / SvelteKit demo / Svelte library - TypeScript: Yes - Add-ons: prettier, eslint, vitest, playwright, tailwindcss, drizzle, lucia, paraglide...

项目结构

my-app/
├── src/
│   ├── lib/              # 共享代码 ($lib 别名)
│   │   ├── components/   # 组件
│   │   ├── server/       # 仅服务端代码 ($lib/server)
│   │   └── utils.ts
│   ├── routes/           # 文件系统路由
│   │   ├── +layout.svelte
│   │   ├── +page.svelte
│   │   ├── +page.server.ts
│   │   ├── about/
│   │   │   └── +page.svelte
│   │   └── api/
│   │       └── users/
│   │           └── +server.ts
│   ├── app.html          # HTML 模板
│   └── app.css           # 全局样式
├── static/               # 静态资源
├── svelte.config.js
├── vite.config.ts
└── package.json

svelte.config.js

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      '@components': 'src/lib/components',
      '@utils': 'src/lib/utils'
    }
  }
};

export default config;

TypeScript 配置

# SvelteKit 项目已内置 TypeScript 支持
# 确保 .svelte 文件中使用 lang="ts"
<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }

  let users: User[] = $state([]);
</script>

快速上手:5 分钟最小示例

npx sv create counter-app
# 选择 SvelteKit minimal + TypeScript
cd counter-app
npm install

src/routes/+page.svelte

<script lang="ts">
  let count = $state(0);
  let history = $state<number[]>([]);
  let lastAction = $derived(
    history.length > 0 ? history[history.length - 1] : null
  );

  function increment() {
    count++;
    history.push(count);
  }

  function decrement() {
    count--;
    history.push(count);
  }

  function reset() {
    count = 0;
    history = [];
  }
</script>

<main>
  <h1>Svelte 5 计数器</h1>
  <p class="count">{count}</p>
  <div class="buttons">
    <button onclick={decrement}>-1</button>
    <button onclick={reset}>重置</button>
    <button onclick={increment}>+1</button>
  </div>
  {#if lastAction !== null}
    <p class="history">上次值: {lastAction}</p>
  {/if}
  {#if history.length > 0}
    <p class="history">历史: {history.join(' → ')}</p>
  {/if}
</main>

<style>
  main { text-align: center; padding: 2rem; font-family: system-ui; }
  .count { font-size: 4rem; font-weight: bold; margin: 1rem; }
  .buttons { display: flex; gap: 1rem; justify-content: center; }
  button { font-size: 1.5rem; padding: 0.5rem 1.5rem; cursor: pointer;
           border: 2px solid #333; border-radius: 8px; background: white; }
  button:hover { background: #f0f0f0; }
  .history { color: #666; margin-top: 1rem; }
</style>

运行:

npm run dev
# 打开 http://localhost:5173


进阶用法

场景一:可复用状态逻辑(.svelte.ts 文件)

// src/lib/stores/counter.svelte.ts
export function createCounter(initial = 0) {
  let count = $state(initial);
  let doubled = $derived(count * 2);

  return {
    get count() { return count; },
    get doubled() { return doubled; },
    increment() { count++; },
    decrement() { count--; },
    reset() { count = initial; }
  };
}

// 全局单例
export const globalCounter = createCounter(0);
<!-- 使用 -->
<script>
  import { globalCounter } from '$lib/stores/counter.svelte';
  // 或创建局部实例
  import { createCounter } from '$lib/stores/counter.svelte';
  const localCounter = createCounter(10);
</script>

<p>全局: {globalCounter.count} (x2={globalCounter.doubled})</p>
<button onclick={globalCounter.increment}>全局+1</button>

<p>局部: {localCounter.count}</p>
<button onclick={localCounter.increment}>局部+1</button>

场景二:SvelteKit 全栈 CRUD

// src/routes/posts/+page.server.ts
import { db } from '$lib/server/db';

export async function load() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' }
  });
  return { posts };
}

export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const title = data.get('title') as string;
    const content = data.get('content') as string;

    if (!title || title.length < 3) {
      return { success: false, error: '标题至少3个字符' };
    }

    await db.post.create({ data: { title, content } });
    return { success: true };
  },

  delete: async ({ request }) => {
    const data = await request.formData();
    const id = data.get('id') as string;
    await db.post.delete({ where: { id } });
    return { success: true };
  }
};
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
  let { data, form } = $props();
</script>

<h1>文章列表</h1>

{#if form?.error}
  <p class="error">{form.error}</p>
{/if}

<form method="POST" action="?/create">
  <input name="title" placeholder="标题" required />
  <textarea name="content" placeholder="内容"></textarea>
  <button type="submit">发布</button>
</form>

{#each data.posts as post}
  <article>
    <h2>{post.title}</h2>
    <p>{post.content}</p>
    <form method="POST" action="?/delete">
      <input type="hidden" name="id" value={post.id} />
      <button type="submit">删除</button>
    </form>
  </article>
{/each}

场景三:动画与过渡

<script>
  import { fly, fade, slide, scale } from 'svelte/transition';
  import { flip } from 'svelte/animate';
  import { quintOut } from 'svelte/easing';

  let items = $state([1, 2, 3, 4, 5]);
  let visible = $state(true);

  function addItem() {
    items.push(Math.max(...items) + 1);
  }

  function removeItem(item: number) {
    items = items.filter(i => i !== item);
  }

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

<button onclick={() => visible = !visible}>切换可见性</button>
<button onclick={addItem}>添加</button>
<button onclick={shuffle}>打乱</button>

{#if visible}
  <div transition:fade={{ duration: 300 }}>
    <p>我会淡入淡出</p>
  </div>
{/if}

<ul>
  {#each items as item (item)}
    <li
      animate:flip={{ duration: 300, easing: quintOut }}
      in:fly={{ y: 20, duration: 300 }}
      out:fade={{ duration: 200 }}
    >
      {item}
      <button onclick={() => removeItem(item)}>✕</button>
    </li>
  {/each}
</ul>

场景四:表单验证与增强

<script lang="ts">
  import { enhance } from '$app/forms';

  let loading = $state(false);
  let errors = $state<Record<string, string>>({});

  function validate(field: string, value: string) {
    if (field === 'email' && !value.includes('@')) {
      errors[field] = '请输入有效的邮箱地址';
    } else if (field === 'password' && value.length < 8) {
      errors[field] = '密码至少8个字符';
    } else {
      delete errors[field];
    }
  }
</script>

<form
  method="POST"
  use:enhance={() => {
    loading = true;
    return async ({ update }) => {
      await update();
      loading = false;
    };
  }}
>
  <label>
    邮箱
    <input
      name="email"
      type="email"
      oninput={(e) => validate('email', e.currentTarget.value)}
    />
    {#if errors.email}<span class="error">{errors.email}</span>{/if}
  </label>

  <label>
    密码
    <input
      name="password"
      type="password"
      oninput={(e) => validate('password', e.currentTarget.value)}
    />
    {#if errors.password}<span class="error">{errors.password}</span>{/if}
  </label>

  <button type="submit" disabled={loading || Object.keys(errors).length > 0}>
    {loading ? '提交中...' : '注册'}
  </button>
</form>

场景五:数据获取与流式加载

// src/routes/dashboard/+page.server.ts
export async function load() {
  // 并行加载
  const [users, stats] = await Promise.all([
    fetchUsers(),
    fetchStats()
  ]);

  return {
    users,
    stats,
    // 流式加载:不阻塞页面渲染
    slowData: fetchSlowData() // 返回 Promise,不 await
  };
}
<!-- src/routes/dashboard/+page.svelte -->
<script>
  let { data } = $props();
</script>

<!-- 立即可用的数据 -->
<h1>用户数: {data.users.length}</h1>
<p>总访问: {data.stats.visits}</p>

<!-- 流式数据 -->
{#await data.slowData}
  <p>加载详细数据...</p>
{:then slowData}
  <div>
    {#each slowData.items as item}
      <p>{item.name}</p>
    {/each}
  </div>
{:catch error}
  <p>加载失败: {error.message}</p>
{/await}

场景六:自定义 Action

<script lang="ts">
  // 点击外部关闭
  function clickOutside(node: HTMLElement, callback: () => void) {
    function handleClick(event: MouseEvent) {
      if (!node.contains(event.target as Node)) {
        callback();
      }
    }
    document.addEventListener('click', handleClick, true);
    return {
      destroy() {
        document.removeEventListener('click', handleClick, true);
      }
    };
  }

  // 长按
  function longpress(node: HTMLElement, duration = 500) {
    let timer: ReturnType<typeof setTimeout>;
    function handleMousedown() {
      timer = setTimeout(() => {
        node.dispatchEvent(new CustomEvent('longpress'));
      }, duration);
    }
    function handleMouseup() {
      clearTimeout(timer);
    }
    node.addEventListener('mousedown', handleMousedown);
    node.addEventListener('mouseup', handleMouseup);
    return {
      update(newDuration: number) { duration = newDuration; },
      destroy() {
        node.removeEventListener('mousedown', handleMousedown);
        node.removeEventListener('mouseup', handleMouseup);
      }
    };
  }

  let menuOpen = $state(false);
</script>

{#if menuOpen}
  <div class="menu" use:clickOutside={() => menuOpen = false}>
    <p>菜单内容</p>
  </div>
{/if}

<button use:longpress={800} onlongpress={() => alert('长按!')}>
  长按我
</button>

场景七:API 路由

// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ url }) => {
  const page = Number(url.searchParams.get('page') ?? 1);
  const limit = Number(url.searchParams.get('limit') ?? 10);

  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit
  });

  return json({ users, page, limit });
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();

  if (!body.name || !body.email) {
    throw error(400, '缺少必填字段');
  }

  const user = await db.user.create({ data: body });
  return json(user, { status: 201 });
};

场景八:布局与路由守卫

<!-- src/routes/+layout.svelte -->
<script>
  import { page } from '$app/stores';
  let { children, data } = $props();
</script>

<nav>
  <a href="/" class:active={$page.url.pathname === '/'}>首页</a>
  <a href="/about" class:active={$page.url.pathname === '/about'}>关于</a>
  {#if data.user}
    <a href="/dashboard">控制台</a>
    <span>{data.user.name}</span>
  {:else}
    <a href="/login">登录</a>
  {/if}
</nav>

<main>
  {@render children()}
</main>

<style>
  .active { font-weight: bold; }
</style>
// src/routes/dashboard/+layout.server.ts
import { redirect } from '@sveltejs/kit';

export async function load({ locals }) {
  if (!locals.user) {
    throw redirect(303, '/login');
  }
  return { user: locals.user };
}

常见问题与排错

问题一:$state 对象修改不触发更新

原因:一般不会发生。Svelte 5 的 $state 使用 Proxy 实现深层响应式,直接修改属性就会触发更新。如果没有触发更新,检查是否遗漏了 $state() 声明。

<script>
  // 错误:忘了用 $state
  let user = { name: '张三' };
  user.name = '李四'; // 不会触发更新

  // 正确
  let user2 = $state({ name: '张三' });
  user2.name = '李四'; // 触发更新
</script>

问题二:$effect 无限循环

原因:在 $effect 中修改了自己依赖的状态。

<script>
  let count = $state(0);

  // 错误:无限循环
  $effect(() => {
    count = count + 1; // 读取 count → 修改 count → 再次触发
  });

  // 正确:使用 untrack 避免追踪
  import { untrack } from 'svelte';
  $effect(() => {
    const current = untrack(() => count);
    // 做一些不涉及修改 count 的事
  });
</script>

问题三:从 Svelte 4 迁移到 Svelte 5

# 使用官方迁移工具
npx sv migrate svelte-5

主要变化对照:

Svelte 4               →  Svelte 5
let x = 0              →  let x = $state(0)
$: doubled = x * 2     →  let doubled = $derived(x * 2)
$: { console.log(x) }  →  $effect(() => { console.log(x) })
export let prop         →  let { prop } = $props()
<slot />                →  {@render children()}
on:click={handler}      →  onclick={handler}

问题四:SvelteKit 数据加载 +page.server.ts vs +page.ts

  • +page.server.ts:只在服务端运行,可安全使用数据库、环境变量、密钥
  • +page.ts:在服务端和客户端都运行,适合调用公共 API
// +page.server.ts(服务端专用)
export async function load() {
  const users = await db.query('SELECT * FROM users');
  return { users };
}

// +page.ts(通用)
export async function load({ fetch }) {
  const res = await fetch('/api/users');
  const users = await res.json();
  return { users };
}

问题五:CSS 样式作用域问题

Svelte 样式默认是组件作用域的:

<style>
  /* 只影响这个组件 */
  p { color: red; }

  /* 穿透到子组件 */
  :global(p) { color: red; }

  /* 部分穿透 */
  .wrapper :global(p) { color: red; }
</style>

问题六:环境变量使用

# .env
PUBLIC_API_URL=https://api.example.com
SECRET_DB_URL=postgresql://...
// 客户端可用(PUBLIC_ 前缀)
import { PUBLIC_API_URL } from '$env/static/public';

// 服务端专用
import { SECRET_DB_URL } from '$env/static/private';

// 动态环境变量
import { env } from '$env/dynamic/private';
console.log(env.SECRET_DB_URL);

问题七:部署配置

# Node.js 服务器
npm i -D @sveltejs/adapter-node

# 静态站点
npm i -D @sveltejs/adapter-static

# Vercel
npm i -D @sveltejs/adapter-vercel

# Cloudflare Pages
npm i -D @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

const config = {
  kit: {
    adapter: adapter({
      out: 'build',
      precompress: true,
      envPrefix: 'APP_'
    })
  }
};

export default config;

问题八:性能优化

<script>
  // 1. 用 $derived 替代 $effect 中的计算
  // 坏:在 effect 中计算并更新另一个状态
  let items = $state([]);
  let total_bad = $state(0);
  $effect(() => { total_bad = items.reduce((a, b) => a + b, 0); }); // 不好

  // 好:用 $derived
  let total_good = $derived(items.reduce((a, b) => a + b, 0));

  // 2. 大列表用 key
  // {#each items as item (item.id)}  ← 提供唯一 key

  // 3. 条件渲染而非隐藏
  // {#if visible}<HeavyComponent />{/if}  ← 不渲染就没有开销
</script>

参考资源

  • 官方文档:https://svelte.dev/docs
  • 官方教程:https://svelte.dev/tutorial
  • SvelteKit 文档:https://svelte.dev/docs/kit
  • Svelte Playground:https://svelte.dev/playground
  • GitHub:https://github.com/sveltejs/svelte
  • Svelte 5 迁移指南:https://svelte.dev/docs/svelte/v5-migration-guide
  • Svelte Society:https://sveltesociety.dev/
  • Awesome Svelte:https://github.com/TheComputerM/awesome-svelte
  • Joy of Code (博客):https://joyofcode.xyz/
  • Svelte Discord:https://svelte.dev/chat