Skip to content

Svelte 最新中文文档教程(22)—— Svelte 5 迁移指南 #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
mqyqingfeng opened this issue Mar 11, 2025 · 0 comments
Open

Comments

@mqyqingfeng
Copy link
Owner

前言

Svelte,一个语法简洁、入门容易,面向未来的前端框架。

从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

image.png

Svelte 以其独特的编译时优化机制著称,具有轻量级高性能易上手等特性,非常适合构建轻量级 Web 项目

为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。

如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

Svelte 5 迁移指南

Svelte 5 采用了全面改进的语法和响应性系统。虽然开始时可能看起来有所不同,但您很快会注意到许多相似之处。本指南详细介绍了这些变化,并向您展示如何升级。同时,我们还提供了关于我们为什么做出这些改变的信息。

您不必立即迁移至新语法 —— Svelte 5 仍然支持旧的 Svelte 4 语法,您可以将使用新语法的组件与使用旧语法的组件混合使用。我们预计很多人可以通过仅修改几行代码就完成升级。还有一个 迁移脚本 可以帮助您自动完成许多步骤。

响应性语法变化

Svelte 5 的核心是新的符文 API。符文基本上是编译器指令,告诉 Svelte 有关响应性的信息。在语法上,符文是以美元符号开头的函数。

let -> $state

在 Svelte 4 中,组件顶层的 let 声明是隐式响应式的。在 Svelte 5 中,事情变得更明确:当使用 $state 符文创建变量时,该变量是响应式的。让我们通过将计数器包装在 $state 中来迁移到符文模式:

<script>
	let count = +++$state(+++0+++)+++;
</script>

其他方面没有变化。count 仍然是数字本身,您可以直接读写它,没有 .valuegetCount() 这样的包装器。

[!DETAILS] 我们为什么这样做
let 在顶层隐式声明响应式工作良好,但这意味着响应性受到限制——在其他地方的 let 声明都不是响应式的。这迫使您在重构代码以便复用时不得不使用 store。这意味着您必须学习一个完全不同的响应模型,结果通常并不那么好用。由于 Svelte 5 中的响应性更明确,您可以在组件顶层之外继续使用相同的 API。请前往 教程 了解更多信息。

$: -> $derived/$effect

在 Svelte 4 中,组件顶层的 $: 语句可用于声明派生,即完全通过其他状态的计算来定义的状态。在 Svelte 5 中,可以使用 $derived 符文实现这一点:

<script>
	let count = +++$state(+++0+++)+++;
	---$:--- +++const+++ double = +++$derived(+++count * 2+++)+++;
</script>

$state 一样,其他方面没有变化。double 仍然是数字本身,您可以直接读取它,而不需要像 .valuegetDouble() 这样的包装器。

$: 语句还可以用于创建副作用。在 Svelte 5 中,可以使用 $effect 符文实现这一点:

<script>
	let count = +++$state(+++0+++)+++;
	---$:---+++$effect(() =>+++ {
		if (count > 5) {
			alert('Count is too high!');
		}
	}+++);+++
</script>

[!DETAILS] 我们为什么这样做
$: 是一个很好的简写,容易上手:您可以在大多数代码前加上 $: 它就能以某种方式工作。这种直观性也是它的缺点,因为您的代码变得更复杂时,它并不那么好理解。代码的意图是创建一个派生,还是创建一个副作用?使用 $derived$effect,您需要进行更多的前期决策(剧透:90% 的时候您想要 $derived),但将来您和团队中的其他开发人员会更容易理解。

还有一些难以发现的陷阱:

  • $: 仅在渲染之前直接更新,这意味着在重新渲染之间你可能会读取到过时的值
  • $: 仅在每个 tick 中运行一次,这意味着语句的运行频率可能低于你的预期
  • $: 依赖关系是通过对依赖项的静态分析确定的。这在大多数情况下有效,但在重构过程中可能会以微妙的方式出错,例如依赖项被移动到一个函数中,从而不再可见
  • $: 语句的顺序也是通过对依赖项的静态分析来确定的。在某些情况下可能会出现平局,导致排序错误,需要手动干预。在重构代码时,顺序也可能会出错,某些依赖项因此不再可见。

最后,它对 TypeScript 不友好(我们的编辑器工具需要跳过一些环节才能使其对 TypeScript 有效),这是使 Svelte 的响应模型真正通用的障碍。

