关于 svelte-kit
面向 Svelte 和 SvelteKit 开发者的开发技能,用于处理实现问题。它侧重 Svelte 5 runes 与现代 SvelteKit 模式,并结合版本、渲染、路由和数据加载情境提供指导。
主要功能
- 01Svelte 5 runes 指导
- 02SvelteKit 路由与加载模式
- 03SSR 与水合分析
- 04组件设计模式
- 05按版本提供建议
适用场景
- 01排查响应式状态问题
- 02选择服务端或通用加载函数
- 03避免 SSR 与水合问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tomlord1122/ tomtom-skill --skill svelte-kit安装后在 Claude Code 中输入/svelte-kit确认已加载。
详细介绍
Svelte/SvelteKit 专家
专注于 Svelte 5 runes 语法、SvelteKit 路由、SSR/SSG 策略和组件设计模式的专家助手。
思维转变(Svelte 4 → 5)
默认采用 Svelte 5 runes 和现代 SvelteKit 惯例。最重要的思维转变:
- Stores → runes。 优先使用
$state/$derived,而不是writable/$:。$state对象/数组具有深层响应性,是 代理(proxy)。 $effect是最后的选择,不是同步工具。 使用$derived派生值;仅在确实需要副作用时(DOM、日志记录、订阅)使用$effect。- Slots → snippets。
{#snippet}/{@render}取代<slot>。 on:click→onclick。 事件现在是普通属性(不再使用冒号)。$app/stores→$app/state。 粒度更细,无需$前缀。export let→$props(),并从$types导入PageProps/LayoutProps。
思考过程
激活后,按照以下结构化思考方法解决 Svelte/SvelteKit 问题:
步骤 1:问题分类
目标: 理解这是哪一类 Svelte 问题。
需要提出的关键问题:
- 这是响应式问题吗?(状态更新未反映、派生值)
- 这是渲染问题吗?(SSR 与 CSR、水合不匹配)
- 这是路由问题吗?(导航、参数、布局)
- 这是数据加载问题吗?(load 函数、表单操作)
- 这是组件设计问题吗?(属性、插槽、事件)
决策点: 对问题进行分类,以选择合适的解决方案:
- 响应式 → 检查 runes 的用法($state、$derived、$effect)
- 渲染 → 考虑 SSR/CSR 的影响
- 路由 → 查看 SvelteKit 惯例
- 数据加载 → 区分 +page.ts 与 +page.server.ts
- 组件 → 应用组合模式
步骤 2:版本与上下文检查
目标: 确保解决方案符合项目使用的 Svelte 版本。
需要提出的关键问题:
- 使用的是 Svelte 5(runes)还是 Svelte 4(stores)?
- 使用的是哪个版本的 SvelteKit?
- 配置了哪种渲染模式?(SSR、SPA、SSG)
操作:
- 检查
package.json中的 svelte 和 @sveltejs/kit 版本 - 查找
svelte.config.js中的适配器配置 - 留意任何预渲染设置
特定版本语法:
| 概念 | 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 name | let { name } = $props() |
决策点: 除非明确是在使用 Svelte 4,否则始终默认采用 Svelte 5 runes 语法。
步骤 3:SSR/CSR 分析
目标: 理解渲染上下文及其影响。
思考框架:
- “这段代码何时运行?”(服务端、客户端或两者)
- “每个阶段能访问哪些数据?”
- “这会导致水合不匹配吗?”
SSR 决策矩阵:
| 代码位置 | 运行位置 | 用途 |
|---|---|---|
| +page.server.ts | 仅服务端 | 数据库访问、机密信息、身份验证 |
| +page.ts | 服务端 + 客户端 | 公共 API 调用、依赖 URL 的数据 |
| +page.svelte | 服务端 + 客户端 | UI 渲染 |
| $effect() | 仅客户端 | DOM 操作、订阅 |
常见 SSR 陷阱:
- 在 SSR 上下文中使用浏览器 API(window、document)
- 服务端与客户端渲染的内容不同
- 错误地访问 cookie/请求头
SSR 安全模式: 使用 import { browser } from '$app/environment' 保护仅限浏览器的 API,并在 $effect(() => { if (browser) { /* ... */ } }) 中执行 DOM 访问(effects 仅在客户端运行)。
步骤 4:数据流设计
目标: 设计正确的数据加载和变更模式。
思考框架:
- “这些数据来自哪里?”(服务端、客户端、URL)
- “什么时候应该获取这些数据?”(导航、操作、间隔)
- “谁可以访问这些数据?”(公开、已认证、已授权)
加载函数选择:
| 需求 | 使用 | 原因 |
|---|---|---|
| 访问机密信息/数据库 | +page.server.ts | 永远不会暴露给客户端 |
| 调用公开 API | +page.ts | 两端都会运行,适合缓存 |
| 对 SEO 至关重要的数据 | +page.server.ts | 保证包含在初始 HTML 中 |
| 仅在客户端使用 | 在 $effect 中执行 fetch | 避免 SSR 开销 |
表单操作思考:
- “此表单执行什么变更?”
- “需要进行哪些验证?”
- “成功/失败时应该如何处理?”
加载重新运行与身份验证影响:
- 使用
invalidate(url)/invalidateAll()重新运行加载;使用depends()声明依赖项;使用untrack()选择退出跟踪。 - 客户端导航时,布局加载不会自动重新运行。 对于身份验证,优先使用
handle钩子或单页服务器加载,而不是仅使用根布局加载。 - 操作更改身份验证状态时,更新
event.locals,以便后续加载获取新状态。 - 在共享服务器函数中使用
getRequestEvent()(SvelteKit ≥ 2.20)读取locals/url,无需通过参数传递事件。
流式传输: 从服务器加载中返回一个未等待的 promise,并使用 {#await} 渲染,以渐进式显示数据。
步骤 5:响应式设计
目标: 根据使用场景应用正确的响应式模式。
思考框架 — Runes 选择:
| 需求 | Rune | 示例 |
|---|---|---|
| 可变状态 | $state | let count = $state(0) |
| 计算值 | $derived | let double = $derived(count * 2) |
| 副作用 | $effect | $effect(() => save(data)) |
| 组件属性 | $props | let { name } = $props() |
| 双向绑定 | $bindable | let { value = $bindable() } = $props() |
响应式规则:
- 仅对需要触发更新的值使用
$state - 对所有计算值使用
$derived(不要手动更新);保持派生表达式纯净(无副作用)。对于多行逻辑,使用$derived.by(() => { ... })。 - 谨慎使用
$effect— 仅用于真正的副作用(DOM、日志记录、订阅),绝不要用它来同步/派生状态。返回一个清理函数以进行清理。 - 绝不要通过
$effect修改$derived值来“修复”它们。派生值可以重新赋值以实现乐观 UI,并会在依赖项更改时自动恢复。
深层响应性与状态细节:
$state对象/数组是深度响应式的代理 — 修改嵌套字段会触发更新。- 绝不要解构响应式状态或属性 — 这会捕获快照并破坏响应性。直接访问字段(
obj.x)或使用 getter。 $state.raw(...)— 浅层/不跟踪的状态;重新赋值整个值,不要进行修改。$state.snapshot(...)— 用于传递给外部 API/非 Svelte API 的普通(非代理)副本。- 通过getter 函数(
() => count)向函数传递实时状态,而不是直接传递变量。
Effect 变体: $effect.pre(在 DOM 更新前运行,例如自动滚动)、$effect.tracking()(是否处于响应式上下文中?)、$effect.root(手动作用域,手动清理)。
应避免的反模式:
<script>
// BAD: using $effect to derive/sync state
let doubled = $state(0);
$effect(() => { doubled = count * 2; }); // use $derived instead
// BAD: destructuring reactive state/props (loses reactivity)
let { user } = data; // snapshot — won't update
let { x, y } = $state({ x: 0, y: 0 });
// BAD: mutating props
let { count } = $props();
count += 1; // props are read-only; use $bindable() for two-way
</script>
步骤 6:组件设计
目标: 设计可复用、可组合的组件。
思考框架:
- “此组件的单一职责是什么?”
- “它需要哪些属性?”
- “插槽组合应有多大的灵活性?”
组件接口设计:
<script>
// Required props
let { title, items } = $props();
// Optional props with defaults
let { variant = 'default', disabled = false } = $props();
// Callback props
let { onClick = () => {} } = $props();
// Bindable props for two-way binding
let { value = $bindable() } = $props();
</script>
插槽模式:
- 默认插槽:主要内容区域
- 命名插槽:页眉、页脚、侧边栏
- 插槽属性:向插槽内容传递数据
第 7 步:性能优化
目标: 确保渲染性能达到最佳。
思考框架:
- “这个响应式值多久变化一次?”
- “重新渲染的成本有多高?”
- “能否使用记忆化或防抖?”
性能检查清单:
- 避免在 $derived 中执行开销大的计算
- 使用 {#key} 块强制重新渲染
- 为长列表实现虚拟化
- 延迟加载大型组件
- 预加载关键路由
第 8 步:错误处理
目标: 提供良好的错误体验。
错误边界: +error.svelte(路由级)、load 函数中的 try/catch、
表单操作中的 fail()、<svelte:boundary>(组件级,参见快速参考)。
错误模式: 当数据缺失时,从 load 中抛出 error(404, 'Not found');
SvelteKit 会使用 page.error/page.status 渲染最近的 +error.svelte。
项目设置
首选包管理器: bun
# Create new SvelteKit project with the modern `sv` CLI
# (replaces the deprecated `npm create svelte`)
bunx sv create my-app
cd my-app
bun install
bun run dev
精简的现代依赖项(均为 devDependencies):@sveltejs/adapter-auto ^6、@sveltejs/kit ^2、@sveltejs/vite-plugin-svelte ^5、svelte ^5、vite ^6。
文档资源
Context7 库 ID: /websites/svelte_dev(5523 个片段,评分:91)
AI 使用指南: https://svelte.dev/docs/ai
官方 llms.txt 资源(包级文件;旧的顶层 /docs/llms-small.txt 返回 404):
https://svelte.dev/llms.txt- 文档索引https://svelte.dev/docs/svelte/llms-full.txt- 完整的 Svelte 文档https://svelte.dev/docs/svelte/llms-small.txt- 压缩版 Svelte 文档https://svelte.dev/docs/kit/llms.txt- SvelteKit 文档
快速参考
Svelte 5 Runes
<script>
// Reactive state
let count = $state(0);
// Derived values (auto-updates when dependencies change)
let doubled = $derived(count * 2);
// Side effects
$effect(() => {
console.log(`Count is now ${count}`);
});
// Multi-line derived
let summary = $derived.by(() => items.reduce((a, b) => a + b.n, 0));
// Props: defaults, rename, rest
let { name = 'World', super: trouper, ...rest } = $props();
// Bindable props (opt-in two-way binding; default to one-way)
let { value = $bindable() } = $props();
// SSR-safe unique id
const id = $props.id();
</script>
现代模板语法
<script>
import type { Snippet } from 'svelte';
let { header, children }: { header?: Snippet; children: Snippet } = $props();
let active = $state(true);
</script>
<button onclick={() => (active = !active)}>Toggle</button>
<div class={{ active, disabled: !active }}>...</div>
{#snippet row(label, value)}
<tr><th>{label}</th><td>{value}</td></tr>
{/snippet}
{@render header?.()}
{@render children()}
{@render row('Status', active ? 'on' : 'off')}
<svelte:boundary>
<RiskyComponent />
{#snippet failed(error, reset)}
<p>{error.message}</p>
<button onclick={reset}>Retry</button>
{/snippet}
</svelte:boundary>
SvelteKit 路由
src/routes/ 下的文件:+page.svelte(UI)、+page.ts(通用 load)、
+page.server.ts(服务器 load/操作)、+layout.svelte / +layout(.server).ts
(布局)、+server.ts(API 端点)、+error.svelte(错误边界)。
动态片段使用 [slug];剩余参数使用 [...rest];可选参数使用 [[lang]]。
Load 函数与类型化数据
// +page.server.ts - Server-only
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals, fetch }) => {
const post = await fetch(`/api/posts/${params.slug}`);
return { post: await post.json() };
};
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
页面状态(使用 $app/state,而非 $app/stores)
<script>
// `$app/stores` is deprecated. `$app/state` is fine-grained, no `$` prefix.
import { page, navigating, updated } from '$app/state';
</script>
<p>Path: {page.url.pathname} · Status: {page.status}</p>
{#if navigating.to}<progress></progress>{/if}
表单操作
// +page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request, locals }) => {
const data = await request.formData();
const email = data.get('email');
// Use fail() for validation errors — do NOT throw
if (!email) return fail(400, { email, missing: true });
return { success: true };
}
};
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" use:enhance>...</form>
页面选项
// +page.ts / +page.server.ts
export const prerender = true; // or false | 'auto'
export const ssr = false; // SPA shell (client-rendered)
export const csr = false; // static HTML, no JS/hydration
// Dynamic prerender entries
export function entries() {
return [{ slug: 'first' }, { slug: 'second' }];
}
Hooks
| Hook | 文件 | 作用范围 | 用途 |
|---|---|---|---|
handle / handleFetch / init | hooks.server | 服务器 | 请求拦截、身份验证、fetch 重写、启动 |
handleError | hooks (.server/.client) | 共享 | 集中式错误记录/整形 |
reroute / transport | hooks | 通用 | URL 重写;在服务器↔客户端之间对自定义类型进行(反)序列化 |
状态管理
- 服务器是无状态的——切勿在模块级全局变量中存储每个用户的数据(这会导致数据泄漏到其他请求中)。保持 load 函数纯净。
- 通过上下文 API(
setContext/getContext)、URL 参数或快照共享客户端状态,而不要使用模块级可变单例。
现代模式(实验性——需选择启用)
这些 API 均为实验性。请显式启用;除非项目选择启用这些功能,否则优先使用上文稳定的 runes/load/action 模式。
异步 Svelte
在 Svelte 5.36 中为实验性功能(标志已移除/在 Svelte 6 中默认启用)。允许在顶层 <script>、$derived(...) 内以及标记中直接使用 await。
// svelte.config.js
export default { compilerOptions: { experimental: { async: true } } };
<script>
let { id } = $props();
// await directly in $derived
let post = $derived(await getPost(id));
</script>
<svelte:boundary>
<h1>{post.title}</h1>
{#snippet pending()}<p>loading…</p>{/snippet}
</svelte:boundary>
- 使用
$effect.pending()检测正在进行的异步操作。 - 当独立的异步工作被不必要地串行执行时,会触发
await_waterfall警告——应同时启动相互独立的工作。 - 错误会向上冒泡至最近的
<svelte:boundary>。
SvelteKit 远程函数
可选启用的类型安全服务器函数,可从客户端调用;可以替代 load 函数和表单操作。与异步 Svelte 自然搭配。
// svelte.config.js
export default { kit: { experimental: { remoteFunctions: true } } };
.remote.ts 文件从 $app/server 导出:
| 导出项 | 用途 | 亮点 |
|---|---|---|
query | 读取动态数据 | 按页面缓存、.refresh()、Standard Schema(Zod/Valibot)校验 |
form | 表单变更 | 将 {...createPost} 展开到 <form> 上;渐进增强;单航班 .updates();通过 withOverride 实现乐观 UI;每个按钮的 buttonProps |
command | 编程式写入 | .updates(query());渲染期间不能运行;不支持重定向 |
prerender | 构建时读取 | inputs: 种子;{ dynamic: true } 选项 |
// data.remote.ts
import { query } from '$app/server';
import * as v from 'valibot';
export const getPost = query(v.string(), async (slug) => {
return await db.posts.find(slug); // server-only, type-safe end to end
});
- 校验失败时返回 400;可通过
handleValidationError自定义。 getRequestEvent()可在远程函数中使用(没有params/route.id;只能在form/command中设置 Cookie)。
向用户展示结果
回答 Svelte/SvelteKit 问题时:
- 提供完整、可运行的代码示例
- 默认使用 Svelte 5 runes 语法和现代 SvelteKit 惯例(
$app/state、PageProps、snippets) - 说明服务器 load 函数与通用 load 函数之间的区别
- 指出 SvelteKit 版本之间的破坏性变更
- 标明实验性 API(异步 Svelte、远程函数)需要选择启用
- 适用时包含 TypeScript 类型
故障排查
“Cannot use $state outside of component”
- Runes 仅适用于
.svelte文件或.svelte.ts文件
“Hydration mismatch”
- 确保服务器和客户端初始渲染的内容相同
- 检查是否有仅限浏览器的代码在 SSR 期间运行
“Load function not running”
- 确认文件名:
+page.ts或+page.server.ts - 检查是否正确导出了
load函数