跳至主要內容

在 SvelteKit 1.24 中解鎖視圖轉換

使用 onNavigate 簡化頁面轉換

最近,視圖轉換 API 在網頁開發領域掀起了一股風潮,而且這是理所當然的。它簡化了在兩個頁面狀態之間進行動畫處理的過程,這對於頁面轉換尤其有用。

然而,直到現在,你還不能在 SvelteKit 應用程式中輕鬆使用這個 API,因為它很難恰當地插入到導航生命週期的正確位置。SvelteKit 1.24 帶來了一個新的 onNavigate 生命週期鉤子,使視圖轉換的整合變得更加容易 — 讓我們深入了解一下。

視圖轉換的工作原理

你可以透過呼叫 document.startViewTransition 並傳遞一個更新 DOM 的回呼來觸發視圖轉換。就我們今天的目的而言,SvelteKit 將在使用者導航時更新 DOM。一旦回呼完成,瀏覽器將轉換到新的頁面狀態 — 預設情況下,它會在舊狀態和新狀態之間進行淡入淡出。

var document: Documentdocument.startViewTransition(async () => {
	await const domUpdate: () => Promise<void>domUpdate(); // mock function for demonstration purposes
});

在幕後,瀏覽器會執行一些非常聰明的操作。當轉換開始時,它會擷取頁面的當前狀態並拍攝螢幕截圖。然後,它會在 DOM 更新時將螢幕截圖保持在原位。一旦 DOM 更新完成,它會擷取新狀態,並在兩個狀態之間製作動畫。

雖然目前僅在 Chrome(和其他基於 Chromium 的瀏覽器)中實作,但 WebKit 也表示支持。即使你使用的是不受支援的瀏覽器,它也是漸進式增強的完美候選者,因為我們始終可以退回到非動畫導航。

重要的是要注意,視圖轉換是一個瀏覽器 API,而不是 SvelteKit API。onNavigate 是我們今天唯一會使用的 SvelteKit 特定 API。其他一切都可以在你為網路編寫程式碼的任何地方使用!有關視圖轉換 API 的更多資訊,我強烈建議閱讀 Jake Archibald 的 Chrome 解釋文件

onNavigate 的工作原理

在學習如何編寫視圖轉換之前,讓我們重點介紹一下讓這一切成為可能的功能:onNavigate

直到最近,SvelteKit 還有兩個導航生命週期函數:beforeNavigate,它在導航開始之前觸發,以及 afterNavigate,它在導航之後頁面更新後觸發。SvelteKit 1.24 引入了第三個函數:onNavigate,它將在每次導航時觸發,緊接在新頁面呈現之前。重要的是,它將在頁面的任何資料載入完成之後執行 — 由於啟動視圖轉換會阻止與頁面的任何互動,因此我們希望盡可能晚地啟動它。

你也可以從 onNavigate 返回一個 promise,它將暫停導航,直到它解析。這將讓我們等待完成導航,直到視圖轉換開始。

