Svelte5开发Skill svelte-development

Svelte 5 开发是一种使用 Svelte 5 框架和 SvelteKit 构建现代 Web 应用程序的前端开发技能,涉及响应式设计、全栈开发、TypeScript 和测试,关键词包括 Svelte 5, SvelteKit, 前端开发, Web 应用, TypeScript, 响应式设计, 符文系统, 组件开发, API 路由, 测试

前端开发 0 次安装 0 次浏览 更新于 3/20/2026

名称: 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 组件