版本 5 带来了全新的语法和反应性系统。虽然一开始可能看起来不同,但很快你就会发现许多相似之处。本指南详细介绍了这些变化,并展示了如何升级。此外,我们还提供了关于为什么进行这些更改的*原因*信息。 您不必立即迁移到新语法 - Svelte 5 仍然支持旧版 Svelte 4 语法,并且您可以使用新语法与旧语法组件混合使用,反之亦然。我们预计许多人只需更改几行代码即可升级。此外,还有一个 [迁移脚本](#Migration-script),可以帮助您自动完成许多这些步骤。 ## 反应性语法更改 Svelte 5 的核心是新的 runes API。Runes 主要是编译器指令,用于告知 Svelte 关于响应性。在语法上,runes 是以美元符号开头的函数。 ### let → $状态 在 Svelte 4 中,组件顶层的一个 `let` 声明是隐式响应式的。在 Svelte 5 中,事情变得更加明确:当使用 `$state` 符文创建变量时,该变量是响应式的。让我们通过将计数器包裹在 `$state` 中将计数器迁移到符文模式: ```svelte ``` 无其他更改。`count`仍然是数字本身,您可以直接读取和写入它,无需像`.value`或`getCount()`这样的包装器。 > \[!详细信息\] 为什么我们在顶层将`let`设置为隐式响应式效果很好,但这意味着响应式被限制 - 任何其他地方的`let`声明都不是响应式的。这迫使你在将代码从组件顶层重构以供重用时求助于使用存储。这意味着你必须学习一个完全不同的响应式模型,而且结果往往不那么容易处理。由于 Svelte 5 中的响应式更明确,你可以在组件顶层之外继续使用相同的 API。前往[教程](/tutorial)了解更多信息。 ### $: → $derived/$effect 在 Svelte 4 中,组件顶层的一个`$:`语句可以用来声明一个派生,即完全通过其他状态的计算定义的状态。在 Svelte 5 中,这通过使用`$derived`符号实现: ```svelte ``` 与`$state`一样,其他什么都没变。`double`仍然是数字本身,你可以直接读取,无需像`.value`或`getDouble()`这样的包装。 A `$:` 语句也可以用来创建副作用。在 Svelte 5 中,这通过使用 `$effect` 符文来实现: ```svelte ``` 请注意,当 `$effect` 运行时与当 `$:` 运行时不同。 > \[!详细信息\] 我们为什么要这样做 `$:` 这是一个很好的简写,并且易于上手:你可以在大多数代码前加上 `$:`,它就会以某种方式工作。这种直观性也是它的缺点,因为随着代码变得越来越复杂,它就不那么容易推理了。代码的意图是创建一个导出,还是副作用?有了 `$derived` 和 `$effect`,你需要在开始时做出更多决策(剧透:90% 的时候你想要 `$derived`),但未来的你和其他团队成员将更容易处理。 > > 也有一些难以发现的陷阱: > > * `$:` 仅在渲染前直接更新,这意味着您可以在重新渲染之间读取过时的值 > * `$:` 每个 tick 只运行一次,这意味着语句可能运行的频率比你想象的要低 > * `$:` 依赖项通过依赖项的静态分析确定。这在大多数情况下都有效,但在重构过程中可能会以微妙的方式出现问题,例如依赖项被移动到函数中,因此不再可见。 > * `$:` 语句也通过依赖关系的静态分析进行排序。在某些情况下可能会出现平局,导致排序错误,需要手动干预。在重构代码时,排序也可能中断,一些依赖关系因此不再可见。 > > 最后,它对 TypeScript 不友好(我们的编辑工具必须跳过一些步骤才能使其对 TypeScript 有效),这成为了使 Svelte 的反应模型真正通用的障碍。 > > `$derived` 和 `$effect` 修复这些问题 > > * 总是返回最新值 > * 运行以保持稳定所需频率 > * 确定运行时的依赖,因此对重构免疫 > * 执行所需依赖项,因此不受顺序问题影响 > * TypeScript 友好 ### 导出 let → $props 在 Svelte 4 中,组件属性使用 `export let` 声明。每个属性都是一个单独的声明。在 Svelte 5 中,所有属性都通过 `$props` 运行符声明,通过解构: ```svelte ``` 存在多种情况,声明属性比几个`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();` ```svelte ``` > \[!详细信息\] 我们为什么要这样做 `export let` 是更具争议性的 API 决定之一,关于你是否应该考虑一个属性是 `export` 或 `import`,有很多争论。`$props` 没有这种特性。这也符合其他符文,一般的想法可以归结为“Svelte 中所有与响应性相关的东西都是一个符文”。 > > 也存在许多关于`export let`的限制,这需要额外的 API,如上所示。`$props`将这一概念统一为一个语法概念,该概念在很大程度上依赖于常规 JavaScript 解构语法。 ## 活动变更 事件处理器在 Svelte 5 中得到了改进。在 Svelte 4 中,我们使用`on:`指令将事件监听器附加到元素上,而在 Svelte 5 中,它们像任何其他属性一样(换句话说——去掉冒号): ```svelte ``` 由于它们只是属性,您可以使用正常的缩写语法... ```svelte ``` ...尽管在使用命名事件处理函数时,通常最好使用更具描述性的名称。 ### 组件事件 在 Svelte 4 中,组件可以通过创建一个派发器来发射事件,使用 `createEventDispatcher`。 此函数在 Svelte 5 中已弃用。取而代之,组件应接受*回调属性* - 这意味着您将这些函数作为属性传递给这些组件: ```svelte { size += power---.detail---; if (size > 75) burst = true; }} ---on:---deflate={(power) => { if (size > 0) size -= power---.detail---; }} /> {#if burst} 💥 {:else} 🎈 {/if} ``` ```svelte Pump power: {power} ``` ### 冒泡事件 而不是使用` ``` 请注意,这也意味着您可以将事件处理器与其他属性一起“分配”到元素上,而不是逐一 tediously forwarding each event separately: ```svelte ``` ### 事件修饰符 在 Svelte 4 中,您可以为处理程序添加事件修饰符: ```svelte ``` 修改器仅针对 `on:` 而言,因此不适用于现代事件处理器。在处理器内部添加如 `event.preventDefault()` 这样的代码更可取,因为所有逻辑都集中在一个地方,而不是分散在处理器和修改器之间。 由于事件处理器只是函数,您可以根据需要创建自己的包装器: ```svelte ``` 有三个修饰符 —— `捕获`、`被动` 和 `非被动` —— 不能表示为包装函数,因为它们需要在事件处理器绑定时应用,而不是在运行时应用。 為了對`capture`進行處理,我們將修飾符添加到事件名稱中: ```svelte ``` 修改事件处理器的 [`被动`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) 选项,同时,这不是一件可以轻率行事的事情。如果您有使用它的用例——您可能没有!——那么您将需要使用一个动作来自己应用事件处理器。 ### 多事件处理器 在 Svelte 4 中,这是可能的: ```svelte ``` 元素上不允许重复属性/属性,现在包括事件处理器——不允许这样做: ```svelte ``` 在传递属性时,本地事件处理器必须放在 *传递之后*,否则它们的风险是被覆盖: ```svelte ``` > \[!详情\] 我们为什么要这样做 `createEventDispatcher` 总是有点模板化: > > * 导入函数 > * 调用获取调度函数的函数 > * 调用带有字符串和可能的有效负载的调度函数 > * 检索另一端通过一个`.detail`属性获取该有效载荷,因为该事件始终是一个`CustomEvent`。 > > 它始终可以使用组件回调属性,但因为你必须使用`on:`监听 DOM 事件,所以在组件事件中出于语法一致性,使用`createEventDispatcher`是有意义的。现在我们有事件属性(`onclick`),情况就相反了:回调属性现在更合理。 > > 删除事件修饰符可能是那些喜欢事件修饰符简写语法的人认为的一个退步。鉴于它们并不常用,我们用更明确的表达方式换取了更小的作用域。修饰符也并不一致,因为大多数修饰符仅适用于 DOM 元素。 > > 多个监听器监听同一事件也不再可能,但这本身就是一个反模式,因为它会阻碍可读性:如果有许多属性,除非它们紧挨着,否则很难发现有两个处理程序。这也暗示了这两个处理程序是独立的,但实际上,在`one`中的 `event.stopImmediatePropagation()` 会阻止`two`被调用。 > > 通过弃用`createEventDispatcher`和`on:`指令,转而使用回调属性和常规元素属性,我们: > > * 降低 Svelte 的学习曲线 > * 删除样板文本,尤其是围绕 `createEventDispatcher` 的部分 > * 删除创建可能甚至没有监听器的 `CustomEvent` 对象的开销 > * 增加传播事件处理器的功能 > * 添加能够知道哪些事件处理器被提供给组件的功能 > * 增加表达给定事件处理器是必需还是可选的能力 > * 提高类型安全性(之前,Svelte 实际上无法保证组件不会发出特定事件) ## 片段而非槽位 在 Svelte 4 中,内容可以通过插槽传递给组件。Svelte 5 用片段取代了它们,这些片段更强大、更灵活,因此插槽在 Svelte 5 中已被弃用。 他们继续工作,然而,你可以将代码片段传递给使用插槽的组件: ```svelte
``` ```svelte default child content {#snippet foo({ message })} message from child: {message} {/snippet} ``` (反之不成立——您不能将槽位内容传递给使用 [`{@render ...}`](/docs/svelte/@render) 标签的组件。) 当使用自定义元素时,您仍然应该像以前一样使用 ``。在未来的版本中,当 Svelte 移除其内部版本的插槽时,它将保留这些插槽不变,即输出一个常规的 DOM 标签而不是将其转换。 ### 默认内容 在 Svelte 4 中,将 UI 传递给子组件最简单的方式是使用 ``。在 Svelte 5 中,这通过使用 `children` 属性来完成,然后通过 `{@render children()}` 显示: ```svelte ------ +++{@render children?.()}+++ ``` ### 多内容占位符 如果您需要多个 UI 占位符,您必须使用命名槽。在 Svelte 5 中,请使用 props,随意命名并`使用它们进行渲染...`: ```svelte
------ +++{@render header()}+++
------ +++{@render main()}+++
------ +++{@render footer()}+++
``` ### 回传数据 在 Svelte 4 中,您会将数据传递给 `slot`,然后在父组件中使用 `let:` 来检索它。在 Svelte 5 中,代码片段承担了这一责任: ```svelte +++{#snippet item(text)}+++ {text} +++{/snippet}+++ ---No items yet--- +++{#snippet empty()} No items yet {/snippet}+++ ``` ```svelte {#if items.length}
    {#each items as entry}
  • ------ +++{@render item(entry)}+++
  • {/each}
{:else} ------ +++{@render empty?.()}+++ {/if} ``` > \[!详情\] 我们为什么要这样做 槽位易于开始使用,但随着用例的日益复杂,语法变得越来越复杂和令人困惑: > > * the `let:` 语法让很多人感到困惑,因为它 *创建* 一个变量,而所有其他的 `:` 指令 *接收* 一个变量 > * 变量使用`let:`声明的作用域不明确。在上面的示例中,看起来您可以在`empty`槽中使用`item`槽属性,但这并不正确。 > * 命名槽必须使用 `slot` 属性应用于一个元素。有时你不想创建一个元素,因此我们必须添加 `` API。 > * 命名槽位也可以应用于组件,这改变了 \``let:`\` 指令可用的语义(即使今天我们维护者也常常不知道它的工作方式) > > 代码片段通过更易读和清晰的方式解决了所有这些问题。同时,它们更强大,因为它们允许你定义可以渲染到任何地方的 UI 部分,而不仅仅是将它们作为属性传递给组件。 ## 迁移脚本 截至目前,你应该已经对“之前/之后”以及旧语法与新语法的关联有了相当好的理解。可能也变得很明显,许多这些迁移相当技术性和重复——这是你不想手动去做的事情。 我们持相同看法,这就是为什么我们提供了一个迁移脚本来自动完成大部分迁移。您可以通过使用`npx sv migrate svelte-5`来升级您的项目。这将执行以下操作: * 提升您的 `package.json` 中的核心依赖项 * 迁移到符文(`let` → `$state` 等) * 迁移到 DOM 元素的属性事件(`on:click` → `onclick`) * 将槽位创建迁移到渲染标签(`` → `{@render children()}`) * 将插槽使用迁移到代码片段(`
...
` → `{#snippet x()}
...
{/snippet}` ) * 迁移明显的组件创建(`new Component(...)` → `mount(Component, ...)`) 您也可以通过 `Migrate Component to Svelte 5 Syntax` 命令在 VS Code 中迁移单个组件,或在我们的 Playground 中通过`迁移`按钮进行迁移。 并非所有内容都可以自动迁移,一些迁移需要在之后进行手动清理。以下部分将更详细地描述这些内容。 ### 运行 您可能会看到迁移脚本将一些您的`$:`语句转换为从`svelte/legacy`导入的`run`函数。这种情况发生在迁移脚本无法可靠地将语句迁移到`$derived`,并得出这是副作用结论时。在某些情况下,这可能是错误的,最好将其更改为使用`$derived`。在其他情况下,这可能是正确的,但由于`$:`语句在服务器上运行,而`$effect`则不运行,因此将其转换为这样的形式是不安全的。相反,使用`run`作为临时解决方案。`run`模仿了大多数`$:`的特性,即它在服务器上运行一次,并在客户端作为`$effect.pre`运行(`$effect.pre`在将更改应用到 DOM 之前运行;您可能更希望使用`$effect`)。 ```svelte ``` ### 事件修饰符 事件修饰符不适用于事件属性(例如,您不能这样做 `onclick|preventDefault={...}`)。因此,在将事件指令迁移到事件属性时,我们需要一个函数替换这些修饰符。这些是从 `svelte/legacy` 导入的,应该迁移开,例如,只需使用 `event.preventDefault()`。 ```svelte ``` ### 事物未自动迁移 迁移脚本不转换 `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` 文件(或类似文件),您需要调整它: ```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; ``` `挂载`和`激活`具有完全相同的 API。区别在于`激活`会在其目标中提取 Svelte 的服务端渲染 HTML 并激活它。它们都返回一个包含组件导出和可能属性访问器(如果编译时带有`accessors: true`)的对象。它们不包含您可能从类组件 API 中了解到的`$on`、`$set`和`$destroy`方法。这些是它们的替代品: 对于`$on`,而不是监听事件,通过选项参数上的`events`属性传递它们。 ```js +++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 } });+++ ``` > \[!注意\] 注意使用 `事件` 是不建议的——相反,[使用回调](#Event-changes) 对于`$set`,请使用`$state`来创建一个响应式属性对象并进行操作。如果您在`.js`或`.ts`文件中执行此操作,请调整扩展名以包含`.svelte`,即`.svelte.js`或`.svelte.ts`。 ```js +++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`代替。 ```js +++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);+++ ``` 作为临时解决方案,您也可以使用`createClassComponent`或`asClassComponent`(从`svelte/legacy`导入)来保持与 Svelte 4 相同的 API,在实例化后使用。 ```js +++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`方法。 ```js /// svelte.config.js export default { compilerOptions: { compatibility: { componentApi: 4 } } }; ``` 请注意,`mount` 和 `hydrate` 并非同步,因此像 `onMount` 这样的函数在函数返回时可能尚未被调用,挂起的 Promise 块也尚未渲染(因为 `#await` 等待一个可能立即解决的 Promise 的微任务)。如果您需要这个保证,请在调用 `mount/hydrate` 之后调用 `flushSync`(从 `'svelte'` 导入)。 ### 服务器 API 更改 类似地,当组件编译为服务器端渲染时,不再具有 `render` 方法。相反,从 `svelte/server` 将函数传递给 `render`: ```js +++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`添加`