跳到主要内容

在 Cursor 中开发 Svelte 和 SvelteKit:完整指南

在 Cursor 中开发 Svelte

Svelte 是一个基于编译器的 UI 框架,它向浏览器发送最少的 JavaScript,而 SvelteKit 是其全栈应用框架。Svelte 5 引入了 runes,一种比之前基于 let 的响应式更明确、更强大的新响应式模型。Cursor 凭借其兼容 VS Code 的基础和集成的 AI 助手,是 Svelte 开发的出色编辑器——它理解 .svelte 文件,提供丰富的 IntelliSense,并能从自然语言提示生成组件。

本指南涵盖了在 Cursor 中完整的 Svelte 开发工作流:设置工具链、掌握 Svelte 5 runes、构建基于文件路由的 SvelteKit 应用、使用 stores 管理共享状态,以及利用 AI 生成和重构组件。

在 Cursor 中设置 Svelte 开发环境​

前置条件​

Svelte 5 和 SvelteKit 需要 Node.js 18 或更高版本。验证你的安装:

node --version
npm --version

如果需要更新版本,使用 fnm 或 nvm:

fnm install 20
fnm use 20
提示

Cursor 的集成终端(Ctrl+`)会记住当前工作目录,因此你可以创建项目脚手架并立即打开文件,无需切换上下文。

安装 Svelte 扩展​

Cursor 依赖官方 Svelte 扩展来提供语言支持。从扩展视图(Ctrl+Shift+X)安装:

  1. 搜索 "Svelte"。
  2. 安装由 Svelte 团队发布的扩展。
  3. 如果提示,重新加载 Cursor。

该扩展提供:

  • .svelte 文件的语法高亮。
  • 通过 svelte-language-server 提供诊断和类型检查。
  • 通过 Prettier 和 prettier-plugin-svelte 包进行格式化。
  • 组件导入和 props 的 IntelliSense。
注意

如果你之前使用过社区的 Svelte 扩展,请在安装官方扩展之前禁用它。冲突的扩展会导致重复诊断和不可靠的格式化。

创建 SvelteKit 项目​

使用官方 sv CLI 创建新的 SvelteKit 应用:

npx sv create my-svelte-app

交互式提示提供预设选项:

? Which template would you like? SvelteKit minimal
? Add type checking with TypeScript? Yes, using TypeScript syntax
? Add ESLint for code linting? Yes
? Add Prettier for code formatting? Yes
? Add Playwright for browser testing? No
? Add Vitest for unit testing? Yes

在 Cursor 中打开项目:

cd my-svelte-app
cursor .
npm install
npm run dev

开发服务器启动于 http://localhost:5173。

提示

你可以在之后添加功能而无需重建项目。sv add 命令可以在现有应用上安装集成,如 Tailwind CSS、Storybook 或 Drizzle:

npx sv add tailwindcss
npx sv add storybook

掌握 Svelte 5 Runes​

什么是 Runes?​

Runes 是 Svelte 5 中引入的特殊编译器识别函数。它们用明确、可预测的模型取代了 Svelte 4 的隐式响应式。核心 runes 包括:

  • $state — 声明响应式状态。
  • $derived — 声明从其他响应式状态计算的值。
  • $effect — 当依赖变化时运行副作用。
  • $props — 声明组件 props。
  • $bindable — 将 prop 标记为可双向绑定。

使用 $state 声明状态​

<script lang="ts">
let count = $state(0);
let user = $state({ name: 'Ada', age: 36 });

function increment() {
count++;
}
</script>

<button onclick={increment}>
Count: {count}
</button>
<p>{user.name} is {user.age}</p>
提示

在 Svelte 5 中,事件处理器使用 onclick 属性(小写,无 on: 前缀)代替旧的 on:click。新语法更接近标准 DOM 属性,且与 TypeScript 配合更好。

使用 $derived 派生值​

<script lang="ts">
let price = $state(100);
let quantity = $state(2);
let total = $derived(price * quantity);
let formatted = $derived(
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(total)
);
</script>

<p>Price: $ {price}</p>
<p>Quantity: {quantity}</p>
<p>Total: {formatted}</p>

使用 $effect 处理副作用​

<script lang="ts">
let theme = $state<'light' | 'dark'>('light');

$effect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
});
</script>

<button onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
注意

$effect 在 DOM 更新后运行。不要用它来计算影响渲染的值——请改用 $derived。Effect 用于与外部系统(API、localStorage、第三方库)同步。

使用 $props 声明组件 Props​

<script lang="ts">
interface Props {
title: string;
count?: number;
onIncrement?: (value: number) => void;
}

let { title, count = 0, onIncrement }: Props = $props();

function handleClick() {
count++;
onIncrement?.(count);
}
</script>

<h2>{title}</h2>
<button onclick={handleClick}>Clicked {count} times</button>

使用 $bindable 进行双向绑定​

<!-- TextInput.svelte -->
<script lang="ts">
interface Props {
value?: string;
}
let { value = $bindable('') }: Props = $props();
</script>

<input bind:value />

在父组件中使用:

<script lang="ts">
import TextInput from './TextInput.svelte';
let name = $state('');
</script>

<TextInput bind:value={name} />
<p>Hello, {name}!</p>
提示

$bindable 是 Svelte 5 对 export let + bind:value 模式的替代。它使双向绑定契约在子组件的类型签名中变得明确。

构建 SvelteKit 应用​

基于文件的路由​

SvelteKit 在 src/routes 目录内使用基于文件的路由。每个 +page.svelte 文件成为一个页面,+layout.svelte 文件包裹嵌套页面。

src/routes/
├── +layout.svelte
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/:slug
└── +layout.svelte

使用 +page.server.ts 加载数据​

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

interface Post {
slug: string;
title: string;
body: string;
}

export const load: PageServerLoad = async ({ params }) => {
const response = await fetch(`https://api.example.com/posts/${params.slug}`);
if (!response.ok) {
throw error(404, 'Post not found');
}
const post: Post = await response.json();
return { post };
};

