在 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(); // mock function for demonstration purposes
});
在幕后,浏览器做了一些非常巧妙的事情。当过渡开始时,它会捕获页面的当前状态并截取屏幕截图。然后,它会保留该屏幕截图,同时 DOM 正在更新。DOM 更新完成后,它会捕获新状态,并在两个状态之间进行动画过渡。
虽然目前它仅在 Chrome(以及其他基于 Chromium 的浏览器)中实现,但WebKit 也表示支持它。即使您使用的是不受支持的浏览器,它也是渐进增强的一个完美候选者,因为我们始终可以回退到非动画导航。
需要注意的是,视图过渡是一个浏览器 API,而不是 SvelteKit API。onNavigate
是我们今天将使用的唯一 SvelteKit 特定 API。其他所有内容都可以在您编写 Web 代码的任何地方使用!有关视图过渡 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;
}
通过添加这行代码,指示器现在将平滑地滑动到新位置,而不是跳跃。
(您可能很容易错过差异——请查看屏幕顶部的那个小的移动三角形指示器!)
减少运动
在 Web 上实现动画时,尊重用户的运动偏好非常重要。仅仅因为您可以实现极端的页面过渡并不意味着您应该这样做。要为偏好减少运动的用户禁用所有页面过渡,您可以将以下内容添加到全局 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 的在线版本。以下是一些您可能觉得有用的其他视图过渡资源