Stories

Detail Return Return

Svelte 最新中文文檔教程(22)—— Svelte 5 遷移指南 - Stories Detail

前言

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

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

user avatar tianmiaogongzuoshi_5ca47d59bef41 Avatar cyzf Avatar Leesz Avatar zaotalk Avatar nihaojob Avatar aqiongbei Avatar razyliang Avatar leexiaohui1997 Avatar longlong688 Avatar inslog Avatar huichangkudelingdai Avatar Dream-new Avatar
Favorites 52 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.