在页面中消费数据:

<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>

<article>
<h1>{data.post.title}</h1>
<p>{data.post.body}</p>
</article>
提示

$types 模块由 SvelteKit 自动生成。它为你提供完全类型化的 load 函数和 data props。Cursor 的 IntelliSense 会自动读取这些类型,因此你可以获得 data.post.title 的自动补全。

表单动作​

SvelteKit 使用在 +page.server.ts 中定义的 actions 处理表单提交:

// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

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

if (!email || !password) {
return fail(400, { message: 'Missing credentials' });
}

const token = await authenticate(String(email), String(password));
if (!token) {
return fail(401, { message: 'Invalid credentials' });
}

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

对应页面使用 enhance action 实现渐进增强:

<!-- src/routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
let { form }: { form: PageData } = $props();
</script>

<form method="POST" use:enhance>
<label>Email <input name="email" type="email" /></label>
<label>Password <input name="password" type="password" /></label>
{#if form?.message}
<p class="error">{form.message}</p>
{/if}
<button type="submit">Log in</button>
</form>
注意

服务器端 +page.server.ts 文件仅在服务器上运行。永远不要将浏览器专用 API(如 localStorage)导入其中,也不要将密钥放在 +page.ts(通用 load)中,因为它也可能在浏览器中运行。

布局和布局组​

<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
import Nav from '../components/Nav.svelte';

let { children }: { children: Snippet } = $props();
</script>

<div class="app">
<Nav />
<main>{@render children()}</main>
</div>

在 Svelte 5 中,children snippet 替代了已弃用的 <slot /> 语法。

使用 Stores 管理共享状态​

Svelte Stores 基础​

即使有了 runes,Svelte stores 对于跨组件状态仍然有用。store 是任何具有 subscribe 方法的对象。

// lib/stores/user.ts
import { writable } from 'svelte/store';

interface User {
id: string;
name: string;
}

export const currentUser = writable<User | null>(null);

使用 $ 前缀消费 store:

<script lang="ts">
import { currentUser } from '$lib/stores/user';
</script>

{#if $currentUser}
<p>Welcome, {$currentUser.name}</p>
{:else}
<p>Please log in.</p>
{/if}

Readable 和 Derived Stores​

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

export const time = readable(new Date(), (set) => {
const interval = setInterval(() => set(new Date()), 1000);
return () => clearInterval(interval);
});

export const formattedTime = derived(time, ($time) =>
$time.toLocaleTimeString('en-US')
);

结合 Runes 和 Stores​

你可以在 $effect 中使用 stores 来桥接旧代码与 runes:

<script lang="ts">
import { currentUser } from '$lib/stores/user';
import { get } from 'svelte/store';

let userSnapshot = $state(get(currentUser));

$effect(() => {
const unsubscribe = currentUser.subscribe((value) => {
userSnapshot = value;
});
return unsubscribe;
});
</script>
提示

对于新项目,推荐使用基于 runes 的共享状态模式,即使用带有 $state 字段的类和 Svelte context API。这避免了 store 的样板代码,同时保持响应式:

// lib/state/counter.svelte.ts
export class CounterState {
count = $state(0);
increment() {
this.count++;
}
}

AI 辅助 Svelte 组件生成​

从提示生成组件​

Cursor 的 AI 在生成使用 runes 的 Svelte 5 组件方面表现出色。打开聊天(Ctrl+Shift+P → "Cursor: New Chat")或使用内联编辑(Ctrl+K)并描述你的需求:

Create a Svelte 5 component (script lang="ts") for an accessible accordion.
It should accept an items prop: array of { id, title, content }. Use runes
for state (which panel is open). Include keyboard navigation (arrow keys)
and scoped styles.

Cursor 生成完整的 .svelte 文件。审查输出,然后迭代——要求它添加 allowMultiple prop 或动画过渡。

将 Svelte 4 迁移到 Svelte 5​

如果你维护较旧的代码库,Cursor 可以将组件迁移到 runes 语法。高亮文件,按 Ctrl+K,并提示:

Migrate this Svelte 4 component to Svelte 5 runes syntax. Replace `let`
reactivity with $state, `$:` with $derived/$effect, `export let` with $props,
and `createEventDispatcher` with callback props. Preserve all behavior.
注意

Runes 迁移会以微妙的方式改变响应式时序。重构后,运行测试套件并手动验证任何使用生命周期钩子或 watcher 的组件,因为 $effect 的触发条件与旧的 afterUpdate 不同。

生成端到端测试​

将 Playwright 与 AI 生成的测试配对:

// tests/accordion.spec.ts
import { test, expect } from '@playwright/test';

test('accordion expands on click', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Section 1' }).click();
await expect(page.getByText('Content of section 1')).toBeVisible();
});

提示 Cursor:"Generate Playwright tests for Accordion.svelte covering expand/collapse and keyboard navigation."

为 Svelte 项目配置 Cursor​

工作区设置​

创建 .vscode/settings.json 以在整个团队中标准化格式化:

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "svelte.svelte-vscode",
"svelte.plugin.svelte.format.config.singleQuote": true,
"svelte.plugin.svelte.format.config.semi": true,
"svelte.plugin.svelte.note-new-transformation": false,
"typescript.tsdk": "node_modules/typescript/lib"
}

