彻底告别响应式陷阱:Svelte V5 Runes迁移实战指南

彻底告别响应式陷阱:Svelte V5 Runes迁移实战指南

【免费下载链接】svelte 网络应用的赛博增强。 【免费下载链接】svelte 项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

你是否还在为Svelte的响应式赋值挠头?是否被$:语法的隐式依赖搞得晕头转向?本文将带你一站式完成Svelte V5的迁移,掌握Runes带来的声明式响应式编程范式,让代码更清晰、更可控、更易于维护。

为什么选择Runes:响应式编程的革命

Svelte 5带来了全新的语法和响应式系统。虽然乍一看可能有所不同,但很快你就会发现许多相似之处。Runes(符文)是Svelte 5引入的核心概念,它们是用于控制Svelte编译器的特殊符号。如果你将Svelte视为一种语言,那么Runes就是它的语法部分——它们是关键字

Svelte Logo

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仍然是数字本身,你可以直接读写它,而无需像.valuegetCount()这样的包装器。

[!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仍然是数字本身,你可以直接读取它,无需像.valuegetDouble()这样的包装器。

$:语句也可用于创建副作用。在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:clickonclick)
  • 迁移插槽创建到渲染标签 (<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中,它们是函数,应该以不同的方式实例化。如果你需要手动实例化组件,应该使用mounthydrate(从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;

mounthydrate具有完全相同的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导入的createClassComponentasClassComponent来在实例化后保持与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的核心变化和步骤。

迁移过程可以总结为:

  1. 使用迁移脚本自动处理大部分机械转换
  2. 手动调整事件处理和生命周期相关代码
  3. 更新组件实例化和服务器渲染代码
  4. 逐步采用代码片段替代插槽,提升组件灵活性

要深入了解Svelte 5的所有变化,请查阅完整的迁移指南:07-v5-migration-guide.md

现在,你已经准备好拥抱Runes新时代,编写更清晰、更可维护的Svelte应用程序了!

相关资源

【免费下载链接】svelte 网络应用的赛博增强。 【免费下载链接】svelte 项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值