mcpField
V
view-transitions

view-transitions

github.com/patternsdev设计与 UI257679 次安装28 周前更新
代码仓库

关于 view-transitions

面向 Web 开发者的页面状态转场技能,介绍如何为 DOM 变化和页面导航添加动画。内容涵盖 View Transitions API、元素级 CSS 控制,以及其他导航动画流程。

主要功能

  1. 01使用 document.startViewTransition
  2. 02为转场元素指定名称
  3. 03为页面导航状态添加动画
  4. 04协调转场与数据加载时机
  5. 05以进入和退出动画作为替代方案

适用场景

  1. 01为详情面板展开添加动画
  2. 02在照片网格与照片详情间转场
  3. 03为 Web 应用添加导航动画
  4. 04为加载较慢的页面变化选择动画方案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill view-transitions

安装后在 Claude Code 中输入/view-transitions确认已加载。

详细介绍

为视图过渡添加动画

目录

适用场景

  • 想要为不同页面状态或导航之间的过渡添加动画时,可以使用此功能。
  • 这有助于为 Web 应用创建精致、类似应用的导航体验。

使用说明

  • 使用 document.startViewTransition(callback) 为 DOM 变更添加动画。
  • 为需要在不同状态之间过渡的元素指定唯一的 view-transition-name CSS 属性。
  • 使用 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 文章 对这些细节做了很好的介绍。现在,让我们看看如何为整页导航添加过渡效果。

页面导航

典型的页面导航如下所示:

  1. 用户点击链接
  2. 请求数据
  3. 根据响应更新 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 导航类似:界面会立即滑动,同时加载下一个屏幕。

  1. 用户点击链接
  2. 为元素添加退场动画;同时发起数据请求
  3. 等待响应返回且动画完成
  4. 为响应内容添加入场动画

这种方案与 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 的渲染事件,并根据需要控制流程来实现:

  1. turbo:visit:请求开始前,添加 turn-exit 类
  2. turbo:before-render:请求完成后、新 HTML 渲染前(类似于 React 的 shouldComponentUpdate),暂停渲染,等待退出动画完成
  3. turbo:render:新 HTML 渲染完成后,移除 turn-exit 类并添加 turn-enter 类
  4. 退出动画完成后,移除 turn-enter 类

Turn 还实验性地支持视图转场,可通过设置 Turn.config.experimental.viewTransitions = true 启用。在支持的情况下,它会使用视图转场,否则会回退到 CSS 动画方案。

总结

页面转场可以很好地传达从一个页面到下一个页面的变化。新的内置 View Transitions API 在提供新旧状态后,可以执行复杂的转场效果。通过监听框架事件,我们可以向 API 传达这些状态变化。对于页面导航,理想情况下应在请求完成后执行转场,以避免 DOM 处于非活动状态。

另一种(或互补的)方案是在用户点击链接后立即执行退出动画。这样可以在新 HTML 到达前,为请求完成争取一些时间。

来源

评论

支持 Markdown