Prettier 与 Svelte 插件​

npm install -D prettier prettier-plugin-svelte

.prettierrc:

{
"singleQuote": true,
"semi": true,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": { "parser": "svelte" }
}
]
}
提示

Prettier 格式化 <script> 和 <style> 块,但故意不处理 Svelte 模板标记,以避免破坏指令。对于模板格式化,依赖 Svelte 扩展的内置格式化器。

ESLint 配置​

// eslint.config.js
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import tsParser from 'typescript-eslint';

export default [
js.configs.recommended,
...svelte.configs['flat/recommended'],
{
languageOptions: {
parserOptions: {
extraFileExtensions: ['.svelte'],
},
},
},
];

调试和性能​

Svelte DevTools​

安装 Svelte DevTools 浏览器扩展以检查组件层次结构和状态。虽然 Cursor 托管编辑器,但浏览器 DevTools 显示运行时值,这对调试 runes 至关重要。

生产环境中的 Source Maps​

SvelteKit 默认生成 source maps。在 svelte.config.js 中确认:

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

const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};

export default config;
注意

部署到 serverless 平台时,验证你的 adapter 是否正确处理 source maps。adapter-node 和 adapter-vercel 的行为不同——在将 source maps 发送到生产环境之前,请查阅 adapter 文档。

