彻底告别响应式陷阱:Svelte V5 Runes迁移实战指南
【免费下载链接】svelte 网络应用的赛博增强。 项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
你是否还在为Svelte的响应式赋值挠头?是否被$:语法的隐式依赖搞得晕头转向?本文将带你一站式完成Svelte V5的迁移,掌握Runes带来的声明式响应式编程范式,让代码更清晰、更可控、更易于维护。
为什么选择Runes:响应式编程的革命
Svelte 5带来了全新的语法和响应式系统。虽然乍一看可能有所不同,但很快你就会发现许多相似之处。Runes(符文)是Svelte 5引入的核心概念,它们是用于控制Svelte编译器的特殊符号。如果你将Svelte视为一种语言,那么Runes就是它的语法部分——它们是关键字。
Runes以$为前缀,看起来像函数:
let message = $state('hello');
但它们与普通JavaScript函数有重要区别:
- 无需导入——它们是语言的一部分
- 不是值——不能赋值给变量或作为参数传递
- 如同JavaScript关键字,仅在特定位置有效(编译器会在错误位置时提供帮助)
有关Runes的更多信息,请参阅官方文档:01-what-are-runes.md
核心语法迁移:从旧范式到Runes
响应式变量:let → $state
在Svelte 4中,组件顶层的let声明是隐式响应式的。而在Svelte 5中,事情变得更加明确:当使用$state Rune创建变量时,该变量是响应式的。让我们通过将计数器包装在$state中来将其迁移到Runes模式:
<script>
let count = $state(0);
</script>
其他一切都没有改变。count仍然是数字本身,你可以直接读写它,而无需像.value或getCount()这样的包装器。
[!DETAILS] 为什么要这样做
let在顶层隐式响应式效果很好,但这意味着响应式受到限制——其他任何地方的let声明都不是响应式的。这迫使你在将代码重构出组件顶层以实现重用时不得不使用stores。这意味着你必须学习完全独立的响应式模型,而结果往往不那么好用。由于Svelte 5中的响应式更加明确,你可以在组件顶层之外继续使用相同的API。前往教程了解更多。
响应式语句:$: → $derived/$effect
在Svelte 4中,组件顶层的$:语句可用于声明派生状态(即完全通过其他状态的计算定义的状态)。在Svelte 5中,这通过$derived Rune实现:
<script>
let count = $state(0);
const double = $derived(count * 2);
</script>
与$state一样,其他一切都没有改变。double仍然是数字本身,你可以直接读取它,无需像.value或getDouble()这样的包装器。
$:语句也可用于创建副作用。在Svelte 5中,这通过$effect Rune实现:
<script>
let count = $state(0);
$effect(() => {
if (count > 5) {
alert('Count is too high!');
}
});
</script>
请注意,$effect的运行时机与$:不同。
组件属性:export let → $props
在Svelte 4中,组件的属性使用export let声明。每个属性都是一个声明。在Svelte 5中,所有属性都通过$props Rune通过解构声明:
<script>
let { optional = 'unset', required } = $props();
</script>
有关更多迁移细节,请参阅:07-v5-migration-guide.md
事件处理:从on:到属性语法
事件处理程序在Svelte 5中进行了改进。在Svelte 4中,我们使用on:指令将事件监听器附加到元素,而在Svelte 5中,它们像其他属性一样(换句话说——删除冒号):
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
由于它们只是属性,你可以使用正常的简写语法:
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
组件事件:从createEventDispatcher到回调属性
在Svelte 4中,组件可以通过createEventDispatcher创建调度器来发出事件。
此函数在Svelte 5中已弃用。相反,组件应接受回调属性——这意味着你可以将函数作为属性传递给这些组件:
App.svelte
<script>
import Pump from './Pump.svelte';
let size = $state(15);
let burst = $state(false);
function reset() {
size = 15;
burst = false;
}
</script>
<Pump
inflate={(power) => {
size += power;
if (size > 75) burst = true;
}}
deflate={(power) => {
if (size > 0) size -= power;
}}
/>
{#if burst}
<button onclick={reset}>new balloon</button>
<span class="boom">💥</span>
{:else}
<span class="balloon" style="scale: {0.01 * size}">
🎈
</span>
{/if}
Pump.svelte
<script>
let { inflate, deflate } = $props();
let power = $state(5);
</script>
<button onclick={() => inflate(power)}>
inflate
</button>
<button onclick={() => deflate(power)}>
deflate
</button>
<button onclick={() => power--}>-</button>
Pump power: {power}
<button onclick={() => power++}>+</button>
插槽到代码片段:更强大的内容分发
在Svelte 4中,可以使用插槽将内容传递给组件。Svelte 5用更强大、更灵活的代码片段(Snippets)取代了它们,因此插槽在Svelte 5中已弃用。
它们仍然可以工作,但是,你可以将代码片段传递给使用插槽的组件:
Child.svelte
<slot />
<hr />
<slot name="foo" message="hello" />
Parent.svelte
<script>
import Child from './Child.svelte';
</script>
<Child>
default child content
{#snippet foo({ message })}
message from child: {message}
{/snippet}
</Child>
(反之则不行——你不能将插槽内容传递给使用{@render ...}标签的组件。)
默认内容
在Svelte 4中,将一段UI传递给子组件的最简单方法是使用<slot />。在Svelte 5中,这通过children属性完成,然后使用{@render children()}显示:
<script>
let { children } = $props();
</script>
{@render children?.()}
多个内容占位符
如果你需要多个UI占位符,则必须使用命名插槽。在Svelte 5中,改用属性,可以随意命名并{@render ...}它们:
<script>
let { header, main, footer } = $props();
</script>
<header>
{@render header()}
</header>
<main>
{@render main()}
</main>
<footer>
{@render footer()}
</footer>
自动化迁移:使用迁移脚本
到目前为止,你应该对前后变化以及旧语法与新语法的关系有了很好的了解。你可能还意识到许多迁移是技术性和重复性的——这不是你想手动完成的事情。
我们也这么认为,这就是为什么我们提供迁移脚本来自动完成大部分迁移。你可以使用npx sv migrate svelte-5升级项目。这将执行以下操作:
- bump package.json中的核心依赖
- 迁移到runes (
let→$state等) - 迁移到DOM元素的事件属性 (
on:click→onclick) - 迁移插槽创建到渲染标签 (
<slot />→{@render children()}) - 迁移插槽使用到代码片段 (
<div slot="x">...</div>→{#snippet x()}<div>...</div>{/snippet}) - 迁移明显的组件创建 (
new Component(...)→mount(Component, ...))
你还可以通过"迁移组件到Svelte 5语法"命令在VS Code中迁移单个组件,或在我们的Playground中通过"迁移"按钮。
并非所有内容都可以自动迁移,有些迁移需要手动清理。以下部分将更详细地描述这些内容。
run函数处理
你可能会看到迁移脚本将一些$:语句转换为从svelte/legacy导入的run函数。如果迁移脚本无法可靠地将语句迁移到$derived并断定这是副作用,则会发生这种情况。在某些情况下这可能是错误的,最好改为使用$derived。在其他情况下,它可能是正确的,但由于$:语句也在服务器上运行而$effect不运行,因此不能安全地将其转换为$effect。相反,run用作权宜之计。run模仿$:的大部分特性,它在服务器上运行一次,并在客户端上作为$effect.pre运行($effect.pre在更改应用到DOM之前运行;你很可能想要使用$effect代替)。
<script>
$effect(() => {
// some side effect code
})
</script>
事件修饰符处理
事件修饰符不适用于事件属性(例如,你不能执行onclick|preventDefault={...})。因此,在将事件指令迁移到事件属性时,我们需要这些修饰符的函数替换。这些是从svelte/legacy导入的,应该迁移到例如使用event.preventDefault()。
<script>
</script>
<button
onclick={(event) => {
event.preventDefault();
// ...
}}
>
click me
</button>
手动迁移注意事项
迁移脚本不会转换createEventDispatcher。你需要手动调整这些部分。它不这样做是因为风险太大,因为它可能导致组件用户的中断,而迁移脚本无法发现。
迁移脚本不会转换beforeUpdate/afterUpdate。它不这样做是因为无法确定代码的实际意图。根据经验,你通常可以结合使用$effect.pre(与beforeUpdate同时运行)和tick(从svelte导入,允许你等待更改应用到DOM,然后执行一些工作)。
组件实例化:从类到函数
在Svelte 3和4中,组件是类。在Svelte 5中,它们是函数,应该以不同的方式实例化。如果你需要手动实例化组件,应该使用mount或hydrate(从svelte导入)。如果你在使用SvelteKit时看到此错误,请尝试先更新到最新版本的SvelteKit,它添加了对Svelte 5的支持。如果你在不使用SvelteKit的情况下使用Svelte,你可能需要调整main.js文件(或类似文件):
import { mount } from 'svelte';
import App from './App.svelte'
const app = mount(App, { target: document.getElementById("app") });
export default app;
mount和hydrate具有完全相同的API。不同之处在于hydrate会在其目标中获取Svelte的服务器渲染HTML并对其进行 hydration。两者都返回一个包含组件导出和潜在属性访问器的对象(如果使用accessors: true编译)。它们没有你可能从类组件API中知道的$on、$set和$destroy方法。以下是它们的替代品:
$on的替代方案
对于$on,不要监听事件,而是通过选项参数上的events属性传递它们。
import { mount } from 'svelte';
import App from './App.svelte'
const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });
[!NOTE] 不鼓励使用
events——相反,使用回调
$set的替代方案
对于$set,使用$state创建响应式属性对象并对其进行操作。如果你在.js或.ts文件中执行此操作,请调整结尾以包含.svelte,即.svelte.js或.svelte.ts。
import { mount } from 'svelte';
import App from './App.svelte'
const props = $state({ foo: 'bar' });
const app = mount(App, { target: document.getElementById("app"), props });
props.foo = 'baz';
$destroy的替代方案
对于$destroy,使用unmount代替。
import { mount, unmount } from 'svelte';
import App from './App.svelte'
const app = mount(App, { target: document.getElementById("app") });
unmount(app);
作为权宜之计,你也可以使用svelte/legacy导入的createClassComponent或asClassComponent来在实例化后保持与Svelte 4相同的API。
import { createClassComponent } from 'svelte/legacy';+++
import App from './App.svelte'
const app = createClassComponent({ component: App, target: document.getElementById("app") });
export default app;
如果此组件不受你控制,你可以使用compatibility.componentApi编译器选项进行自动应用的向后兼容性,这意味着使用new Component(...)的代码无需调整即可继续工作(请注意,这会给每个组件实例增加一些开销)。这还将为通过bind:this获得的所有组件实例添加$set和$on方法。
/// svelte.config.js
export default {
compilerOptions: {
compatibility: {
componentApi: 4
}
}
};
服务器API变化
同样,组件在编译用于服务器端渲染时不再具有render方法。相反,将函数传递给svelte/server中的render:
import { render } from 'svelte/server';
import App from './App.svelte';
const { html, head } = render(App, { props: { message: 'hello' } });
在Svelte 4中,将组件渲染为字符串还返回所有组件的CSS。在Svelte 5中,默认情况下不再是这种情况,因为大多数时候你使用的工具链会以其他方式处理它(如SvelteKit)。如果你需要从render返回CSS,可以将css编译器选项设置为'injected',它会将<style>元素添加到head。
总结与下一步
Svelte V5的Runes系统为响应式编程带来了更明确、更强大的范式。通过本文档,你已经了解了从Svelte 4迁移到Svelte 5的核心变化和步骤。
迁移过程可以总结为:
- 使用迁移脚本自动处理大部分机械转换
- 手动调整事件处理和生命周期相关代码
- 更新组件实例化和服务器渲染代码
- 逐步采用代码片段替代插槽,提升组件灵活性
要深入了解Svelte 5的所有变化,请查阅完整的迁移指南:07-v5-migration-guide.md
现在,你已经准备好拥抱Runes新时代,编写更清晰、更可维护的Svelte应用程序了!
相关资源
- 官方Runes文档:02-runes
- Svelte 5模板语法:03-template-syntax
- 组件API参考:21-svelte.md
【免费下载链接】svelte 网络应用的赛博增强。 项目地址: https://gitcode.com/GitHub_Trending/sv/svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




