在 SvelteKit 1.24 中解鎖視圖轉換
使用 onNavigate 簡化頁面轉換
最近,視圖轉換 API 在網頁開發領域掀起了一股風潮,而且這是理所當然的。它簡化了在兩個頁面狀態之間進行動畫處理的過程,這對於頁面轉換尤其有用。
然而,直到現在,你還不能在 SvelteKit 應用程式中輕鬆使用這個 API,因為它很難恰當地插入到導航生命週期的正確位置。SvelteKit 1.24 帶來了一個新的 onNavigate
生命週期鉤子,使視圖轉換的整合變得更加容易 — 讓我們深入了解一下。
視圖轉換的工作原理
你可以透過呼叫 document.startViewTransition
並傳遞一個更新 DOM 的回呼來觸發視圖轉換。就我們今天的目的而言,SvelteKit 將在使用者導航時更新 DOM。一旦回呼完成,瀏覽器將轉換到新的頁面狀態 — 預設情況下,它會在舊狀態和新狀態之間進行淡入淡出。
var document: Document
document.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.
Promise((res: (value: unknown) => void
res) => function setTimeout(callback: (args: void) => void, ms?: number): NodeJS.Timeout (+2 overloads)
setTimeout(res: (value: unknown) => void
res, 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: OnNavigate
navigation) => {
if (!var document: Document
document.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.
Promise((resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => void
resolve) => {
var document: Document
document.startViewTransition(async () => {
resolve: (value: void | (() => void) | PromiseLike<void | (() => void)>) => void
resolve();
await navigation: OnNavigate
navigation.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>
Promise((Creates a new Promise.
resolve: (value: unknown) => void
resolve) => {var document: Document
document.startViewTransition(async () => {resolve: (value: unknown) => void
resolve(); 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 的即時版本。以下是一些你可能會覺得有幫助的其他視圖轉換資源