名称: svelte 描述: Svelte 5 模式,包括 TanStack Query 突变、shadcn-svelte 组件和组件组合。适用于编写 Svelte 组件、使用 TanStack Query 或处理 shadcn-svelte UI 时。
Svelte 指南
突变模式偏好
在 Svelte 文件中 (.svelte)
始终优先使用 TanStack Query 的 createMutation 进行突变。这提供了:
- 加载状态 (
isPending) - 错误状态 (
isError) - 成功状态 (
isSuccess) - 更好的用户体验,具有自动状态管理
首选模式
将 onSuccess 和 onError 作为第二个参数传递给 .mutate() 以获取最大上下文:
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import * as rpc from '$lib/query';
// 将 .options 包装在访问器函数中,不要在 .options 上使用括号
// 根据功能命名,而不是使用 "Mutation" 后缀(冗余)
const deleteSession = createMutation(
() => rpc.sessions.deleteSession.options,
);
// 我们可以在回调中访问的局部状态
let isDialogOpen = $state(false);
</script>
<Button
onclick={() => {
// 将回调作为第二个参数传递给 .mutate()
deleteSession.mutate(
{ sessionId },
{
onSuccess: () => {
// 访问局部状态和上下文
isDialogOpen = false;
toast.success('会话已删除');
goto('/sessions');
},
onError: (error) => {
toast.error(error.title, { description: error.description });
},
},
);
}}
disabled={deleteSession.isPending}
>
{#if deleteSession.isPending}
删除中...
{:else}
删除
{/if}
</Button>
为什么使用此模式?
- 更多上下文:在调用站点访问局部变量和状态
- 更好的组织:成功/错误处理与操作位于同一位置
- 灵活性:不同的调用可以有不同的成功/错误行为
在 TypeScript 文件中 (.ts)
始终使用 .execute(),因为 createMutation 需要组件上下文:
// 在 .ts 文件中(例如,加载函数、实用程序)
const result = await rpc.sessions.createSession.execute({
body: { title: '新会话' },
});
const { data, error } = result;
if (error) {
// 处理错误
} else if (data) {
// 处理成功
}
例外:在 Svelte 文件中何时使用 .execute()
仅在以下情况下在 Svelte 文件中使用 .execute():
- 不需要加载状态
- 执行一次性操作
- 需要细粒度控制异步流程
无 handle* 函数 - 始终内联
切勿在脚本标签中创建以 handle 前缀开头的函数。如果函数只使用一次且逻辑不深层嵌套,直接在模板中内联它:
<!-- 差:不必要的包装函数 -->
<script>
function handleShare() {
share.mutate({ id });
}
function handleSelectItem(itemId: string) {
goto(`/items/${itemId}`);
}
</script>
<Button onclick={handleShare}>分享</Button>
<Item onclick={() => handleSelectItem(item.id)} />
<!-- 好:直接内联逻辑 -->
<Button onclick={() => share.mutate({ id })}>分享</Button>
<Item onclick={() => goto(`/items/${item.id}`)} />
这使相关逻辑与触发它的 UI 元素位于同一位置,使代码更容易跟踪。
样式
有关一般 CSS 和 Tailwind 指南,请参见 styling 技能。
shadcn-svelte 最佳实践
组件组织
- 使用 CLI:
bunx shadcn-svelte@latest add [组件] - 每个组件在其自己的文件夹下,位于
$lib/components/ui/,并有index.ts导出 - 文件夹名称遵循 kebab-case(例如,
dialog/,toggle-group/) - 将相关子组件分组在同一文件夹中
- 当使用 $state、$derived 或仅在标记中引用一次的函数时,直接内联它们
导入模式
命名空间导入(首选用于多部分组件):
import * as Dialog from '$lib/components/ui/dialog';
import * as ToggleGroup from '$lib/components/ui/toggle-group';
命名导入(用于单个组件):
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
Lucide 图标(始终使用 @lucide/svelte 中的单个导入):
// 好:单个图标导入
import Database from '@lucide/svelte/icons/database';
import MinusIcon from '@lucide/svelte/icons/minus';
import MoreVerticalIcon from '@lucide/svelte/icons/more-vertical';
// 差:不要从 lucide-svelte 导入多个图标
import { Database, MinusIcon, MoreVerticalIcon } from 'lucide-svelte';
路径使用 kebab-case(例如,more-vertical, minimize-2),您可以任意命名导入(通常为 PascalCase,可选 Icon 后缀)。
样式和自定义
- 始终使用来自
$lib/utils的cn()实用程序来组合 Tailwind 类 - 直接修改组件代码,而不是用复杂 CSS 覆盖样式
- 使用
tailwind-variants进行组件变体系统 - 遵循
background/foreground颜色约定 - 利用 CSS 变量实现主题一致性
组件使用模式
遵循 shadcn-svelte 模式使用适当的组件组合:
<Dialog.Root bind:open={isOpen}>
<Dialog.Trigger>
<Button>打开</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>标题</Dialog.Title>
</Dialog.Header>
</Dialog.Content>
</Dialog.Root>
自定义组件
- 扩展 shadcn 组件时,创建维护设计系统的包装组件
- 为复杂组件属性添加 JSDoc 注释
- 确保自定义组件遵循相同的组织模式
- 考虑语义适当性(例如,对于页面部分使用节标题而不是卡片)
属性模式
始终内联属性类型
切勿创建单独的 type Props = {...} 声明。始终直接在 $props() 中内联类型:
<!-- 差:单独的 Props 类型 -->
<script lang="ts">
type Props = {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
};
let { selectedWorkspaceId, onSelect }: Props = $props();
</script>
<!-- 好:内联属性类型 -->
<script lang="ts">
let { selectedWorkspaceId, onSelect }: {
selectedWorkspaceId: string | undefined;
onSelect: (id: string) => void;
} = $props();
</script>
Children 属性永远不需要类型注释
children 属性在 Svelte 中是隐式类型的。切勿注释它:
<!-- 差:注释 children -->
<script lang="ts">
let { children }: { children: Snippet } = $props();
</script>
<!-- 好:children 是隐式类型的 -->
<script lang="ts">
let { children } = $props();
</script>
<!-- 好:其他属性需要类型,但 children 不需要 -->
<script lang="ts">
let { children, title, onClose }: {
title: string;
onClose: () => void;
} = $props();
</script>
自包含组件模式
优先组件组合而不是父级状态管理
构建交互式组件时(尤其是对话框/模态),创建自包含组件,而不是在父级别管理状态。
反模式(父级状态管理)
<!-- 父组件 -->
<script>
let deletingItem = $state(null);
</script>
{#each items as item}
<Button onclick={() => (deletingItem = item)}>删除</Button>
{/each}
<AlertDialog open={!!deletingItem}>
<!-- 所有项目的单个对话框 -->
</AlertDialog>
模式(自包含组件)
<!-- DeleteItemButton.svelte -->
<script lang="ts">
import { createMutation } from '@tanstack/svelte-query';
import { rpc } from '$lib/query';
let { item }: { item: Item } = $props();
let open = $state(false);
const deleteItem = createMutation(() => rpc.items.delete.options);
</script>
<AlertDialog.Root bind:open>
<AlertDialog.Trigger>
<Button>删除</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<Button onclick={() => deleteItem.mutate({ id: item.id })}>
确认删除
</Button>
</AlertDialog.Content>
</AlertDialog.Root>
<!-- 父组件 -->
{#each items as item}
<DeleteItemButton {item} />
{/each}
为什么此模式有效
- 无父级状态污染:父级不需要跟踪哪个项目正在被删除
- 更好的封装:所有删除逻辑集中在一处
- 更简单的心理模型:每行有自己的删除按钮和自己的对话框
- 无需回调:组件内部处理一切
- 扩展性更好:添加新操作不会使父级复杂化
何时应用此模式
- 表格行中的操作按钮(删除、编辑等)
- 列表项的确认对话框
- 任何需要模态交互的重复 UI 元素
- 当您发现自己传递回调仅更新父级状态时
关键见解:实例化多个对话框(每行一个)而不是管理具有复杂状态的单个共享对话框是完全可行的。现代框架有效处理这一点,代码清晰度是值得的。