渲染模式
动画视图过渡
注意: 单页应用程序的视图过渡 API 在 Chrome 111+ 中可用。
视图过渡简介
该 视图过渡 API 提供了一种简单的方法来过渡任何视觉 DOM 更改,从一个状态到下一个状态。这可能包括小的更改,例如切换一些内容,或者更大的更改,例如从一个页面导航到另一个页面。以下是一个 演示 在 SPA(单页应用程序)中使用视图过渡 API 的方法 src
JavaScript API 围绕 document.startViewTransition(callback)
展开,其中 callback
是一个函数,通常会更新 DOM 到新的状态。
让我们以切换 <details>
元素为例。
if (document.startViewTransition) {
// (check for browser support)
document.addEventListener("click", function (event) {
if (event.target.matches("summary")) {
event.preventDefault(); // (we'll toggle the element ourselves)
const details = event.target.closest("details");
document.startViewTransition(() => details.toggleAttribute("open"));
}
});
}
document.startViewTransition
在调用回调之前会拍摄当前 DOM 的快照。在这里,我们的回调只是切换 open
属性。完成后,浏览器就可以在初始快照和新版本之间进行过渡。
这些旧版本和新版本以伪元素的形式呈现,可以使用 CSS 中的 ::view-transition-old(root)
和 ::view-transition-new(root)
分别引用它们。例如,为了强调过渡,我们可以像这样延长 animation-duration
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 2s;
}
视图过渡还能够为更高级的动画动画化多个更改,这些动画超越了默认的交叉淡入淡出。通过为特定元素提供 CSS view-transition-name
,以及 containment
的 layout
或 paint
,API 让开发者可以精细控制元素的过渡方式,包括它们的宽度、高度和位置。这些高级过渡可以真正帮助传达从一个页面到另一个页面的流程。
以这个 图片库 为例。
最明显的过渡是图片的大小和位置,当每个页面上的 <img>
元素都被赋予相同的唯一 view-transition-name
,以及 CSS containment
值为 layout
时,会自动实现这种过渡。在这个演示中,view-transition-name
是在样式属性中硬编码的,但你也可以动态地添加它们(例如,在 onclick
处理程序中),只要它们对页面是唯一的,并且在开始过渡之前添加即可。
图片下方细节需要一些额外的样式。如果你注意到,每行细节都有一个分阶段的滑入/滑出动画。
我们为每个行元素赋予它自己的 view-transition-name
figcaption h2 {
contain: layout;
view-transition-name: photo-heading;
}
figcaption div {
contain: layout;
view-transition-name: photo-location-time;
}
figcaption dl {
contain: layout;
view-transition-name: photo-meta;
}
这将为每个区域生成过渡组,就像前面提到的新/旧快照一样,但只覆盖页面的一部分区域,而不是整个文档。就像整个文档过渡元素可以用 ::view-transition-old(root)
和 ::view-transition-new(root)
来定位一样,这些过渡组可以用 ::view-transition-old(NAME)
和 ::view-transition-new(NAME)
来定位。请注意,细节文本在图片网格页面上不存在,因此当从网格过渡到图片页面时,只会有 ::view-transition-new(NAME)
,而不是 ::view-transition-old(NAME)
,反之亦然,当以相反的方向导航时。因此,我们可以使用 :only-child
伪类定位这些情况,并自定义动画。对于 photo-heading
组。
/* Enter */
::view-transition-new(photo-heading):only-child {
animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-up;
}
/* Exit */
::view-transition-old(photo-heading):only-child {
animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-down;
}
这就是 API 的基础知识。 Jake Archibald 优秀的视图过渡文章 很好地涵盖了细节。现在,让我们看看如何过渡全页面导航。
页面导航
一个典型的页面导航看起来像这样。
- 用户点击链接
- 请求数据
- DOM 用响应更新
为了在这个流程中应用视图过渡,有一些需要考虑的地方。
首先,是最大限度地减少屏幕处于冻结状态的时间。你可能已经从上面的慢速过渡示例中注意到,一旦视图过渡开始,DOM 就会在回调完成之前不可交互。如果我们在用户点击链接时开始过渡,他们可能需要等待一段时间,而且 UI 会处于冻结状态。为了最大限度地减少这种令人讨厌的情况,理想情况下,document.startViewTransition
应该在请求完成之后调用。这样,我们就可以准备好进行更改,并且 DOM 可以尽可能快地更新。
其次,我们需要确保在更新 DOM 之前已经捕获了初始 DOM 快照。当使用第三方框架中的页面导航时,我们无法完全控制渲染过程;DOM 在接收到响应时会自动更新。因此,我们没有一个可以传递给 document.startViewTransition
的独立函数,该函数可以整齐地执行 DOM 更新。我们可能需要拦截、暂停和恢复渲染,以营造一种我们只有一个更新 DOM 函数的假象。
如果从 DOM 更新回调中返回一个 promise,视图过渡 API 会在执行动画之前等待它解析。我们可以使用这个功能来处理上面提到的时间问题。
React 组件示例
为了解决上述问题,我们将创建一个 React 类组件,因为它比函数组件更容易解释流程。我们将使用以下生命周期方法来控制渲染。
shouldComponentUpdate
: 我们将在这里返回false
并开始视图过渡——这将为我们赢得一些时间,让快照捕获完成。forceUpdate
: 在快照捕获完成后手动重新渲染组件。componentDidUpdate
: 通知视图过渡 API DOM 已经更新。
以下是它的样子。
import { Component } from "react";
export default class ViewTransition extends Component {
shouldComponentUpdate() {
if (!document.startViewTransition) return true; // skip when not supported
document.startViewTransition(() => this.#updateDOM());
return false; // don't update the component, we'll do this manually
}
#updateDOM() {
// now we know the screenshot has been taken, we can force render
// (which skips `shouldComponentUpdate`)
this.forceUpdate();
// set up a promise that will resolve when the component renders
return new Promise((resolve) => {
this.#rendered = resolve;
});
}
render() {
return this.props.children;
}
#rendered = () => {};
componentDidUpdate() {
// resolve the `updateDOM` promise to notify the View Transition API
// that the DOM has been updated
this.#rendered();
}
}
注意:Next.js 应用路由器 目前处于测试阶段,关于它和页面目录的最佳实践可能会发生变化。
要在 Next.js 应用中使用它,首先我们需要在开发环境中禁用 React 严格模式。严格模式通过两次渲染组件来运行其检查。这会干扰开发环境中 ViewTransition
的渲染流程,所以我们将全局禁用它,并使用 StrictMode
组件为子组件重新启用它。
// next.config.js
const nextConfig = {
reactStrictMode: false,
};
module.exports = nextConfig;
接下来,在 pages/_app.js
中,我们将把 Component
包裹在我们的 ViewTransition
和 StrictMode
组件中,我们应该开始看到动画过渡。
// pages/_app.js
import "@/styles/globals.css";
import { StrictMode } from "react";
import ViewTransition from "@/components/ViewTransition";
export default function App({ Component, pageProps }) {
return (
<ViewTransition>
<StrictMode>
<Component {...pageProps} />
</StrictMode>
</ViewTransition>
);
}
查看 Next.js 演示,我们的 实时 Next.js 演示 及其 源代码。
注意: React 文档建议不要使用 shouldComponentUpdate
和 forceUpdate
,并指出它们应该只用于性能优化,并且 shouldComponentUpdate
不保证被调用。由于页面动画是一种增强功能,并且即使 shouldComponentUpdate
没有被调用,这个组件也能正常工作,所以我对这个警告没有意见。
一种没有视图过渡的替代方法
视图过渡 API 用于页面过渡的一个必要缺点是,它需要在动画化之前获得新页面的 HTML。这可能需要一些时间,并且在用户点击链接后不会给用户任何反馈。一个加载动画可以填补空白,但我们可以通过在用户点击链接时立即动画化元素,然后在 HTML 到达时动画化新 HTML 来争取一些时间。这类似于标准 iOS 导航在加载下一个屏幕时立即滑过。
- 用户点击链接
- 元素被动画化出去;同时发出数据请求
- 等待响应和动画都完成
- 动画化响应
这种方法和视图过渡 API 的主要区别在于,它无法在元素之间过渡,因为在它动画化出去时,它没有新的 HTML 来进行过渡。
两种方法都有用,具体取决于情况。例如,如果页面之间有共享元素,你可能选择视图过渡,而如果更改很大,只有少数共享元素,你可以从退出动画的即时反馈中受益。
为了实现这一点,我们需要钩入路由事件,这将取决于你使用的框架或库。特别是,我们需要在用户导航时得到通知。在 Next.js 中,我们可以使用 routeChangeStart
路由事件 来启动退出动画,但让我们看看如何在没有 Next.js、React 或完全客户端渲染的 HTML 的情况下实现这一点。
使用 Turbo 和 Turn 为服务器端渲染的多页面应用程序制作动画
注意: 视图过渡 API 计划支持多页面导航,即不使用 JavaScript。但是,JavaScript API 对于更高级的过渡可能仍然需要。
Turbo 是 Hotwire 库套件的一部分(不要与 Vercel 的 Turbo 混淆),提供了一种逐步增强多页应用程序 (MPA) 的渲染方法。它旨在实现 SPA 的速度,而无需将代码架构为完全客户端渲染的应用程序。它是通过捕获链接点击和表单提交,使用 JavaScript 执行请求,并将 `<body>` 替换为响应中的新 `<body>` 来实现的。这样,它是一种混合方法:HTML 在服务器上生成,但 DOM 通过 JavaScript 更新。
Turn 是一个用于使用 Turbo 动画化页面导航的库。它支持两种动画方法(尽管当前视图过渡处于实验阶段)。Turn 在适当的时候向 `<html>` 元素添加 `turn-before-exit`、`turn-exit` 和 `turn-enter` 类,为开发者提供了一种定制动画的方法。
要使其正常工作,请将 `data-turn-exit` 和 `data-turn-enter` 属性添加到要动画化的元素中,然后应用您的 CSS 样式。例如,对于淡入/淡出
html.turn-exit [data-turn-exit] {
animation-name: fade-out;
animation-duration: 0.3s;
animation-fill-mode: forwards;
}
html.turn-enter [data-turn-enter] {
animation-name: fade-in;
animation-duration: 0.6s;
animation-fill-mode: forwards;
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
然后将 `Turn` 库导入到您的应用程序 JavaScript 中并调用 `Turn.start()`。
它的工作原理是挂钩到 Turbo 的渲染事件,并在需要时控制流程
turbo:visit
:在请求开始之前,添加 `turn-exit` 类turbo:before-render
:请求完成但新 HTML 渲染之前(类似于 React 的 `shouldComponentUpdate`),暂停渲染以等待任何退出动画完成turbo:render
:新 HTML 渲染后,移除 `turn-exit` 类并添加 `turn-enter` 类- 退出动画完成,移除 `turn-enter` 类
Turn 还对视图过渡提供了实验性支持,通过设置 `Turn.config.experimental.viewTransitions = true` 来启用。这将在支持的情况下使用视图过渡,并在不支持的情况下回退到 CSS 动画方法。(探索如何在个案基础上切换方法正在进行中 :)
总结
页面过渡可以是传达页面之间变化的好方法。新的内置视图过渡 API 可以使用旧状态和新状态执行复杂的过渡。通过挂钩到框架事件,我们可以将这些状态更改传达给 API。对于页面导航,理想情况下,过渡应该在请求完成之后发生,以避免 DOM 处于非活动状态。
另一种(或补充)方法是在用户点击链接后立即执行退出动画。这样做的好处是,在新的 HTML 到达之前,为请求完成争取一些时间。