Svelte 5 迁移指南
版本 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
仍然是数字本身,您可以直接读取和写入它,而无需像 .value
或 getCount()
这样的包装器。
我们这样做的原因
顶层
let
隐式地具有响应式效果很好,但这意味着响应式受到限制 - 任何其他地方的let
声明都没有响应式。这迫使您在将代码从组件的顶层重构出来以供重用时诉诸于使用存储。这意味着您必须学习一个完全独立的响应式模型,并且结果通常不如使用起来那么好。因为 Svelte 5 中的响应式更明确,所以您可以在组件顶层之外继续使用相同的 API。前往 教程 了解更多信息。
$: -> $derived/$effect
在 Svelte 4 中,组件顶层的 $:
语句可用于声明派生,即完全通过计算其他状态定义的状态。在 Svelte 5 中,这是使用 $derived
符文实现的
<script>
let count = $state(0);
$: const double = $derived(count * 2);
</script>
与 $state
一样,其他内容没有变化。double
仍然是数字本身,您可以直接读取它,而无需像 .value
或 getDouble()
这样的包装器。
$:
语句也可用于创建副作用。在 Svelte 5 中,这是使用 $effect
符文实现的
<script>
let count = $state(0);
$:$effect(() => {
if (count > 5) {
alert('Count is too high!');
}
});
</script>
我们这样做的原因
$:
是一种很棒的简写方法,易于上手:您可以在大多数代码前面加上$:
,它就会以某种方式工作。这种直观性也是它的缺点,因为您的代码变得越复杂,就越难以理解。代码的意图是创建派生还是副作用?使用$derived
和$effect
,您需要进行一些预先的决策(剧透警告:90% 的时间您想要$derived
),但未来的您和团队中的其他开发人员会更容易理解。也有一些难以发现的陷阱
$:
仅在渲染之前更新,这意味着您可以在重新渲染之间读取陈旧的值$:
每个周期只运行一次,这意味着语句可能运行的频率低于您想象的$:
依赖项是通过对依赖项的静态分析确定的。这在大多数情况下都能正常工作,但在重构期间可能会以细微的方式中断,例如,依赖项将被移动到函数中,并且因此不再可见$:
语句也通过对依赖项的静态分析来排序。在某些情况下,可能会出现平局,导致排序错误,需要手动干预。在重构代码时,排序也可能会中断,并且某些依赖项不再可见。最后,它对 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} {...$$restPropsrest}>click me</button>
我们这样做的原因
export let
是更有争议的 API 决策之一,关于您是否应该考虑属性被export
还是import
进行了很多讨论。$props
没有这个特性。它也与其他符文一致,并且总体思路简化为“Svelte 中所有与响应式相关的特殊内容都是符文”。
export let
也存在很多限制,这需要额外的 API,如上所示。$props
将其统一在一个语法概念中,该概念严重依赖于常规 JavaScript 解构语法。
事件更改
Svelte 5 中的事件处理程序进行了改进。在 Svelte 4 中,我们使用 on:
指令将事件侦听器附加到元素,而在 Svelte 5 中,它们就像其他属性一样(换句话说,删除冒号)
<script>
let count = $state(0);
</script>
<button on:click={() => count++}>
clicks: {count}
</button>
由于它们只是属性,因此您可以使用正常的简写语法...
<script>
let count = $state(0);
function onclick() {
count++;
}
</script>
<button {onclick}>
clicks: {count}
</button>
...尽管在使用命名事件处理程序函数时,通常最好使用更具描述性的名称。
组件事件
在 Svelte 4 中,组件可以通过使用 createEventDispatcher
创建调度程序来发出事件。
此函数在 Svelte 5 中已弃用。相反,组件应该接受 *回调属性* - 这意味着您随后将函数作为属性传递给这些组件
<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}>new balloon</button>
<span class="boom">💥</span>
{:else}
<span class="balloon" style="scale: {0.01 * size}">
🎈
</span>
{/if}
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let { inflate, deflate } = $props();
let power = $state(5);
</script>
<button onclick={() => dispatch('inflate', power)inflate(power)}>
inflate
</button>
<button onclick={() => dispatch('deflate', power)deflate(power)}>
deflate
</button>
<button onclick={() => power--}>-</button>
Pump power: {power}
<button onclick={() => power++}>+</button>
冒泡事件
与其执行 <button on:click>
以将事件从元素“转发”到组件,不如让组件接受 onclick
回调属性
<script>
let { onclick } = $props();
</script>
<button on:click {onclick}>
click me
</button>
请注意,这也意味着您可以将事件处理程序与其他属性一起“扩展”到元素上,而不是费力地分别转发每个事件
<script>
let props = $props();
</script>
<button {...$$props} on:click on:keydown on:all_the_other_stuff {...props}>
click me
</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>
有三个修饰符——capture
、passive
和 nonpassive
——无法表示为包装器函数,因为它们需要在绑定事件处理程序时应用,而不是在事件处理程序运行时应用。
对于capture
,我们将修饰符添加到事件名称中。
<button onclickcapture={...}>...</button>
同时,更改事件处理程序的passive
选项并非易事。如果您有使用场景——而且您可能没有!——那么您需要使用操作自己应用事件处理程序。
多个事件处理程序
在 Svelte 4 中,这是可能的。
<button on:click={one} on:click={two}>...</button>
不允许在元素上重复属性(现在包括事件处理程序)。请改用以下方法:
<button
onclick={(e) => {
one(e);
two(e);
}}
>
...
</button>
在传播 props 时,本地事件处理程序必须放在传播之后,否则它们有被覆盖的风险。
<button
{...props}
onclick={(e) => {
doStuff(e);
props.onclick?.(e);
}}
>
...
</button>
我们这样做的原因
createEventDispatcher
总是有点模板化。
- 导入函数。
- 调用函数以获取分发函数。
- 使用字符串和可能的有效负载调用上述分发函数。
- 通过
.detail
属性在另一端检索上述有效负载,因为事件本身始终是CustomEvent
。始终可以使用组件回调 props,但由于您必须使用
on:
监听 DOM 事件,因此由于语法一致性,使用createEventDispatcher
处理组件事件是有意义的。现在我们有了事件属性(onclick
),情况正好相反:回调 props 现在是更明智的做法。事件修饰符的移除可以说是那些喜欢事件修饰符简写语法的用户认为是倒退的一项更改。鉴于它们的使用频率不高,我们用更小的表面积换取了更高的明确性。修饰符也不一致,因为它们中的大多数只能用于 DOM 元素。
同一事件的多个侦听器也不再可能,但无论如何这是一种反模式,因为它会影响可读性:如果有很多属性,则很难发现有两个处理程序,除非它们彼此相邻。它还暗示这两个处理程序是独立的,而实际上
one
中的event.stopImmediatePropagation()
会阻止调用two
。通过弃用
createEventDispatcher
和on:
指令,转而使用回调 props 和普通元素属性,我们
- 降低了 Svelte 的学习曲线。
- 消除了样板代码,尤其是在
createEventDispatcher
周围。- 消除了为可能甚至没有侦听器的事件创建
CustomEvent
对象的开销。- 添加了传播事件处理程序的功能。
- 添加了了解哪些事件处理程序提供给组件的功能。
- 添加了表达给定事件处理程序是必需还是可选的功能。
- 提高了类型安全性(以前,Svelte 实际上不可能保证组件不会发出特定事件)。
代码片段代替插槽
在 Svelte 4 中,可以使用插槽将内容传递给组件。Svelte 5 使用更强大、更灵活的代码片段替换它们,因此插槽在 Svelte 5 中已弃用。
但是,它们继续工作,您可以在组件中混合和匹配代码片段和插槽。
使用自定义元素时,您仍然应该像以前一样使用<slot />
。在未来的版本中,当 Svelte 删除其内部版本的插槽时,它会将这些插槽保持原样,即输出常规 DOM 标签而不是转换它。
默认内容
在 Svelte 4 中,将 UI 片段传递给子组件的最简单方法是使用<slot />
。在 Svelte 5 中,这是使用children
prop 完成的,然后使用{@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 中,代码片段承担了这项责任。
<script>
import List from './List.svelte';
</script>
<List items={['one', 'two', 'three']} let:item>
{#snippet item(text)}
<span>{text}</span>
{/snippet}
<span slot="empty">No items yet</span>
{#snippet empty()}
<span>No items yet</span>
{/snippet}
</List>
<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}
我们这样做的原因
插槽很容易上手,但用例越高级,语法就越复杂和令人困惑。
let:
语法让许多人感到困惑,因为它创建了一个变量,而所有其他:
指令都接收一个变量。- 使用
let:
声明的变量的作用域并不明确。在上面的示例中,它可能看起来像您可以在empty
插槽中使用item
插槽 prop,但事实并非如此。- 命名插槽必须使用
slot
属性应用于元素。有时您不想创建元素,因此我们必须添加<svelte:fragment>
API。- 命名插槽也可以应用于组件,这改变了
let:
指令可用位置的语义(即使在今天,我们维护人员也经常不知道它到底是如何工作的)。代码片段通过更易读和更清晰的方式解决了所有这些问题。同时,它们功能更强大,因为它们允许您定义可以在任何地方渲染的 UI 部分,而不仅仅是将它们作为 props 传递给组件。
迁移脚本
到目前为止,您应该已经对前后关系以及旧语法与新语法之间的关系有了相当好的了解。它可能也变得很清楚,很多这些迁移在技术上相当复杂且重复——这是您不想手动完成的事情。
我们也有同样的想法,这就是为什么我们提供一个迁移脚本来自动执行大部分迁移。您可以通过使用npx sv migrate svelte-5
升级您的项目。这将执行以下操作
- 在您的
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, ...)
)。
您还可以通过 VS Code 中的“将组件迁移到 Svelte 5 语法”命令或在我们的 Playground 中通过“迁移”按钮迁移单个组件。
并非所有内容都可以自动迁移,并且某些迁移之后需要手动清理。以下部分将更详细地描述这些内容。
run
您可能会看到迁移脚本将某些$:
语句转换为从svelte/legacy
导入的run
函数。如果迁移脚本无法可靠地将语句迁移到$derived
并得出结论,这是一个副作用,则会发生这种情况。在某些情况下,这可能是错误的,最好将其更改为使用$derived
。在其他情况下,这可能是正确的,但由于$:
语句也在服务器上运行,而$effect
没有,因此将其转换为此类是不安全的。相反,run
用作权宜之计。run
模仿了$:
的大多数特性,因为它在服务器上运行一次,并在客户端作为$effect.pre
运行($effect.pre
在将更改应用于 DOM之前运行;大多数情况下,您希望使用$effect
)。
<script>
import { run } from 'svelte/legacy';
run(() => {
$effect(() => {
// some side effect code
})
</script>
事件修饰符
事件修饰符不适用于事件属性(例如,您不能执行onclick|preventDefault={...}
)。因此,在将事件指令迁移到事件属性时,我们需要一个函数替换这些修饰符。这些是从svelte/legacy
导入的,应该迁移到例如只使用event.preventDefault()
。
<script>
import { preventDefault } from 'svelte/legacy';
</script>
<button
onclick={preventDefault((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 { function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount } from 'svelte';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte'
const app = new App({ target: document.getElementById("app") });
const const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app = mount<Record<string, any>, {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
...;
} & Record<...>
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount(const App: LegacyComponentType
App, { target: Document | Element | ShadowRoot
Target element where the component will be mounted.
target: var document: Document
document.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
getElementById("app") });
export default const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app;
mount
和 hydrate
具有完全相同的 API。区别在于 hydrate
会在其目标元素内获取 Svelte 的服务器端渲染的 HTML 并将其水化。两者都返回一个包含组件导出内容的对象,以及潜在的属性访问器(如果使用 accessors: true
编译)。它们不包含您可能从类组件 API 中知道的 $on
、$set
和 $destroy
方法。以下是它们的替代方法。
对于 $on
,无需监听事件,而是通过选项参数上的 events
属性传递它们。
import { function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount } from 'svelte';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte'
const app = new App({ target: document.getElementById("app") });
app.$on('event', callback);
const const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app = mount<Record<string, any>, {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
...;
} & Record<...>
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount(const App: LegacyComponentType
App, { target: Document | Element | ShadowRoot
Target element where the component will be mounted.
target: var document: Document
document.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
getElementById("app"), events?: Record<string, (e: any) => any> | undefined
Allows the specification of events.
events: { event: any
event: callback } });
请注意,不建议使用
events
—— 而应该 使用回调函数
对于 $set
,请改用 $state
创建一个响应式属性对象并对其进行操作。如果在 .js
或 .ts
文件中执行此操作,请调整文件扩展名以包含 .svelte
,例如 .svelte.js
或 .svelte.ts
。
import { function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount } from 'svelte';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte'
const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$set({ foo: 'baz' });
const const props: {
foo: string;
}
props = function $state<{
foo: string;
}>(initial: {
foo: string;
}): {
foo: string;
} (+1 overload)
namespace $state
$state({ foo: string
foo: 'bar' });
const const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app = mount<Record<string, any>, {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
...;
} & Record<...>
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount(const App: LegacyComponentType
App, { target: Document | Element | ShadowRoot
Target element where the component will be mounted.
target: var document: Document
document.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
getElementById("app"), props?: Record<string, any> | undefined
Component properties.
props });
const props: {
foo: string;
}
props.foo: string
foo = 'baz';
对于 $destroy
,请改用 unmount
。
import { function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount, function unmount(component: Record<string, any>): void
Unmounts a component that was previously mounted using mount
or hydrate
.
unmount } from 'svelte';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte'
const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
app.$destroy();
const const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app = mount<Record<string, any>, {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
...;
} & Record<...>
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true
) of the component.
Transitions will play during the initial render unless the intro
option is set to false
.
mount(const App: LegacyComponentType
App, { target: Document | Element | ShadowRoot
Target element where the component will be mounted.
target: var document: Document
document.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
getElementById("app") });
function unmount(component: Record<string, any>): void
Unmounts a component that was previously mounted using mount
or hydrate
.
unmount(const app: {
$on?(type: string, callback: (e: any) => void): () => void;
$set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
app);
作为一种权宜之计,您还可以使用 createClassComponent
或 asClassComponent
(从 svelte/legacy
导入)来保留实例化后 Svelte 4 中已知的相同 API。
import { function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: ComponentConstructorOptions<Props> & {
component: ComponentType<SvelteComponent<Props, Events, Slots>> | Component<Props>;
}): SvelteComponent<Props, Events, Slots> & Exports
Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
createClassComponent } from 'svelte/legacy';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte'
const app = new App({ target: document.getElementById("app") });
const const app: SvelteComponent<Record<string, any>, any, any> & Record<string, any>
app = createClassComponent<Record<string, any>, Record<string, any>, any, any>(options: ComponentConstructorOptions<Record<string, any>> & {
component: Component<...> | ComponentType<...>;
}): SvelteComponent<...> & Record<...>
Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
createClassComponent({ component: Component<Record<string, any>, {}, string> | ComponentType<SvelteComponent<Record<string, any>, any, any>>
component: const App: LegacyComponentType
App, ComponentConstructorOptions<Props extends Record<string, any> = Record<string, any>>.target: Document | Element | ShadowRoot
target: var document: Document
document.Document.getElementById(elementId: string): HTMLElement | null
Returns a reference to the first object with the specified value of the ID attribute.
getElementById("app") });
export default const app: SvelteComponent<Record<string, any>, any, any> & Record<string, any>
app;
如果此组件不受您的控制,您可以使用 compatibility.componentApi
编译器选项来自动应用向后兼容性,这意味着使用 new Component(...)
的代码无需调整即可继续工作(请注意,这会为每个组件增加一些开销)。这也会为通过 bind:this
获取的所有组件实例添加 $set
和 $on
方法。
/// svelte.config.js
export default {
compilerOptions: {
compatibility: {
componentApi: number;
};
}
compilerOptions: {
compatibility: {
componentApi: number;
}
compatibility: {
componentApi: number
componentApi: 4
}
}
};
请注意,mount
和 hydrate
不是同步的,因此诸如 onMount
之类的内容在函数返回时不会被调用,并且挂起的 Promise 块也不会被渲染(因为 #await
会等待一个微任务来等待一个可能立即解析的 Promise)。如果您需要此保证,请在调用 mount/hydrate
后调用 flushSync
(从 'svelte'
导入)。
服务器 API 更改
类似地,当为服务器端渲染编译时,组件不再具有 render
方法。而是将函数传递给 svelte/server
中的 render
。
import { function render<Comp extends SvelteComponent<any> | Component<any>, Props extends ComponentProps<Comp> = ComponentProps<Comp>>(...args: {} extends Props ? [component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp, options?: {
props?: Omit<Props, "$$slots" | "$$events">;
context?: Map<any, any>;
}] : [component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp, options: {
props: Omit<Props, "$$slots" | "$$events">;
context?: Map<any, any>;
}]): RenderOutput
Only available on the server and when compiling with the server
option.
Takes a component and returns an object with body
and head
properties on it, which you can use to populate the HTML when server-rendering your app.
render } from 'svelte/server';
import type App = SvelteComponent<Record<string, any>, any, any>
const App: LegacyComponentType
App from './App.svelte';
const { html, head } = App.render({ props: { message: 'hello' }});
const { const html: string
html, const head: string
HTML that goes into the <head>
head } = render<SvelteComponent<Record<string, any>, any, any>, Record<string, any>>(component: ComponentType<SvelteComponent<Record<string, any>, any, any>>, options?: {
...;
} | undefined): RenderOutput
Only available on the server and when compiling with the server
option.
Takes a component and returns an object with body
and head
properties on it, which you can use to populate the HTML when server-rendering your app.
render(const App: LegacyComponentType
App, { props?: Omit<Record<string, any>, "$$slots" | "$$events"> | undefined
props: { message: string
message: 'hello' }});
在 Svelte 4 中,将组件渲染为字符串也会返回所有组件的 CSS。在 Svelte 5 中,默认情况下不再如此,因为大多数情况下您都在使用一个工具链,该工具链以其他方式(如 SvelteKit)处理它。如果您需要从 render
返回 CSS,您可以将 css
编译器选项设置为 'injected'
,它会将 <style>
元素添加到 head
中。
组件类型更改
从类转向函数的更改也反映在类型定义中:Svelte 4 中的基类 SvelteComponent
已被弃用,取而代之的是新的 Component
类型,该类型定义了 Svelte 组件的函数形状。在 d.ts
文件中手动定义组件形状
import type { interface Component<Props extends Record<string, any> = {}, Exports extends Record<string, any> = {}, Bindings extends keyof Props | "" = string>
Can be used to create strongly typed Svelte components.
Example:
You have component library on npm called component-library
, from which
you export a component called MyComponent
. For Svelte+TypeScript users,
you want to provide typings. Therefore you create a index.d.ts
:
import type { Component } from 'svelte';
export declare const MyComponent: Component<{ foo: string }> {}
Typing this makes it possible for IDEs like VS Code with the Svelte extension
to provide intellisense and to use the component like this in a Svelte file
with TypeScript:
<script lang="ts">
import { MyComponent } from "component-library";
</script>
<MyComponent foo={'bar'} />
Component } from 'svelte';
export declare const const MyComponent: Component<{
foo: string;
}, {}, string>
MyComponent: interface Component<Props extends Record<string, any> = {}, Exports extends Record<string, any> = {}, Bindings extends keyof Props | "" = string>
Can be used to create strongly typed Svelte components.
Example:
You have component library on npm called component-library
, from which
you export a component called MyComponent
. For Svelte+TypeScript users,
you want to provide typings. Therefore you create a index.d.ts
:
import type { Component } from 'svelte';
export declare const MyComponent: Component<{ foo: string }> {}
Typing this makes it possible for IDEs like VS Code with the Svelte extension
to provide intellisense and to use the component like this in a Svelte file
with TypeScript:
<script lang="ts">
import { MyComponent } from "component-library";
</script>
<MyComponent foo={'bar'} />
Component<{
foo: string
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" />
这两个实用程序类型 ComponentEvents
和 ComponentType
也已弃用。ComponentEvents
已过时,因为事件现在定义为回调属性,而 ComponentType
已过时,因为新的 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
标签内保留空白。
与以前一样,您可以通过在编译器设置中或在 <svelte:options>
中的每个组件的基础上设置 preserveWhitespace
选项来禁用空白修剪。
需要现代浏览器
Svelte 5 由于各种原因需要现代浏览器(换句话说,不是 Internet Explorer)。
- 它使用
Proxies
- 带有
clientWidth
/clientHeight
/offsetWidth
/offsetHeight
绑定的元素使用ResizeObserver
而不是复杂的<iframe>
技巧。 <input type="range" bind:value={...} />
只使用input
事件监听器,而不是也像以前那样监听change
事件作为后备。
legacy
编译器选项(生成更庞大但对 IE 友好的代码)已不再存在。
编译器选项的更改
false
/true
(之前已弃用)和"none"
值已从css
选项的有效值中删除。legacy
选项已被重新利用。hydratable
选项已被删除。Svelte 组件现在始终可水化。enableSourcemap
选项已被删除。源映射现在始终生成,工具可以选择忽略它。tag
选项已被删除。请改用组件内部的<svelte:options customElement="tag-name" />
。loopGuardTimeout
、format
、sveltePath
、errorMode
和varsReport
选项已被删除。
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
访问。此更改使事情更容易理解,因为它强制在 props 和导出内容之间进行清晰的区分。
需要使用 $bindable() 显式定义绑定
在 Svelte 4 语法中,每个属性(通过 export let
声明)都是可绑定的,这意味着您可以 bind:
到它。在符文模式下,属性默认不可绑定:您需要使用 $bindable
符文来表示可绑定属性。
如果可绑定属性具有默认值(例如 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
字段。用 $state(...)
包装 new Foo()
不会有任何效果——只有普通对象和数组会被深度设置为响应式。
<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>
<!-- these are equivalent -->
<Thing />
<svelte:component this={Thing} />
触摸和滚轮事件处于被动状态
当使用 onwheel
、onmousewheel
、ontouchstart
和 ontouchmove
事件属性时,处理程序处于 被动 状态,以与浏览器默认值保持一致。这通过允许浏览器立即滚动文档(而不是等待查看事件处理程序是否调用 event.preventDefault()
)极大地提高了响应速度。
在您需要阻止这些事件默认值(非常罕见)的情况下,您应该改用 on
(例如在操作内部)。
属性/prop 语法更严格
在 Svelte 4 中,复杂的属性值无需用引号括起来。
<Component prop=this{is}valid />
这是一个“陷阱”。在符文模式下,如果要连接内容,则必须将值用引号括起来。
<Component prop="this{is}valid" />
请注意,如果将单个表达式用引号括起来,例如 answer="{42}"
,Svelte 5 也会发出警告——在 Svelte 6 中,这将导致该值转换为字符串,而不是作为数字传递。
HTML 结构更严格
在 Svelte 4 中,您可以编写在服务器端渲染时浏览器会修复的 HTML 代码。例如,您可以编写以下代码...
<table>
<tr>
<td>hi</td>
</tr>
</table>
...浏览器会自动插入 <tbody>
元素。
<table>
<tbody>
<tr>
<td>hi</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 选择器现在使用:where(.svelte-xyz123)
选择器修饰符以及.svelte-xyz123
(其中xyz123
与之前一样,是<style>
内容的哈希值)。您可以阅读更多详细信息此处。
如果您需要支持不实现:where
的旧版浏览器,则可以手动更改发出的 CSS,但代价是不可预测的特异性更改。
css = css.replace(/:where\((.+?)\)/, '$1');
错误/警告代码已重命名
错误和警告代码已重命名。之前它们使用连字符分隔单词,现在使用下划线(例如,foo-bar 变为 foo_bar)。此外,一些代码的措辞略有改动。
减少命名空间数量
您可以传递给编译器选项namespace
的有效命名空间数量已减少到html
(默认值)、mathml
和svg
。
foreign
命名空间仅对 Svelte Native 有用,我们计划在 5.x 的次要版本中以不同的方式支持它。
beforeUpdate/afterUpdate 更改
如果beforeUpdate
修改了模板中引用的变量,则它在初始渲染时不再运行两次。
父组件中的afterUpdate
回调现在将在任何子组件中的afterUpdate
回调之后运行。
这两个函数在 runes 模式下不允许使用——请改用$effect.pre(...)
和$effect(...)
。
contenteditable 行为更改
如果您有一个contenteditable
节点,它具有相应的绑定以及其中的响应式值(例如:<div contenteditable=true bind:textContent>count is {count}</div>
),则contenteditable
内部的值不会因count
的更新而更新,因为绑定会立即完全控制内容,并且应该只通过它来更新。
oneventname 属性不再接受字符串值
在 Svelte 4 中,可以在 HTML 元素上将事件属性指定为字符串。
<button onclick="alert('hello')">...</button>
这并不推荐,并且在 Svelte 5 中不再可能,在 Svelte 5 中,诸如onclick
之类的属性取代on:click
作为添加事件处理程序的机制。
null 和 undefined 变成空字符串
在 Svelte 4 中,null
和undefined
被打印为相应的字符串。在 100 个案例中,您希望这变成空字符串,这也是大多数其他框架所做的。因此,在 Svelte 5 中,null
和undefined
变成空字符串。
bind:files 值只能为 null、undefined 或 FileList
bind:files
现在是双向绑定。因此,在设置值时,它需要是假值(null
或undefined
)或类型为FileList
。
绑定现在对表单重置做出反应
之前,绑定没有考虑表单的reset
事件,因此值可能会与 DOM 不同步。Svelte 5 通过在文档上放置一个reset
监听器并在必要时调用绑定来修复此问题。
walk 不再导出
svelte/compiler
为了方便重新导出了estree-walker
中的walk
。在 Svelte 5 中,这不再成立,如果您需要它,请直接从该包中导入它。
svelte:options 内部的内容被禁止
在 Svelte 4 中,您可以在<svelte:options />
标签内包含内容。它被忽略了,但您可以在其中写入一些内容。在 Svelte 5 中,该标签内的内容是编译器错误。
<slot> 元素在声明式 Shadow DOM 中被保留
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') {
// stash the values...
const initial = { markup, src };
// unset them...
markup = src = undefined;
$effect(() => {
// ...and reset after we've mounted
markup = initial.markup;
src = initial.src;
});
}
</script>
{@html markup}
<img {src} />
水合工作方式不同
Svelte 5 在服务器端渲染期间使用注释,这些注释用于在客户端上进行更强大、更高效的水合。因此,如果您打算对其进行水合,则不应从 HTML 输出中删除注释,并且如果您手动编写 HTML 以供 Svelte 组件水合,则需要调整该 HTML 以在正确的位置包含这些注释。
onevent 属性被委托
事件属性替换事件指令:您不再使用on:click={handler}
,而是使用onclick={handler}
。为了向后兼容,on:event
语法仍然受支持,并且行为与 Svelte 4 中相同。但是,一些onevent
属性被委托,这意味着您需要注意不要手动停止事件传播,因为它们可能永远不会到达根目录中此事件类型的监听器。
--style-props 使用不同的元素
Svelte 5 使用额外的<svelte-css-wrapper>
元素而不是<div>
来包装使用 CSS 自定义属性时的组件。