解锁 SvelteKit 1.24 中的视图过渡
使用 onNavigate 实现流畅的页面过渡
最近,视图过渡 API 在 Web 开发圈引起了广泛关注,这绝非偶然。它简化了两个页面状态之间动画的过程,特别适用于页面过渡。
然而,到目前为止,你很难在 SvelteKit 应用中使用这个 API,因为在导航生命周期的正确位置插入它是很困难的。SvelteKit 1.24 引入了一个新的 onNavigate
生命周期钩子,使视图过渡的集成更加简单——让我们深入了解一下。
视图过渡如何工作
你可以通过调用 document.startViewTransition
并传递一个更新 DOM 的回调来触发视图过渡。就我们今天的目的来说,SvelteKit 将在用户导航时更新 DOM。一旦回调完成,浏览器将过渡到新的页面状态——默认情况下,它会在新旧状态之间进行交叉淡入淡出。
var document: Document
document.startViewTransition(async () => {
await const domUpdate: () => Promise<void>
domUpdate(); // 演示用的伪函数
});
在幕后,浏览器做了一些非常聪明的事情。当过渡开始时,它会捕获当前页面状态并拍摄一个截图。在 DOM 更新的过程中,它会保持该截图的显示。一旦 DOM 更新完成,它会捕获新的状态,并在两个状态之间进行动画过渡。
虽然目前这个功能仅在 Chrome(以及其他基于 Chromium 的浏览器)中实现,WebKit 也持支持态度。即使你使用的是不支持的浏览器,这也是一个逐步增强的完美候选,因为我们总是可以回退到非动画式的导航。
需要注意的是,视图过渡是一个浏览器 API,而不是 SvelteKit 的功能。onNavigate
是我们今天唯一会用到的 SvelteKit 特定的 API,其他的可以在任何 Web 开发中使用!关于视图过渡 API 的更多信息,我强烈推荐 Jake Archibald 的 Chrome explainer。
onNavigate 如何工作
在学习如何编写视图过渡之前,我们先来看一下让这一切成为可能的函数:onNavigate
。
在最近,SvelteKit 有两个导航生命周期函数:beforeNavigate
,它在导航开始之前触发;另一个是 afterNavigate
,它在导航后的页面更新完成后触发。SvelteKit 1.24 引入了第三个:onNavigate
,它将在每次导航时触发,在新页面呈现出来之前立即运行。重要的是,它会在页面数据加载完成之后运行——因为启动视图过渡会阻止页面交互,我们希望尽可能晚地启动它。
你还可以从 onNavigate
返回一个 Promise,这会延迟导航的完成,直到 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) => {
// 在导航完成前立即执行一些操作
// 可选地返回一个 Promise,以便延迟导航直到其被解析
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 应该完成导航。这里关键的一点是,导航需要等到视图过渡启动之后完成——浏览器需要捕获旧状态的快照,以便过渡到新状态。
最后,在视图过渡回调中,我们通过等待
navigation.complete
确保 SvelteKit 完成导航。一旦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 的优秀文章,如果你想了解这个 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 { // 保留现有的自定义内容 namespace App { // interface Error {} // interface Locals {} // interface PageData {} // interface Platform {} } // 添加以下几行 interface ViewTransition { updateCallbackDone: Promise<void>; ready: Promise<void>; finished: Promise<void>; skipTransition: () => void; } interface Document { startViewTransition(updateCallback: () => Promise<void>): ViewTransition; } } export {};