function function delayNavigation(): Promise<unknown>delayNavigation() {
	return new 
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((res: (value: unknown) => voidres) => function setTimeout(callback: (args: void) => void, ms?: number): NodeJS.Timeout (+2 overloads)setTimeout(res: (value: unknown) => voidres, 100));
} onNavigate(async (navigation) => { // do some work immediately before the navigation completes // optionally return a promise to delay navigation until it resolves return function delayNavigation(): Promise<unknown>delayNavigation(); });

有了這些,讓我們看看如何在你的 SvelteKit 應用程式中使用視圖轉換。

視圖轉換入門

查看視圖轉換運作情況的最佳方式是親自嘗試。你可以透過在你的本機終端機中執行 npm create svelte@latest,或在 StackBlitz 上的瀏覽器中啟動 SvelteKit 示範應用程式。請務必使用支援視圖轉換 API 的瀏覽器。執行應用程式後,將以下內容新增至 src/routes/+layout.svelte 中的腳本區塊。

import { function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<void | (() => void)>): void

A lifecycle function that runs the supplied callback immediately before we navigate to a new URL except during full-page navigations.

If you return a Promise, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use document.startViewTransition. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.

If a function (or a Promise that resolves to a function) is returned from the callback, it will be called once the DOM has updated.

onNavigate must be called during a component initialization. It remains active as long as the component is mounted.

onNavigate
} from '$app/navigation';
function onNavigate(callback: (navigation: import("@sveltejs/kit").OnNavigate) => MaybePromise<void | (() => void)>): void

A lifecycle function that runs the supplied callback immediately before we navigate to a new URL except during full-page navigations.

If you return a Promise, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use document.startViewTransition. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.

If a function (or a Promise that resolves to a function) is returned from the callback, it will be called once the DOM has updated.

onNavigate must be called during a component initialization. It remains active as long as the component is mounted.

onNavigate
((navigation: OnNavigatenavigation) => {
if (!var document: Documentdocument.startViewTransition) return; return new
var Promise: PromiseConstructor
new <void | (() => void)>(executor: (resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => void, reject: (reason?: any) => void) => void) => Promise<void | (() => void)>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => voidresolve) => {
var document: Documentdocument.startViewTransition(async () => { resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => voidresolve(); await navigation: OnNavigatenavigation.Navigation.complete: Promise<void>

A promise that resolves once the navigation is complete, and rejects if the navigation fails or is aborted. In the case of a willUnload navigation, the promise will never resolve

complete
;
}); }); });

這樣一來,發生的每次導航都會觸發視圖轉換。你已經可以看到它的運作情況 — 預設情況下,瀏覽器會在舊頁面和新頁面之間進行淡入淡出。

程式碼的工作原理

這段程式碼看起來可能有點嚇人 — 如果你很好奇,我可以逐行分解它,但現在只需知道新增它將允許你在導航期間與視圖轉換 API 互動。

如上所述,onNavigate 回呼將在導航之後新頁面呈現之前立即執行。在回呼內部,我們會檢查 document.startViewTransition 是否存在。如果不存在(即瀏覽器不支援它),我們會提早退出。

然後,我們會返回一個 promise,以延遲完成導航,直到視圖轉換開始。我們使用 promise 建構函式,以便我們可以控制 promise 何時解析。

return new 
var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>

Creates a new Promise.

@paramexecutor A callback used to initialize the promise. This callback is passed two arguments: a resolve callback used to resolve the promise with a value or the result of another promise, and a reject callback used to reject the promise with a provided reason or error.
Promise
((resolve: (value: unknown) => voidresolve) => {
var document: Documentdocument.startViewTransition(async () => { resolve: (value: unknown) => voidresolve(); await navigation.complete; }); });

在 promise 建構函式中,我們會啟動視圖轉換。在視圖轉換回呼內部,我們會解析剛返回的 promise,這表示 SvelteKit 應該完成導航。重要的是,導航必須等待完成,直到 *之後* 我們才啟動視圖轉換 — 瀏覽器需要快照舊狀態,以便它可以轉換到新狀態。

最後,在視圖轉換回呼內部,我們會等待 SvelteKit 透過等待 navigation.complete 來完成導航。一旦 navigation.complete 解析,新頁面就已載入到 DOM 中,並且瀏覽器可以在兩個狀態之間製作動畫。

這有點囉嗦,但是透過不抽象化它,我們可以讓你直接與視圖轉換互動,並進行你需要的任何自訂。

使用 CSS 自訂轉換

我們也可以使用 CSS 動畫自訂此頁面轉換。在你的 +layout.svelte 的樣式區塊中,新增以下 CSS 規則。

@keyframes fade-in {
	from {
		opacity: 0;
	}
}

@keyframes fade-out {
	to {
		opacity: 0;
	}
}

@keyframes slide-from-right {
	from {
		transform: translateX(30px);
	}
}

@keyframes slide-to-left {
	to {
		transform: translateX(-30px);
	}
}

