$props() 接收父组件传值
基本用法
在 Svelte 5 中,$props() 替代了旧版的 export let 语法。它返回一个包含所有传入 props 的对象,使用解构赋值提取各个属性:
<!-- Card.svelte -->
<script lang="ts">
// 使用 $props() 声明并解构所有 props
let {
title,
description,
imageUrl = '/default.png', // 带默认值
badge, // 可选 prop
variant = 'default' // 联合类型 prop
} = $props<{
title: string;
description: string;
imageUrl?: string;
badge?: string;
variant?: 'default' | 'featured' | 'compact';
}>();
</script>
<article class="card {variant}">
<img src={imageUrl} alt={title} />
{#if badge}
<span class="badge">{badge}</span>
{/if}
<h2>{title}</h2>
<p>{description}</p>
</article>
<!-- 父组件使用 -->
<Card
title="Svelte 5 发布"
description="Runes 系统彻底改变响应式编程"
badge="新功能"
variant="featured"
/>
$$restProps — 传递剩余属性
当你需要将父组件传入的额外 HTML 属性透传给内部元素时,使用 $$restProps(Svelte 5 中通过 rest 解构实现):
<!-- Button.svelte —— 包装原生 button,支持所有 HTML 属性 -->
<script lang="ts">
import type { Snippet } from 'svelte';
let {
variant = 'primary',
loading = false,
children,
...restProps // 收集剩余 props(class, disabled, onclick 等)
} = $props<{
variant?: 'primary' | 'secondary' | 'danger';
loading?: boolean;
children: Snippet;
[key: string]: unknown;
}>();
</script>
<button
class="btn btn-{variant}"
disabled={loading}
{...restProps} <!-- 展开传递所有额外属性 -->
>
{#if loading}⟳ 加载中...{:else}{@render children()}{/if}
</button>
<!-- 父组件:可以传入任意 button 属性 -->
<Button variant="primary" type="submit" class="w-full">
提交表单
</Button>
Svelte 5 Snippets(替代 Slot)
什么是 Snippets?
Svelte 5 用 Snippets(片段)替代了 Svelte 4 的 <slot> 机制。Snippets 本质上是可复用的模板片段,可以在组件内部定义,也可以作为 props 从父组件传入子组件。
基本 Snippets 用法
<!-- Layout.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
let {
children, // 默认内容片段
header, // 自定义 header 片段
footer, // 可选 footer 片段
sidebar // 可选侧边栏片段
} = $props<{
children: Snippet;
header: Snippet;
footer?: Snippet;
sidebar?: Snippet;
}>();
</script>
<div class="layout">
<header>{@render header()}</header>
<div class="body">
{#if sidebar}
<aside>{@render sidebar()}</aside>
{/if}
<main>{@render children()}</main>
</div>
{#if footer}
<footer>{@render footer()}</footer>
{/if}
</div>
<!-- 父组件使用 -->
<Layout>
{#snippet header()}
<nav><a href="/">首页</a></nav>
{/snippet}
{#snippet sidebar()}
<ul>
<li><a href="/docs">文档</a></li>
</ul>
{/snippet}
<!-- children:标签内直接写的内容 -->
<h1>页面主要内容</h1>
<p>这里是正文...</p>
</Layout>
带参数的 Snippets(替代作用域 Slot)
Snippets 支持参数,这使得子组件可以向父组件提供的模板片段传入数据(替代 Svelte 4 的 slot let:):
<!-- DataTable.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
interface Column<T> {
key: keyof T;
label: string;
}
let {
data,
columns,
row // 父组件提供的行渲染片段,接收行数据
} = $props<{
data: Record<string, unknown>[];
columns: Column<any>[];
row?: Snippet<[Record<string, unknown>]>;
}>();
</script>
<table>
<thead>
<tr>
{#each columns as col}
<th>{col.label}</th>
{/each}
</tr>
</thead>
<tbody>
{#each data as item}
<tr>
{#if row}
{@render row(item)} <!-- 传入 item 给父组件的 snippet -->
{:else}
{#each columns as col}
<td>{item[col.key]}</td>
{/each}
{/if}
</tr>
{/each}
</tbody>
</table>
<!-- 父组件:自定义行渲染 -->
<DataTable {data} {columns}>
{#snippet row(item)}
<td style:color={item.status === 'active' ? 'green' : 'red'}>
{item.name}
</td>
<td>{item.status}</td>
{/snippet}
</DataTable>
相比旧版 slot,Snippets 是类型安全的——TypeScript 可以检查 snippet 参数的类型。Snippets 也可以在同一组件内部复用,不必传给子组件,非常适合减少重复模板代码。
$bindable():双向绑定 Props
export let value + 父组件 bind:value 的组合。<!-- Counter.svelte —— 可双向绑定的计数器 -->
<script lang="ts">
let {
// $bindable() 标记这个 prop 可被父组件 bind: 绑定
count = $bindable(0), // 参数是默认值
step = 1
} = $props<{
count?: number;
step?: number;
}>();
function increment() {
count += step; // 修改 count → 自动通知父组件
}
function decrement() {
count -= step;
}
</script>
<div class="counter">
<button onclick={decrement}>-</button>
<span>{count}</span>
<button onclick={increment}>+</button>
</div>
<!-- 父组件:使用 bind: 双向绑定 -->
<script lang="ts">
let quantity = $state(1);
</script>
<Counter bind:count={quantity} step={5} />
<p>父组件看到的数量:{quantity}</p>
<!-- 如果 prop 名与变量名相同,可以简写 -->
<Counter bind:count /> <!-- 等价于 bind:count={count} -->
- 谨慎使用双向绑定:会使数据流变得难以追踪。对于表单输入、受控组件等场景合适;对于业务状态应优先使用单向数据流(通过 callback prop 通知父组件)。
- 只有标记了 $bindable() 的 prop 才能被 bind::如果父组件尝试 bind: 一个普通 prop,Svelte 会在开发模式下发出警告。
- $bindable() 的参数是默认值:
$bindable(0)表示未传入时默认为 0,等同于解构默认值。
实际应用:受控输入组件
<!-- SearchInput.svelte —— 带防抖的搜索输入框 -->
<script lang="ts">
let {
value = $bindable(''), // 双向绑定搜索值
placeholder = '搜索...',
debounce = 300
} = $props<{
value?: string;
placeholder?: string;
debounce?: number;
}>();
let timer: ReturnType<typeof setTimeout>;
function handleInput(e: Event) {
const input = e.target as HTMLInputElement;
clearTimeout(timer);
// 防抖:延迟更新 value,减少触发频率
timer = setTimeout(() => {
value = input.value; // 赋值给 $bindable prop → 通知父组件
}, debounce);
}
</script>
<div class="search-input">
<input
type="search"
value={value}
{placeholder}
oninput={handleInput}
/>
{#if value}
<button onclick={() => value = ''}>✕</button>
{/if}
</div>
<!-- 父组件:实时响应搜索词变化 -->
<script lang="ts">
let searchTerm = $state('');
const results = $derived(
items.filter(item => item.name.includes(searchTerm))
);
</script>
<SearchInput bind:value={searchTerm} placeholder="搜索产品..." />
<p>找到 {results.length} 个结果</p>
组件事件:callback props 模式
Svelte 5 移除了 createEventDispatcher,改用更直观的 callback prop 模式:将函数作为 prop 传给子组件,子组件调用该函数来"触发事件"。这与 React 的 onChange/onXxx 模式类似,数据流更清晰。
dispatch('event', data) 改为调用对应的 callback prop。<!-- FileUploader.svelte —— 使用 callback prop 通知父组件 -->
<script lang="ts">
let {
accept = 'image/*',
maxSize = 5 * 1024 * 1024, // 默认 5MB
// callback props:类型为函数
onupload, // 上传成功回调
onerror, // 错误回调
onprogress // 进度回调(可选)
} = $props<{
accept?: string;
maxSize?: number;
onupload: (file: File, url: string) => void;
onerror: (message: string) => void;
onprogress?: (percent: number) => void;
}>();
let uploading = $state(false);
async function handleFileChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
if (file.size > maxSize) {
onerror(`文件超过 ${maxSize / 1024 / 1024}MB 限制`);
return;
}
uploading = true;
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const { url } = await response.json();
onupload(file, url); // 调用 callback prop,传入文件和 URL
} catch (err) {
onerror('上传失败,请重试');
} finally {
uploading = false;
}
}
</script>
<div class="uploader">
<input
type="file"
{accept}
disabled={uploading}
onchange={handleFileChange}
/>
{#if uploading}<span>上传中...</span>{/if}
</div>
<!-- 父组件:提供 callback -->
<script lang="ts">
let avatarUrl = $state('');
let errorMsg = $state('');
</script>
<FileUploader
accept="image/*"
maxSize={2 * 1024 * 1024}
onupload={(file, url) => { avatarUrl = url; }}
onerror={(msg) => { errorMsg = msg; }}
/>
{#if errorMsg}<p class="error">{errorMsg}</p>{/if}
{#if avatarUrl}<img src={avatarUrl} alt="头像" />{/if}
组件引用:bind:this
有时父组件需要直接调用子组件的方法(命令式接口)。Svelte 5 通过 bind:this 获取组件实例,子组件通过 $bindable() 暴露方法。
<!-- Modal.svelte —— 暴露 open/close 方法 -->
<script lang="ts">
let {
title,
children,
onclose
} = $props<{
title: string;
children: Snippet;
onclose?: () => void;
}>();
let isOpen = $state(false);
// 通过 $expose 暴露方法给父组件(Svelte 5.2+)
export function open() { isOpen = true; }
export function close() {
isOpen = false;
onclose?.();
}
</script>
{#if isOpen}
<div class="modal-overlay" onclick={close}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h2>{title}</h2>
{@render children()}
<button onclick={close}>关闭</button>
</div>
</div>
{/if}
<!-- 父组件:通过 bind:this 获取组件实例 -->
<script lang="ts">
import Modal from './Modal.svelte';
let modal: ReturnType<typeof Modal>;
function handleDeleteClick() {
modal.open(); // 命令式调用子组件方法
}
</script>
<Modal bind:this={modal} title="确认删除">
<p>此操作无法撤销,确认要删除吗?</p>
</Modal>
<button onclick={handleDeleteClick}>删除</button>
TypeScript 类型化 Props 的最佳实践
在生产级 Svelte 5 项目中,为 props 定义完整类型是必要的。以下是几种常见的 TypeScript 类型定义模式:
<!-- 方式1:内联类型(简单组件)-->
let { name, age = 0 } = $props<{ name: string; age?: number }>();
<!-- 方式2:外部 interface(复杂组件)-->
<script lang="ts">
import type { Snippet } from 'svelte';
// 定义 Props 接口(可以导出供父组件使用)
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
icon?: Snippet;
children: Snippet;
onclick?: (e: MouseEvent) => void;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
icon,
children,
onclick
}: ButtonProps = $props();
</script>
<!-- 方式3:泛型组件(如 Select<T>)-->
<script lang="ts" generics="T extends { id: string; label: string }">
let {
options,
value = $bindable(),
getLabel = (item: T) => item.label
} = $props<{
options: T[];
value?: T | null;
getLabel?: (item: T) => string;
}>();
</script>
<select
value={value?.id}
onchange={(e) => {
value = options.find(o => o.id === e.currentTarget.value) ?? null;
}}
>
{#each options as opt}
<option value={opt.id}>{getLabel(opt)}</option>
{/each}
</select>
组件设计模式
复合组件模式(Compound Components)
复合组件将一个功能拆分为多个协同工作的子组件,通过 Context 共享状态。适合 Accordion、Tabs、Menu 等组件。
<!-- Tabs.svelte —— 复合组件的容器 -->
<script lang="ts">
import { setContext } from 'svelte';
import type { Snippet } from 'svelte';
let {
defaultTab,
children
} = $props<{
defaultTab: string;
children: Snippet;
}>();
let activeTab = $state(defaultTab);
// 通过 Context 向子组件共享状态和方法
setContext('tabs', {
getActive: () => activeTab,
setActive: (tab: string) => { activeTab = tab; }
});
</script>
<div class="tabs">{@render children()}</div>
<!-- Tab.svelte —— 单个标签 -->
<script lang="ts">
import { getContext } from 'svelte';
import type { Snippet } from 'svelte';
let { id, label, children } = $props<{
id: string;
label: string;
children: Snippet;
}>();
const { getActive, setActive } = getContext<any>('tabs');
const isActive = $derived(getActive() === id);
</script>
<div>
<button
class:active={isActive}
onclick={() => setActive(id)}
>{label}</button>
{#if isActive}
<div class="tab-content">{@render children()}</div>
{/if}
</div>
<!-- 使用方式 -->
<Tabs defaultTab="profile">
<Tab id="profile" label="个人资料">
<ProfileForm />
</Tab>
<Tab id="security" label="安全设置">
<SecurityForm />
</Tab>
</Tabs>
Svelte 4 → Svelte 5 迁移对照表
| Svelte 4 | Svelte 5 等价写法 | 说明 |
|---|---|---|
export let value | let { value } = $props() | 接收 prop |
export let value = 0 | let { value = 0 } = $props() | 带默认值的 prop |
export let value + 父组件 bind: | let { value = $bindable() } = $props() | 双向绑定 |
<slot /> | {@render children()} | 默认插槽 |
<slot name="header" /> | {@render header()} | 具名插槽 |
<slot let:item={item} /> | {@render row(item)} | 作用域插槽 |
createEventDispatcher() | callback prop(函数类型的 prop) | 组件事件 |
$$restProps | let { a, ...rest } = $props() | 剩余属性 |
本章小结
- $props() 是 Svelte 5 接收 prop 的唯一方式:使用解构赋值提取各属性,支持默认值;与 TypeScript 泛型配合实现完整类型检查。
- rest 解构替代 $$restProps:
let { a, ...rest } = $props(),然后{...rest}展开到元素上,实现 prop 透传。 - Snippets 替代 slot:
{#snippet name(params)}定义,{@render name(args)}渲染;children是特殊的默认 snippet;带参数的 snippet 替代let:作用域插槽。 - $bindable() 用于双向绑定:只有标记了
$bindable()的 prop 才能被父组件bind:;应谨慎使用,优先单向数据流。 - Callback prop 替代 createEventDispatcher:将函数类型的 prop 传给子组件,子组件调用时向父组件传递数据;更类型安全,更显式。
- bind:this 获取组件实例:配合
export function暴露命令式接口;适合 Modal、Toast 等需要命令式控制的组件。 - 复合组件通过 setContext/getContext 共享状态:适合 Tabs、Accordion 等父子协作的 UI 组件。