关于 view-transitions
面向 Web 开发者的页面状态转场技能,介绍如何为 DOM 变化和页面导航添加动画。内容涵盖 View Transitions API、元素级 CSS 控制,以及其他导航动画流程。
主要功能
- 01使用 document.startViewTransition
- 02为转场元素指定名称
- 03为页面导航状态添加动画
- 04协调转场与数据加载时机
- 05以进入和退出动画作为替代方案
适用场景
- 01为详情面板展开添加动画
- 02在照片网格与照片详情间转场
- 03为 Web 应用添加导航动画
- 04为加载较慢的页面变化选择动画方案
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill view-transitions安装后在 Claude Code 中输入/view-transitions确认已加载。
详细介绍
为视图过渡添加动画
目录
适用场景
- 想要为不同页面状态或导航之间的过渡添加动画时,可以使用此功能。
- 这有助于为 Web 应用创建精致、类似应用的导航体验。
使用说明
- 使用
document.startViewTransition(callback)为 DOM 变更添加动画。 - 为需要在不同状态之间过渡的元素指定唯一的
view-transition-nameCSS 属性。 - 使用 API 前检查浏览器是否支持(
if (document.startViewTransition))。 - 在数据获取完成后再启动过渡,以尽量缩短 DOM 冻结的时间。
- 对于尚不支持 View Transitions API 的浏览器,可以考虑使用 CSS 动画作为回退方案。
详情
View Transitions 简介
View Transitions API 提供了一种简单的方法,可以将 DOM 的任何视觉变化从一种状态过渡到另一种状态。这既包括切换某些内容之类的小变化,也包括从一个页面导航到另一个页面之类的大幅变化。
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;
}
View Transitions 还能够使用更高级的动画为多处变化添加动画效果,而不仅限于默认的交叉淡化。为特定元素设置 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 撰写的优秀 View Transitions 文章 对这些细节做了很好的介绍。现在,让我们看看如何为整页导航添加过渡效果。
页面导航
典型的页面导航如下所示:
- 用户点击链接
- 请求数据
- 根据响应更新 DOM
在此流程中应用视图过渡时,需要考虑几件事。
首先,要尽量缩短屏幕处于冻结状态的时间。你可能已经注意到,视图过渡一旦开始,在回调完成之前,DOM 就无法交互。如果在用户点击链接时就开始过渡,他们可能要面对冻结的界面等待一段时间。为尽量减少这种不便,理想情况下应在请求完成后调用 document.startViewTransition。这样一来,我们就能立即进行更改,并尽可能迅速地更新 DOM。
其次,我们需要确保在更新 DOM 之前,已经捕获初始 DOM 的截图。使用第三方框架进行页面导航时,我们无法完全控制渲染过程;收到响应后,DOM 会自动更新。因此,我们没有一个独立的函数可以传递给 document.startViewTransition,以便妥善执行 DOM 更新。我们可能需要拦截、暂停并恢复渲染,从而营造出由单个函数更新 DOM 的假象。
好在,如果从 DOM 更新回调中返回一个 promise,视图过渡 API 会等到该 promise 兑现后再执行动画。我们可以利用此特性来处理上述时序问题。
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 App Router 仍处于 beta 阶段,相关最佳实践以及 pages 目录的用法可能会发生变化。
要在 Next.js 应用中使用此功能,首先要在开发环境中禁用 React 严格模式。严格模式会通过渲染两次组件来运行检查。这会干扰开发环境中的 ViewTransition 渲染流程,因此我们会全局禁用严格模式,并通过 StrictMode 组件为子组件重新启用它。
// next.config.js
const nextConfig = {
reactStrictMode: false,
};
module.exports = nextConfig;
接下来,在 pages/_app.js 中,我们会用 ViewTransition 和 StrictMode 组件包裹 Component,然后就能看到动画过渡效果:
// 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,该组件仍能正常工作,因此可以接受这一限制。
不使用 View Transitions 的替代方案
使用 View Transitions API 实现页面过渡有一个不可避免的缺点:动画开始前需要先获取新页面的 HTML。这可能需要一些时间,用户点击链接后也得不到任何反馈。加载指示器或许能填补这段空档,但我们可以在用户点击链接时立即为元素添加退场动画,然后在新 HTML 到达时再添加入场动画,从而争取一些时间。这与标准的 iOS 导航类似:界面会立即滑动,同时加载下一个屏幕。
- 用户点击链接
- 为元素添加退场动画;同时发起数据请求
- 等待响应返回且动画完成
- 为响应内容添加入场动画
这种方案与 View Transitions API 的主要区别在于,它无法让元素从一种状态_过渡_到另一种状态,因为在动画退场时,它还没有新 HTML,无法据此完成过渡。
两种方案都各有适用场景。例如,如果页面之间有共享元素,可以选择视图过渡;而如果页面变化较大、共享元素较少,则可以利用退场动画提供即时反馈。
要实现这一点,我们需要监听路由事件,具体取决于你使用的框架或库。尤其是,我们需要在用户导航时收到通知。使用 Next.js 时,可以通过 routeChangeStart router event 启动退场动画;但接下来我们来看看,如何在_不使用_ Next.js、React 或完全由客户端渲染的 HTML 的情况下实现这一点。
使用 Turbo 和 Turn 为服务器端渲染的多页面应用添加动画
注意:View Transition 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 动画方案。
总结
页面转场可以很好地传达从一个页面到下一个页面的变化。新的内置 View Transitions API 在提供新旧状态后,可以执行复杂的转场效果。通过监听框架事件,我们可以向 API 传达这些状态变化。对于页面导航,理想情况下应在请求完成后执行转场,以避免 DOM 处于非活动状态。
另一种(或互补的)方案是在用户点击链接后立即执行退出动画。这样可以在新 HTML 到达前,为请求完成争取一些时间。