:root::view-transition-old(root) {
	animation:
		90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
		300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

:root::view-transition-new(root) {
	animation:
		210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
		300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

現在,當你在頁面之間導航時,舊頁面將淡出並滑向左側,而新頁面將淡入並從右側滑入。這些特定的動畫樣式來自 Jake Archibald 出色的 關於視圖轉換的 Chrome 開發人員文章,如果你想了解此 API 的所有功能,則非常值得一讀。

請注意,我們必須在 ::view-transition 虛擬元素之前新增 :root — 這些元素僅位於文件的根目錄,因此我們不希望 Svelte 將它們範圍限定到元件。

你可能已經注意到整個頁面都會滑入和滑出,即使頁首在舊頁面和新頁面上都是相同的。為了使轉換更順暢,我們可以為頁首提供唯一的 view-transition-name,以便它與頁面的其餘部分分開製作動畫。在 src/routes/Header.svelte 中,在樣式區塊中找到 header CSS 選取器並新增視圖轉換名稱。

header {
	display: flex;
	justify-content: space-between;
	view-transition-name: header;
}

現在,頁首將不會在導航時滑入和滑出,但頁面的其餘部分會。

修正類型

由於並非所有瀏覽器都支援 startViewTransition,因此你的 IDE 可能不知道它存在。為了消除錯誤並取得正確的類型,請將以下內容新增至你的 app.d.ts

declare global {
	// preserve any customizations you have here
	namespace App {
		// interface Error {}
		// interface Locals {}
		// interface PageData {}
		// interface Platform {}
	}

	// add these lines
	interface ViewTransition {
		updateCallbackDone: Promise<void>;
		ready: Promise<void>;
		finished: Promise<void>;
		skipTransition: () => void;
	}

	interface Document {
		startViewTransition(updateCallback: () => Promise<void>): ViewTransition;
	}
}

export {};

轉換個別元素

我們剛剛看到如何為元素提供 view-transition-name 可將其與頁面其餘部分的動畫分開。設定 view-transition-name 也會指示瀏覽器在轉換完成後平滑地將其動畫化到新位置。view-transition-name 充當唯一識別碼,因此瀏覽器可以識別舊狀態和新狀態中的相符元素。

讓我們看看它看起來像什麼 — 我們的示範應用程式的導航有一個小三角形,指示活動頁面。現在,在我們導航後,它會在新位置突然出現。讓我們為它提供一個 view-transition-name,以便瀏覽器將其動畫化到新位置。

src/routes/Header.svelte 中,找到建立活動頁面指示器的 CSS 規則,並為其提供一個 view-transition-name

li[aria-current='page']::before {
	/* other existing rules */
	view-transition-name: active-page;
}

透過新增這一行,指示器現在會平滑地滑到新位置,而不是跳躍。

(可能很容易錯過差異 — 請查看螢幕頂部移動的小三角形指示器!)

減少運動

在網路上實作動畫時,尊重使用者的運動偏好非常重要。僅僅因為你可以實作極端的頁面轉換,並不表示你應該這麼做。若要為偏好減少運動的使用者停用所有頁面轉換,你可以將以下內容新增至全域 styles.css

@media (prefers-reduced-motion) {
	::view-transition-group(*),
	::view-transition-old(*),
	::view-transition-new(*) {
		animation: none !important;
	}
}

雖然這可能是最安全的選項,但減少運動不一定表示沒有動畫。相反地,你可以根據情況逐一考慮你的視圖轉換。例如,也許我們會停用滑動動畫,但保留預設的淡入淡出(不涉及運動)。你可以透過將你要停用的 ::view-transition 規則包裝在 prefers-reduced-motion: no-preference 媒體查詢中來完成此操作

@media (prefers-reduced-motion: no-preference) {
	:root::view-transition-old(root) {
		animation:
			90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
			300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
	}

	:root::view-transition-new(root) {
		animation:
			210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
			300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
	}
}

下一步是什麼?

如你所見,SvelteKit 並未抽象化太多關於視圖轉換 *如何* 運作的資訊 — 你是直接與瀏覽器的內建 document.startViewTransition::view-transition API 互動,而不是像 Nuxt 和 Astro 中那樣的框架抽象。我們很期待看到人們最終如何在 SvelteKit 應用程式中使用視圖轉換,以及在未來是否加入我們自己更高層次的抽象化是有意義的。

資源

你可以在 GitHub 上找到這篇文章的示範程式碼,以及 部署到 Vercel 的即時版本。以下是一些你可能會覺得有幫助的其他視圖轉換資源