Svelte前端开发最佳实践Skill svelte

这个技能专注于使用 Svelte 5 框架进行高效前端开发,涵盖 TanStack Query 的突变模式管理、shadcn-svelte UI 组件库的集成与最佳实践,以及组件组合和自我封装的设计模式。适用于构建现代化、响应式的单页应用,提升开发效率和用户体验。关键词:Svelte, TanStack Query, shadcn-svelte, 前端开发, 组件开发, 状态管理, UI 设计, 数据突变

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

名称: svelte 描述: Svelte 5 模式,包括 TanStack Query 突变、shadcn-svelte 组件和组件组合。适用于编写 Svelte 组件、使用 TanStack Query 或处理 shadcn-svelte UI 时。

Svelte 指南

突变模式偏好

在 Svelte 文件中 (.svelte)

始终优先使用 TanStack Query 的 createMutation 进行突变。这提供了:

  • 加载状态 (isPending)
  • 错误状态 (isError)
  • 成功状态 (isSuccess)
  • 更好的用户体验,具有自动状态管理

首选模式

onSuccessonError 作为第二个参数传递给 .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()

  1. 不需要加载状态
  2. 执行一次性操作
  3. 需要细粒度控制异步流程

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/utilscn() 实用程序来组合 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 元素
  • 当您发现自己传递回调仅更新父级状态时

关键见解:实例化多个对话框(每行一个)而不是管理具有复杂状态的单个共享对话框是完全可行的。现代框架有效处理这一点,代码清晰度是值得的。