Bundle 分析​

添加 rollup-plugin-visualizer 来检查 bundle 大小:

// vite.config.ts
import { sveltekit } from '@sveltejs/vite-plugin-sveltekit';
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
plugins: [
sveltekit(),
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
}),
],
});

在 Cursor 中开发 Svelte 的最佳实践​

项目结构​

src/
├── lib/
│ ├── components/
│ │ ├── Accordion.svelte
│ │ └── TextInput.svelte
│ ├── stores/
│ │ └── user.ts
│ ├── state/
│ │ └── counter.svelte.ts
│ └── utils/
├── routes/
│ ├── +layout.svelte
│ └── +page.svelte
└── app.html

Runes 文件的命名约定​

Svelte 5 引入了 .svelte.ts(和 .svelte.js)扩展名,用于在组件外使用 runes 的模块。相应地命名共享状态文件:

// src/lib/state/cart.svelte.ts
export class CartState {
items = $state<{ id: number; qty: number }[]>([]);

get total() {
return this.items.reduce((sum, i) => sum + i.qty, 0);
}

add(id: number, qty = 1) {
this.items.push({ id, qty });
}
}
提示

.svelte.ts 后缀告诉 Svelte 编译器在普通 TypeScript 模块中处理 runes。没有它,$state 被视为未定义函数,构建会失败。

默认可访问性​

Svelte 的编译器会警告缺少 alt、aria-* 和标签关联。在 CI 中将这些警告视为错误:

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

const config = {
preprocess: vitePreprocess(),
kit: { adapter: adapter() },
compilerOptions: {
accessors: false,
},
};

export default config;

常见陷阱及如何避免​

使用旧的事件语法​

Svelte 5 弃用了 on:click,改用 onclick。在过渡期间混合使用两者可以工作但会产生警告。统一使用新语法:

<!-- Svelte 4(已弃用) -->
<button on:click={handler}>Click</button>

<!-- Svelte 5(推荐) -->
<button onclick={handler}>Click</button>

修改 Props​

Props 默认是只读的。使用 $bindable 进行双向绑定或发出回调:

<script lang="ts">
interface Props {
value: string;
onChange?: (v: string) => void;
}
let { value, onChange }: Props = $props();

function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
onChange?.(target.value);
}
</script>

<input value={value} oninput={handleInput} />

过度使用 $effect​

Effect 应该与外部系统同步,而不是计算渲染值。如果你发现自己在写 $effect 来更新模板中使用的变量,请改用 $derived:

<script lang="ts">
let a = $state(1);
let b = $state(2);

// 错误 — 用 effect 计算派生值
// let sum = $state(0);
// $effect(() => { sum = a + b; });

// 正确 — 派生
let sum = $derived(a + b);
</script>
注意

依赖自身的 effect 可能创建无限循环。Svelte 编译器能检测某些情况,但始终要验证你的 effect 依赖是有限且终止的。

总结​

Cursor 是 Svelte 和 SvelteKit 开发的强大环境。官方 Svelte 扩展提供类型安全的编写和格式化,Svelte 5 runes 带来明确和可预测的响应式,SvelteKit 的基于文件的路由和类型化的 load 函数加速全栈工作。通过将这些工具与 Cursor 的 AI 助手配合进行组件生成、重构和测试脚手架,你可以高效地构建现代 Svelte 应用。从 sv create 开始,锁定你的 Prettier 和 ESLint 配置,让 runes 和 AI 处理重复的脚手架工作,而你专注于交付精致的用户体验。