$derived$effect 解决了所有这些问题:

  • 始终返回最新值
  • 根据需要运行以保持稳定
  • 在运行时确定依赖关系,因此对重构免疫
  • 根据需要执行依赖关系,因此免受排序问题的影响
  • 对于 TypeScript 友好

export let -> $props

在 Svelte 4 中,组件的属性是通过 export let 声明的。每个属性都是一个声明。在 Svelte 5 中,所有属性都是通过 $props 符文声明的,通过解构:

<script>
	---export let optional = 'unset';
	export let required;---
	+++let { optional = 'unset', required } = $props();+++
</script>

在某些情况下,声明属性变得不如有几个 export let 声明那样简单:

  • 您想重命名属性,例如因为名称是保留标识符(例如 class
  • 您不知道预期还有哪些其他属性
  • 您想将每个属性转发到另一个组件

在 Svelte 4 中,所有这些情况都需要特殊语法:

  • 重命名:export { klass as class}
  • 其他属性:$$restProps
  • 所有属性:$$props

在 Svelte 5 中,$props 符文使这变得简单,无需任何额外的 Svelte 特定语法:

  • 重命名:使用属性重命名 let { class: klass } = $props();
  • 其他属性:使用展开语法 let { foo, bar, ...rest } = $props();
  • 所有属性:不要解构 let props = $props();
<script>
	---let klass = '';
	export { klass as class};---
	+++let { class: klass, ...rest } = $props();+++
</script>
<button class={klass} {...---$$restProps---+++rest+++}>点击我</button>

[!DETAILS] 我们为什么这样做
export let 是一个颇具争议的 API 决策,围绕您是否应该考虑属性被 exportimport 存在了很多争论。$props 没有这种特性。这也与其他符文保持一致,总体思路简化为“在 Svelte 中,所有与响应性有关的都是符文”。

export let 还存在许多局限性,需要额外的 API 去解决,如上所示。$props 将这些统一为一个语法概念,严重依赖常规的 JavaScript 解构语法

事件变化

在 Svelte 5 中,事件处理程序进行了改头换面。在 Svelte 4 中,我们使用 on: 指令将事件监听器附加到元素上,而在 Svelte 5 中,它们像其他属性一样(换句话说 —— 去掉冒号):

<script>
	let count = $state(0);
</script>

<button on---:---click={() => count++}>
	点击次数:{count}
</button>

由于它们只是属性,您可以使用正常的简写语法...

<script>
	let count = $state(0);

	function onclick() {
		count++;
	}
</script>

<button {onclick}>
	点击次数:{count}
</button>

...尽管在使用命名事件处理函数时,通常最好使用更具描述性的名称。

组件事件

在 Svelte 4 中,组件可以使用 createEventDispatcher 创建一个调度器来发出事件。

该函数在 Svelte 5 中已弃用。相反,组件应接受 回调属性 —— 这意味着您可以将函数作为属性传递给这些组件:

<!--- file: App.svelte --->
<script>
	import Pump from './Pump.svelte';

	let size = $state(15);
	let burst = $state(false);

	function reset() {
		size = 15;
		burst = false;
	}
</script>

<Pump
	---on:---inflate={(power) => {
		size += power---.detail---;
		if (size > 75) burst = true;
	}}
	---on:---deflate={(power) => {
		if (size > 0) size -= power---.detail---;
	}}
/>

{#if burst}
	<button onclick={reset}>新气球</button>
	<span class="boom">💥</span>
{:else}
	<span class="balloon" style="scale: {0.01 * size}">
		🎈
	</span>
{/if}
<!--- file: Pump.svelte --->
<script>
    ---import { createEventDispatcher } from 'svelte';
    const dispatch = createEventDispatcher();
    ---
	+++let { inflate, deflate } = $props();+++
	let power = $state(5);
</script>

<button onclick={() => ---dispatch('inflate', power)---+++inflate(power)+++}>
	充气
</button>
<button onclick={() => ---dispatch('deflate', power)---+++deflate(power)+++}>
	放气
</button>
<button onclick={() => power--}>-</button>
泵的气压:{power}
<button onclick={() => power++}>+</button>

事件冒泡

组件应该接受一个 onclick 回调属性,而不是通过 <button on:click> 将事件从元素“转发”到组件:

<script>
	+++let { onclick } = $props();+++
</script>

<button ---on:click--- +++{onclick}+++>
	点击我
</button>

请注意,这也意味着您可以将事件处理程序与其他属性一起“展开”到元素上,而不必繁琐地单独转发每个事件:

<script>
	let props = $props();
</script>

<button ---{...$$props} on:click on:keydown on:all_the_other_stuff--- +++{...props}+++>
	点击我
</button>

事件修饰符

在 Svelte 4 中,您可以向事件处理程序添加事件修饰符:

<button on:click|once|preventDefault={handler}>...</button>

修饰符特定于 on:,因此不适用于现代事件处理程序。在处理程序内部添加 event.preventDefault() 等内容是更可取的,因为所有逻辑都集中在一个地方,而不是拆分在处理程序和修饰符之间。

由于事件处理程序只是函数,您可以根据需要创建自己的封装:

<script>
	function once(fn) {
		return function (event) {
			if (fn) fn.call(this, event);
			fn = null;
		};
	}

	function preventDefault(fn) {
		return function (event) {
			event.preventDefault();
			fn.call(this, event);
		};
	}
</script>

<button onclick={once(preventDefault(handler))}>...</button>

有三个修饰符——capturepassivenonpassive —— 不能被表示为包装函数,因为它们需要在事件处理程序绑定时应用,而不是在运行时应用。

对于 capture,我们将修饰符添加到事件名称中:

<button onclickcapture={...}>...</button>

更改事件处理程序的 passive 选项并不是轻而易举的事情。如果您有此用例——您可能没有!——那么您需要使用一个 action 来自己应用事件处理程序。

多个事件处理程序

在 Svelte 4 中,这样做是可以的:

<button on:click={one} on:click={two}>...</button>

元素上的重复特性/属性 —— 现在包括事件处理程序 —— 是不允许的。相反,请改为这样做:

<button
	onclick={(e) => {
		one(e);
		two(e);
	}}
>
	...
</button>

在展开属性时,本地事件处理程序必须在展开之后,否则可能会被覆盖:

<button
	{...props}
	onclick={(e) => {
		doStuff(e);
		props.onclick?.(e);
	}}
>
	...
</button>

[!DETAILS] 我们为什么这样做
createEventDispatcher 一直有点模板化:

  • 导入函数
  • 调用该函数以获取调度函数
  • 使用字符串和可能的有效负载调用该调度函数
  • 通过 .detail 属性在另一端检索该有效负载,因为事件本身始终是 CustomEvent

一直可以使用组件回调属性,但由于您必须使用 on: 监听 DOM 事件,因此出于语法一致性,使用 createEventDispatcher 处理组件事件是有意义的。现在我们有了事件属性(onclick),情况正好相反:回调属性现在是更合理的选择。

放弃事件修饰符无疑是对那些喜欢事件修饰符简写语法的人的一种倒退。考虑到它们并不常用,我们用更小的表面积换取了更明确性。修饰符也不一致,因为它们中的大多数只能用于 DOM 元素。

同一事件的多个监听器也不再可能,但这本身就是一种反模式,因为它妨碍了可读性:如果有很多属性,则更难发现有两个处理程序,除非它们紧挨在一起。它还暗示这两个处理程序是独立的,而实际上,如果 one 内部包含 event.stopImmediatePropagation(),会阻止 two 被调用。

通过弃用 createEventDispatcheron: 指令,改为使用回调属性和普通元素属性,我们:

  • 降低了 Svelte 的学习曲线
  • 消除了样板代码,特别是在 createEventDispatcher 周围
  • 消除了为可能没有监听者的事件创建 CustomEvent 对象的开销
  • 增加了展开事件处理程序的能力
  • 增加了了解哪些事件处理程序被提供给组件的能力
  • 增加了表达给定事件处理程序是必需的还是可选的能力
  • 提高了类型安全性(之前,Svelte 实际上无法保证组件不发出特定事件)

代码片段而非插槽

在 Svelte 4 中,可以使用插槽将内容传递给组件。Svelte 5 用更强大和灵活的代码片段替换了它们,因此插槽在 Svelte 5 中被弃用。

不过,它们仍然可以继续使用,您可以在组件中混合使用代码片段和插槽。

在使用自定义元素时,您仍然应该像以前一样使用 <slot />。在未来的版本中,当 Svelte 移除其内部版本的插槽时,它将保持这些插槽不变,即输出一个常规的 DOM 标签,而不是进行转换。

默认内容

在 Svelte 4 中,传递 UI 给子组件的最简单方法是使用一个 <slot />。在 Svelte 5 中,改为使用 children 属性,然后通过 {@render children()} 显示:

<script>
	+++let { children } = $props();+++
</script>

---<slot />---
+++{@render children?.()}+++

多个内容占位符

如果您想要多个 UI 占位符,您必须使用命名插槽。在 Svelte 5 中,改为使用 props,随意命名它们,并 {@render ...} 它们:

<script>
	+++let { header, main, footer } = $props();+++
</script>

<header>
	---<slot name="header" />---
	+++{@render header()}+++
</header>

<main>
	---<slot name="main" />---
	+++{@render main()}+++
</main>

<footer>
	---<slot name="footer" />---
	+++{@render footer()}+++
</footer>

向上传递数据

在 Svelte 4 中,您将数据传递给 <slot />,然后在父组件中使用 let: 检索它。在 Svelte 5 中,代码片段承担了这一责任:

<!--- file: App.svelte --->
<script>
	import List from './List.svelte';
</script>

<List items={['one', 'two', 'three']} ---let:item--->
	+++{#snippet item(text)}+++
		<span>{text}</span>
	+++{/snippet}+++
	---<span slot="empty">尚无条目</span>---
	+++{#snippet empty()}
		<span>尚无条目</span>
	{/snippet}+++
</List>
<!--- file: List.svelte --->
<script>
	let { items, +++item, empty+++ } = $props();
</script>

{#if items.length}
	<ul>
		{#each items as entry}
			<li>
				---<slot item={entry} />---
				+++{@render item(entry)}+++
			</li>
		{/each}
	</ul>
{:else}
	---<slot name="empty" />---
	+++{@render empty?.()}+++
{/if}

[!DETAILS] 我们为什么这样做
插槽易于上手,但随着用例的复杂性增加,语法越发复杂和令人困惑:

  • let: 语法让许多人感到困惑,因为它创建了一个变量,而所有其他 : 指令则是接收一个变量
  • let: 声明的变量的作用域并不清晰。在上面的例子中,您可能会认为可以在 empty 插槽中使用 item 插槽属性,但事实并非如此
  • 命名插槽必须使用 slot 属性应用于元素。有时您不希望创建一个元素,因此我们不得不添加 <svelte:fragment> API
  • 命名插槽也可以应用于组件,这改变了 let: 指令可用范围的语义(即使在今天,我们的维护者也常常不知道它的工作原理)

代码片段通过更具可读性和清晰性解决了所有这些问题。同时,它们更加强大,因为它们允许您定义可以在 任何地方 渲染的 UI 部分,而不仅仅是将其作为 props 传递给组件。

迁移脚本

到目前为止,您应该对之前/之后的情况以及旧语法与新语法的关系有了相当不错的理解。您可能也意识到了,很多迁移都是相当技术性和重复的,您并不想手动完成这些事情。

我们也是这样认为的,这就是为什么我们提供了迁移脚本,用于自动完成大部分迁移。您可以使用 npx sv migrate svelte-5 升级您的项目。这将执行以下操作:

  • 更新您的 package.json 中的核心依赖项
  • 迁移到符文(let -> $state 等)
  • 将 DOM 元素的事件属性迁移为事件属性(on:click -> onclick
  • 将插槽创建迁移为渲染标签(<slot /> -> {@render children()}
  • 将插槽用法迁移至片段(<div slot="x">...</div> -> {#snippet x()}<div>...</div>{/snippet})
  • 迁移明显的组件创建(new Component(...) -> mount(Component, ...)

您还可以通过 VS Code 中的 Migrate Component to Svelte 5 Syntax 命令迁移单个组件,或在我们的 Playground 中通过 Migrate 按钮完成。

并非所有内容都可以自动迁移,一些迁移在之后需要手动清理。以下部分将更详细地描述这些内容。

run

您可能会看到迁移脚本将一些 $: 语句转换为从 svelte/legacy 导入的 run 函数。如果迁移脚本无法可靠地将语句迁移到 $derived 并得出结论这是一个副作用,就会发生这种情况。

在某些情况下,这可能是错误的,最好将其改为使用 $derived。在其他情况下,这可能是正确的,但由于 $: 语句在服务端也会运行,而 $effect 不会,因此不能安全地转换它。于是,run 被用作权宜之计。run 模拟了 $: 的大多数特征,因为它在服务端上运行一次,并在客户端作为 $effect.pre 运行($effect.pre 在更改应用于 DOM 之前运行;您最有可能想要使用 $effect 代替)。

<script>
	---import { run } from 'svelte/legacy';---
	---run(() => {---
	+++$effect(() => {+++
		// 一些副作用代码
	})
</script>

事件修饰符

事件修饰符不适用于事件属性(例如,您不能做 onclick|preventDefault={...})。因此,当将事件指令迁移到事件属性时,我们需要一个函数替代这些修饰符。这些从 svelte/legacy 中导入,帮助支持迁移,例如仅使用 event.preventDefault()

<script>
	---import { preventDefault } from 'svelte/legacy';---
</script>

<button
	onclick={---preventDefault---((event) => {
		+++event.preventDefault();+++
		// ...
	})}
>
	点击我
</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 = new App({ target: document.getElementById("app") });---
+++const app = mount(App, { target: document.getElementById("app") });+++

export default app;

mounthydrate 具有完全相同的 API。不同之处在于 hydrate 会在其目标内提取 Svelte 的服务端渲染 HTML 并进行水合。两者都返回一个包含组件导出的对象以及可能的属性访问器(如果编译时使用 accessors: true)。它们不包含您可能熟悉的类组件 API 中的 $on$set$destroy 方法。这些是它的替代品:

对于 $on,不要监听事件,而是通过 events 属性在选项参数中传递它们。

+++import { mount } from 'svelte';+++
import App from './App.svelte'

---const app = new App({ target: document.getElementById("app") });
app.$on('event', callback);---
+++const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });+++

[!NOTE] 请注意,使用 events 是不推荐的——请改为 使用回调

对于 $set,请使用 $state 来创建一个响应式属性对象并进行操作。如果您在 .js.ts 文件中执行此操作,请调整文件结尾包含 .svelte,即 .svelte.js.svelte.ts

+++import { mount } from 'svelte';+++
import App from './App.svelte'

---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$set({ foo: 'baz' });---
+++const props = $state({ foo: 'bar' });
const app = mount(App, { target: document.getElementById("app"), props });
props.foo = 'baz';+++

对于 $destroy,请使用 unmount 代替。

+++import { mount, unmount } from 'svelte';+++
import App from './App.svelte'

---const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$destroy();---
+++const app = mount(App, { target: document.getElementById("app") });
unmount(app);+++

作为权宜之计,您还可以使用 createClassComponentasClassComponent(从 svelte/legacy 导入)来保持 保持在实例化后与 Svelte 4 相同的 API。

+++import { createClassComponent } from 'svelte/legacy';+++
import App from './App.svelte'

---const app = new App({ target: document.getElementById("app") });---
+++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
		}
	}
};

注意 mounthydrate 不是同步的,因此类似 onMount 这样的内容在函数返回时不会被调用,待处理的 Promise 块尚未呈现(因为 #await 等待一个微任务以等待一个可能立即 resolve 的 Promise)。如果您需要这个保证,在调用 mount/hydrate 之后调用 flushSync(从 'svelte' 导入)。

服务端 API 变化

同样,组件在服务端渲染编译时,不再具有 render 方法。相反,将函数传递给 svelte/serverrender

+++import { render } from 'svelte/server';+++
import App from './App.svelte';

---const { html, head } = App.render({ props: { message: 'hello' }});---
+++const { html, head } = render(App, { props: { message: 'hello' }});+++

在 Svelte 4 中,将组件渲染为字符串也会返回所有组件的 CSS。在 Svelte 5 中,默认情况下不再这样,因为大多数情况下您使用工具链以其他方式处理它(例如 SvelteKit)。如果您需要从 render 返回 CSS,您可以将 css 编译器选项设置为 'injected',它将在 head 中添加 <style> 元素。

组件类型变化

从类到函数的变化也反映在类型定义中:SvelteComponent,Svelte 4 的基类已被弃用,取而代之的是新的 Component 类型,它定义了 Svelte 组件的函数形状。要在 d.ts 文件中手动定义组件形状:

import type { Component } from 'svelte';
export declare const MyComponent: Component<{
	foo: string;
}>;

声明某种类型的组件是必需的:

<script lang="ts">
	import type { ---SvelteComponent--- +++Component+++ } from 'svelte';
	import {
		ComponentA,
		ComponentB
	} from 'component-library';

	---let component: typeof SvelteComponent<{ foo: string }>---
	+++let component: Component<{ foo: string }>+++ = $state(
		Math.random() ? ComponentA : ComponentB
	);
</script>

<svelte:component this={component} foo="bar" />

两个工具类型 ComponentEventsComponentType 已被弃用。因为事件现在被定义为回调属性,而 ComponentEvents 已过时,因为新的 Component 类型已经是组件类型(例如 ComponentType<SvelteComponent<{ prop: string }>> == Component<{ prop: string }>)。

bind:this 变化

由于组件不再是类,使用 bind:this 不再返回带有 $set$on$destroy 方法的类实例。它仅返回实例导出(export function/const),并且如果您使用 accessors 选项,则返回每个属性的 getter/setter 对。

空格处理变化

此前,Svelte 使用了一个非常复杂的算法来确定是否应该保留空格。Svelte 5 简化了这一点,使开发人员更容易理解。规则如下:

  • 节点之间的空格被折叠为一个空格
  • 标签开头和结尾的空格被完全移除
  • 某些例外情况,例如在 pre 标签内保留空格

和之前一样,您可以通过在编译器设置中将 preserveWhitespace 选项设置为 true 或在 <svelte:options> 中针对每个组件设置来禁用空格修剪。

需要现代浏览器

Svelte 5 需要现代浏览器(换句话说,不支持 Internet Explorer),原因如下:

  • 它使用 Proxies
  • 具有 clientWidth/clientHeight/offsetWidth/offsetHeight 绑定的元素使用 ResizeObserver,而不是复杂的 <iframe> 技巧
  • <input type="range" bind:value={...} /> 仅使用 input 事件监听器,而不是同时监听 change 事件作为后备方案

legacy 编译器选项(该选项生成体积较大但兼容 IE 的代码)不再存在。

编译器选项的变化

  • 从 css 选项中移除了 false / true(之前已经弃用)和 "none" 这些有效值
  • legacy 选项被重新调整用途
  • hydratable 选项已被移除。Svelte 组件现在始终是可水合的
  • enableSourcemap 选项已被移除。现在始终生成 source map,工具可以选择忽略它
  • tag 选项已被移除。请改用组件内的 <svelte:options customElement="tag-name" />
  • loopGuardTimeoutformatsveltePatherrorModevarsReport 选项已被移除

children 属性被保留

组件标签里的内容变为名为 children 的代码片段属性。你不能使用相同的名称定义其他属性。

点符号表示组件

在 Svelte 4 中,<foo.bar> 将创建一个标签名为 "foo.bar" 的元素。在 Svelte 5 中,foo.bar 被视为组件。这在 each 块中特别有用:

{#each items as item}
	<item.component {...item.props} />
{/each}

符文模式中的重大变化

某些重大变更仅在组件处于符文模式时才适用。

不允许绑定到组件导出

符文模式下,组件的导出不能直接绑定。例如,组件 A 中有 export const foo = ...,然后执行 <A bind:foo />,将导致错误。使用 bind:this 代替——<A bind:this={a} />——并通过 a.foo 访问导出。此更改使事情更容易理解,因为它强制了属性和导出之间的清晰分离。

绑定需要使用 $bindable() 显式定义

在 Svelte 4 语法中,每个属性(通过 export let 声明)都是可绑定的,这意味着您可以对其使用 bind:。在符文模式中,属性默认不具有可绑定性:您需要使用 $bindable 符文来标记可绑定的 props。

如果一个可绑定属性有默认值(例如 let { foo = $bindable('bar') } = $props();),当你要绑定该属性时,需要传递一个非 undefined 的值。这可以防止出现模棱两可的行为 —— 父组件和子组件必须具有相同的值,并能获得更好的性能(在 Svelte 4 中,默认值被反映回父组件,导致额外的无用渲染周期)。

accessors 选项被忽略

accessors 选项设置为 true 可使组件的属性在组件实例上直接访问。在符文模式下,属性永远不会在组件实例上访问。如果您需要暴露它们,可以使用组件导出。

immutable 选项被忽略

在符文模式下,设置 immutable 选项没有效果。这个概念被 $state 及其变体的工作方式所替代。

类不再是“自动响应式”

在 Svelte 4 中,执行以下操作会触发响应性:

<script>
	let foo = new Foo();
</script>

<button on:click={() => (foo.value = 1)}>{foo.value}</button>

这是因为 Svelte 编译器将对 foo.value 的赋值视为更新所有引用 foo 的内容的指令。在 Svelte 5 中,响应性在运行时而不是编译时确定,因此您应该将 value 定义为 Foo 类上的响应式 $state 字段。将 new Foo() 包装在 $state(...) 中将不会产生任何效果——只有简单的对象和数组会被深度响应式化。

<svelte:component> 不再必要

在 Svelte 4 中,组件是 静态的 —— 如果您渲染 <Thing>,并且 Thing 的值发生变化,不会发生任何事情。要使其动态,必须使用 <svelte:component>

在 Svelte 5 中,这不再成立:

<script>
	import A from './A.svelte';
	import B from './B.svelte';

	let Thing = $state();
</script>

<select bind:value={Thing}>
	<option value={A}>A</option>
	<option value={B}>B</option>
</select>

<!-- 这些是等效的 -->
<Thing />
<svelte:component this={Thing} />

触控和滚轮事件是 passive

当使用 onwheelonmousewheelontouchstartontouchmove 事件属性时,处理程序是 passive,以符合浏览器默认行为。这极大地提高了响应能力,因为浏览器可以立即滚动文档,而不是等待查看事件处理程序是否调用 event.preventDefault()

在极少数需要阻止这些事件默认行为的情况下,你应该使用 on来代替(例如在 action 内部)。

Attribute / prop 语法更严格

在 Svelte 4 中,复杂的属性值不需要加引号:

<Component prop=this{is}valid />

这是一个潜在问题。在符文模式下,如果您希望连接内容,必须将值放在引号中:

<Component prop="this{is}valid" />

注意,如果你在 Svelte 5 中使用引号包裹单个表达式(例如 answer="{42}"),也会收到警告 —— 在 Svelte 6 中,这将导致值被转换为字符串,而不是作为数字传递。

HTML 结构更严格

在Svelte 4中,你可以编写一些在服务器端渲染时会被浏览器修复的HTML代码。例如你可以这样写...

<table>
	<tr>
		<td>你好</td>
	</tr>
</table>

...浏览器将自动插入 <tbody> 元素:

<table>
	<tbody>
		<tr>
			<td>你好</td>
		</tr>
	</tbody>
</table>

Svelte 5 对 HTML 结构的要求更加严格,在浏览器会修复 DOM 的情况下会抛出编译错误。

其他重大变化

更严格的 @const 赋值验证

不再允许对const声明的解构部分进行赋值。这种操作本就不应该被允许。

:is(...) 和 :where(...) 是作用域的

以前,Svelte 不分析 :is(...):where(...) 内部的选择器,实际上会将它们视为全局选择器。Svelte 5 会在当前组件的上下文中分析它们。因此,如果某些选择器依赖于这种处理方式,现在可能会被视为未使用。要修复这个问题,请在 :is(...)/:where(...) 选择器内使用 :global(...)

在使用 Tailwind 的 @apply 指令时,添加 :global 选择器以保留使用 Tailwind 生成的 :is(...) 选择器的规则:

main +++:global+++ {
	@apply bg-blue-100 dark:bg-blue-900;
}

CSS 哈希位置不再具有确定性

以前,Svelte 总是会在最后插入 CSS 哈希值。在 Svelte 5 中这一点不再有保证。这只有在 有非常奇怪的 css 选择器 时才会导致问题。

作用域 CSS 使用 :where(...)

为了避免由不可预测的特异性变化引起的问题,作用域 CSS 选择器现在使用 .svelte-xyz123(其中 xyz123 如前所述,是 <style> 内容的哈希)旁边使用 :where(.svelte-xyz123) 选择器修饰符。您可以在 这里 阅读更多细节。

如果您需要支持不实现 :where 的古老浏览器,您可以手动修改生成的 CSS,但代价是会产生不可预测的特异性变化:

// @errors: 2552
css = css.replace(/:where\((.+?)\)/, '$1');

错误/警告代码已重命名

错误和警告代码已重命名。以前它们使用破折号分隔单词,现在使用下划线(例如,foo-bar 变为 foo_bar)。此外,一些代码的措辞也略有改动。

命名空间数量减少

您可以传递给编译器选项 namespace 的有效命名空间数量减少到 html(默认)、mathmlsvg

foreign 命名空间仅对 Svelte Native 有用,我们计划在 5.x 次要版本中以不同方式支持它。

beforeUpdate/afterUpdate 变更

如果 beforeUpdate 修改了模板中引用的变量,则在初始渲染时不再运行两次。

父组件中的 afterUpdate 回调现在将在任何子组件的 afterUpdate 回调之后运行。

当组件包含 <slot> 且其内容更新时,beforeUpdate/afterUpdate 不再运行。

这两个函数在符文模式下被禁止 —— 请改为使用 $effect.pre(...)$effect(...)

contenteditable 行为变化

如果您有一个 contenteditable 节点,并且有一个对应的绑定 一个响应式值(例如:<div contenteditable=true bind:textContent>count is {count}</div>),那么contenteditable 内的值不会因 count 的更新而更新,因为绑定会立即完全控制内容,且内容应该只通过绑定来更新。

oneventname 属性不再接受字符串值

在Svelte 4中,可以将 HTML 元素的事件属性指定为字符串:

<button onclick="alert('你好')">...</button>

不推荐这种做法,在 Svelte 5 中已不再可用,其中 onclick 属性替代 on:click 成为添加事件处理程序的机制。

nullundefined 变为空字符串

在 Svelte 4 中,nullundefined 会被打印为对应的字符串。在 100 个案例中,99 次您希望将其变为空字符串,而这也是其他大多数框架所做的。因此,在 Svelte 5 中,nullundefined 变为空字符串。

bind:files 值只能是 nullundefinedFileList

bind:files 现在是一个双向绑定。因此,在设置值时,它需要是 假值( nullundefined)或 FileList 类型。

绑定现在会响应表单重置

之前,绑定不会考虑表单的 reset 事件,因此值可能会与 DOM 不同步。Svelte 5 通过在文档上放置 reset 监听器并在必要时调用绑定来修复这个问题。

walk 不再导出

svelte/compiler 为了方便从 estree-walker 重新导出了 walk。在 Svelte 5 中,这种情况不再存在,如果需要请直接从该包中导入。

svelte:options 里的内容被禁止

在 Svelte 4 中,您可以在 <svelte:options /> 标签内写入内容。它会被忽略,但您可以在里面写一些东西。在 Svelte 5 中,该标签里的内容会导致编译错误。

声明式 shadow roots 中的 <slot> 元素会被保留

Svelte 4 在所有地方都用自己版本的插槽替换了 <slot /> 标签。Svelte 5 在这些标签作为 <template shadowrootmode="..."> 元素的子元素时会保留它们。

<svelte:element> 标签必须是表达式

在 Svelte 4 中,<svelte:element this="div"> 是有效的代码。这没有什么意义——您应该直接使用 <div>。在极少数确实需要使用字面值的情况下,你可以这样做:

<svelte:element this=+++{+++"div"+++}+++>

请注意,虽然 Svelte 4 会将 <svelte:element this="input">(举例)与 <input> 视为相同,以确定可以应用哪些 bind: 指令。但 Svelte 5 不会这样做。

mount 默认播放过渡效果

用于渲染组件树的 mount 函数默认播放过渡效果,除非将 intro 选项设置为 false。这与传统的类组件不同,后者在手动实例化时默认不播放过渡效果。

<img src={...}>{@html ...} 水合不匹配不会被修复

在 Svelte 4 中,如果 src 属性或 {@html ...} 标签的值在服务端和客户端不同(即水合不匹配),这种不匹配会被修复。这个过程代价很高:设置 src 属性(即使它计算出相同的结果)会导致图像和 iframe 被重新加载,并且重新插入大量 HTML 是缓慢的。

由于这些不匹配极为罕见,Svelte 5 假定这些值保持不变,但在开发环境中如果它们不匹配会向你发出警告。要强制更新,你可以这样做:

<script>
	let { markup, src } = $props();

	if (typeof window !== 'undefined') {
		// 储存值...
		const initial = { markup, src };

		// 取消设置它们...
		markup = src = undefined;

		$effect(() => {
			// ...在我们挂载后重置
			markup = initial.markup;
			src = initial.src;
		});
	}
</script>

{@html markup}
<img {src} />

水合行为不同

Svelte 5 在服务端渲染期间使用注释,这些注释用于在客户端进行更健壮和高效的水合。因此,如果您打算对其进行水合,您不应该删除HTML输出中的注释,如果您手动编写了要由 Svelte 组件水合的 HTML,则需要在正确的位置添加这些注释。

onevent 属性被委托

事件属性替代事件指令:使用 onclick={handler} 而不是 on:click={handler}。为了向后兼容,on:event 语法仍然受到支持,并且行为与 Svelte 4 中相同。然而,某些 onevent 属性是被委托的,这意味着您需要注意不要手动停止这些事件的传播,因为它们可能永远不会传递到根节点的该事件类型的监听器。

--style-props 使用了不同的元素

在使用 CSS 自定义属性时,Svelte 5 使用额外的<svelte-css-wrapper> 元素而不是 <div>来包装组件。

Svelte 中文文档

点击查看中文文档 - Svelte 5迁移指南

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant