跳转到内容

视图过渡动画

Astro 仅需几行代码就可以支持可选择的,每个页面的视图过渡动画。视图过渡动画可以在不刷新浏览器的情况下更新页面内容,并在页面之间提供无缝的动画效果。

Astro 提供了一个 <ViewTransitions /> 路由组件,可以添加到单个页面的 <head> 中,以控制页面在导航到另一个页面时的过渡效果。它提供了一个轻量级的客户端路由器,拦截导航 并允许你自定义页面之间的过渡效果。

将此组件添加到一个可复用的 .astro 组件中,例如公共头部或布局组件,以便 在整个站点上实现动画页面过渡(SPA 模式)

Astro 的视图过渡动画支持由新的 View Transitions 浏览器 API 提供,还包括:

向页面添加视图过渡动画

段落标题 向页面添加视图过渡动画

通过在每个页面的 <head> 中导入和添加 <ViewTransitions /> 路由组件,可以选择在单个页面上使用视图过渡动画。

src/pages/index.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="en">
<head>
<title>我的主页</title>
<ViewTransitions />
</head>
<body>
<h1>欢迎来到我的网站!</h1>
</body>
</html>

整站视图过渡动画(SPA 模式)

段落标题 整站视图过渡动画(SPA 模式)

导入 <ViewTransitions /> 组件并将其添加到公共 <head> 或共享布局组件中。Astro 将根据旧页面和新页面之间的相似之处创建默认的页面动画,并为不支持的浏览器提供回退行为。

下面的示例显示了如何通过导入和添加此组件到 <CommonHead /> Astro 组件中,为不支持的浏览器提供默认的页面导航动画,包括默认的回退控制选项,从而在整个站点上添加 Astro 的默认页面导航动画:

src/components/CommonHead.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<!-- 主要 Meta 标签 -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<ViewTransitions />

无需其他配置即可启用 Astro 的默认客户端导航!

对于更精细的控制,可以在单个元素上使用 过渡动画指令覆盖默认的客户端导航

Astro 会自动为在旧页面和新页面中找到的相应元素并分配一个共享的唯一view-transition-name。这对匹配的元素是通过元素的类型和其在 DOM 中的位置推断出来的。

在你的 .astro 组件的页面元素上使用可选的 transition:* 指令,以更精确地控制导航期间的页面过渡行为。

  • transition:name:允许你覆盖 Astro 的默认元素匹配方式,为一对 DOM 元素 指定过渡动画名称
  • transition:animate:在用新元素替换旧元素时,允许你通过指定动画类型来覆盖 Astro 的默认动画效果。可以使用 Astro 的 内置动画指令创建自定义动画
  • transition:persist: 允许你覆盖 Astro 的默认行为。不同于在导航到另一个页面时用将替换旧元素为新元素的方式,该行为则是在导航到另一个页面时 保留组件和 HTML 元素的状态

在某些情况下,你可能希望自己对相应的且包含视图过渡动画的元素进行标识,那么你可以使用 transition:name 指令为一对元素指定一个名称。

src/pages/old-page.astro
<aside transition:name="hero">
src/pages/new-page.astro
<aside transition:name="hero">

注意,提供的 transition:name 的值在每个页面上只能使用一次。当 Astro 无法自动推断出合适的名称,或需要更精确地控制匹配元素时,请手动设置这个名称。

添加于: astro@2.10.0

你可以使用 transition:persist 指令在页面导航时保持组件和 HTML 元素的状态(而不是替换它们)。

例如,以下 <video> 元素在你导航到包含相同视频元素的另一个页面时仍然会继续播放。这适用于前进和后退导航。

src/components/Video.astro
<video controls="" autoplay="" transition:persist>
<source src="https://ia804502.us.archive.org/33/items/GoldenGa1939_3/GoldenGa1939_3_512kb.mp4" type="video/mp4">
</video>

你也可以将指令放在一个 Astro 群岛 上(即带有 client: 指令 的 UI 框架组件)。如果该组件存在于下一个页面上,旧页面上的岛屿与其当前状态将继续显示,而不是用新页上的岛屿替换它。

在下面的示例中,当在包含具有 transition:persist 属性的 <Counter /> 组件的页面之间来回导航时,组件内部状态的计数不会重置。

components/Header.astro
<Counter client:load transition:persist initialCount={5} />

如果岛屿(或元素)在两个页面之间位于不同的组件中,你也可以手动标识相应的元素

src/pages/old-page.astro
<Video controls="" autoplay="" transition:name="media-player" transition:persist />
src/pages/new-page.astro
<MyVideo controls="" autoplay="" transition:name="media-player" transition:persist />

作为一种便捷的方式,transition:persist 还可以将过渡动画名称作为值传入。

src/pages/index.astro
<video controls="" autoplay="" transition:persist="media-player">

添加于: astro@4.5.0

这允许你控制在导航时是否应该保持岛屿的 props。

默认情况下,当你向岛屿添加 transition:persist 时,状态会在导航时保持,但你的组件将会使用新的 props 重新渲染。例如,当一个组件接收到特定于页面的 props,如当前页面的 title 时,这是很有用的。

除了 transition:persist 之外,你也可以使用 transition:persist-props 覆盖这种行为。添加这个指令将会保持岛屿现有的 props(不使用新值重新渲染)并维持其现有状态。

Astro 提供了一些内置的动画选项,用于覆盖默认的 fade 过渡动画。在单个元素上添加 transition:animate 指令,以自定义特定过渡的行为。

  • fade (默认):一个基于 Astro 的淡入淡出动画。旧内容淡出,新内容淡入。
  • initial: 退出 Astro 的默认淡入淡出动画,并使用浏览器的默认样式。
  • slide: 旧内容向左滑出,新内容从右侧滑入的动画。在后退导航时,动画是相反的。
  • none: 禁用浏览器的默认动画。在页面的 <html> 元素上使用,以禁用页面上每个元素的默认淡入淡出动画。

组合这些指令以完全控制页面动画。在 <html> 元素上设置页面默认值,并根据需要在任何单个元素上进行覆盖。

下面的示例为页面内容产生了一个滑动动画,同时禁用了页面其余部分的浏览器默认淡入淡出动画:

---
import CommonHead from '../components/CommonHead.astro';
---
<html transition:animate="none">
<head>
<CommonHead />
</head>
<body>
<header>
...
</header>
<!-- 覆盖单个元素上的页面默认值 -->
<main transition:animate="slide">
...
</main>
</body>
</html>

你可以使用任何 CSS 动画属性来自定义过渡的各个方面。

要自定义内置动画,请首先从 astro:transitions 导入该动画,然后传入自定义选项。

下面的示例展示了如何自定义了内置 fade 动画的持续时间:

---
import { fade } from 'astro:transitions';
---
<header transition:animate={fade({ duration: '0.4s' })}>

你还可以通过定义前进和后退行为以及新旧页面来为 transition:animate 定义自己的动画,具体类型如下所示:

export interface TransitionAnimation {
name: string; // 关键帧的名称
delay?: number | string;
duration?: number | string;
easing?: string;
fillMode?: string;
direction?: string;
}
export interface TransitionAnimationPair {
old: TransitionAnimation | TransitionAnimation[];
new: TransitionAnimation | TransitionAnimation[];
}
export interface TransitionDirectionalAnimations {
forwards: TransitionAnimationPair;
backwards: TransitionAnimationPair;
}

下面的示例展示了在根布局文件中的 <style is:global> 标签内,定义自定义 bump 动画的所有必要属性:

src/layouts/Layout.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<html lang="en">
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
<style is:global>
@keyframes bump {
0% {
opacity: 0;
transform: scale(1) translateX(200px);
}
50% {
opacity: 0.5;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1) translateX(0);
}
}
</style>

动画的行为必须在每个使用动画的组件中,定义在 frontmatter 里:

src/pages/index.astro
---
const anim = {
old: {
name: 'bump',
duration: '0.5s',
easing: 'ease-in',
direction: 'reverse',
},
new: {
name: 'bump',
duration: '0.5s',
easing: 'ease-in-out',
},
};
const customTransition = {
forwards: anim,
backwards: anim,
};
---
<header transition:animate={customTransition}> ... </header>

当定义一个自定义动画时,你具有很强大的灵活性。为了实现渴望的效果,或许你得考虑一些不那么寻常的动画组合,例如针对不同的对象使用不同的样式应用,或是为它们提供不同的关键帧动画。

<ViewTransitions /> 路由器通过监听以下事件来处理导航:

  • 点击 <a> 元素。
  • 后退和前进导航事件。

如下选项允许你进一步控制在路由器中发生的导航事件:

只有关联的两个页面都使用了 <ViewTransitions /> 路由器才能通过客户端路由进行导航,防止重新加载全页面。你可能也不希望在每次导航更改时都使用客户端路由,而是更希望在特定路由上进行传统的页面导航。

你可以通过将 data-astro-reload 属性添加到任何 <a><form> 标签中,为单个链接选择不使用客户端路由器。此属性将覆盖任何现有的 <ViewTransitions /> 组件,而是在导航期间触发浏览器刷新。

以下示例显示了仅从主页导航到文章时如何阻止客户端路由器。这样,当从文章列表页面导航到同一页面时,仍然可以对共享元素(例如主视觉图)进行动画处理:

src/pages/index.astro
<a href="/articles/emperor-penguins" data-astro-reload>
src/pages/articles.astro
<a href="/articles/emperor-penguins">

具有 data-astro-reload 属性的链接将会被路由器忽略,并且进行全页面导航。

你还可以使用 navigate 方法在通常不被 <ViewTransitions /> 路由器监听的事件里触发客户端导航。该函数来自于 astro:transitions/client 模块,可以在任何客户端脚本和通过客户端指令激活了的客户端组件中使用。

下面的示例展示了一个当访客选择菜单中的选项时导航到另一个页面的 Astro 组件:

src/components/Form.astro
<script>
import { navigate } from 'astro:transitions/client';
// 自动导航到所选选项
document.querySelector('select').onchange = (ev) => {
let href = ev.target.value;
navigate(href);
};
</script>
<select>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>
src/pages/index.astro
---
import Form from "../components/Form.astro";
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form />
</body>
</html>

下面的示例在一个 React 的 <Form /> 组件中使用 navigate() 实现了相同的功能:

src/components/Form.jsx
import { navigate } from "astro:transitions/client";
export default function Form() {
return (
<select onChange={(e) => navigate(e.target.value)}>
<option value="/play">Play</option>
<option value="/blog">Blog</option>
<option value="/about">About</option>
<option value="/contact">Contact</option>
</select>
);
}

这个 <Form /> 组件可以在使用 <ViewTransitions /> 路由器的 Astro 页面上带着客户端指令渲染:

src/pages/index.astro
---
import Form from "../components/Form.jsx";
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<Form client:load />
</body>
</html>

navigate 方法接受以下参数:

  • href(必需项)- 要导航到的新页面。
  • options - 一个可选的对象,具有以下属性:
    • history: 'push' | 'replace' | 'auto'
      • 'push': 路由器将使用 history.pushState 在浏览器历史中创建一个新条目。
      • 'replace': 路由器将使用 history.replaceState 更新 URL,而不向导航添加新条目。
      • 'auto'(默认): 路由器将尝试使用 history.pushState,但如果无法转换到指定的 URL,则当前 URL 将保持不变,浏览器历史不会发生变化。
  • formData: 一个 POST 请求的 FormData 对象。

要想通过浏览器的历史记录进行后退和前进导航,你可以将 navigate() 与浏览器内置的 history.back()history.forward()history.go() 等函数结合使用。如果 navigate() 在服务器渲染时被调用,它不会有任何效果。

替换浏览器历史条目

段落标题 替换浏览器历史条目

通常情况下,你每次导航时都会在浏览器的历史记录中写入一个新条目。这样可以使用浏览器的 backforward 按钮在页面之间导航。

<ViewTransitions /> 路由器器允许你通过向任何单独的 <a> 标签添加 data-astro-history 属性来覆盖历史记录条目。

data-astro-history 属性可以设置为与 navigate() 函数的 history 选项 相同的三个值:

data-astro-history: 'push' | 'replace' | 'auto'

  • 'push':路由器将使用 history.pushState 在浏览器历史记录中创建一个新条目。
  • 'replace':路由器将使用 history.replaceState 更新 URL 而不添加新的导航条目。
  • 'auto'(默认值):路由器将尝试使用 history.pushState,但是如果 URL 不能进行过渡,则当前 URL 将保持不变,并且浏览器历史记录也不会发生任何变更。

以下示例导航到 /main 页面,但不向浏览历史记录中添加新条目,而是重用历史记录中的当前条目(/confirmation)并进行覆盖。

src/pages/confirmation.astro
<a href="/main" data-astro-history="replace">

这样做的效果是,如果你从 /main 页面返回,则浏览器不会显示 /confirmation 页面,而是显示该页面之前的页面。

表单的视图过渡动画

段落标题 表单的视图过渡动画

添加于: astro@4.0.0

<ViewTransitions /> 路由器将从 <form> 元素触发页面内的转换,支持 GETPOST 请求。

默认情况下,当你的 method 设置为 POST 时,Astro 会将表单数据提交为 multipart/form-data。如果你想要匹配浏览器的默认行为,请使用 enctype 属性将你的数据编码为 application/x-www-form-urlencoded

src/components/Form.astro
<form action="/contact" method="POST" enctype="application/x-www-form-urlencoded">
<!-- -->
</form>

你可以使用 data-astro-reload 属性在任何单独的表单上选择退出路由器视图过渡动画:

src/components/Form.astro
<form action="/contact" data-astro-reload>
<!-- -->
</form>

<ViewTransitions /> 路由器在支持视图过渡动画的浏览器(如 Chromium 浏览器)中表现最佳,但也包含了对其他浏览器的默认回退支持。即当浏览器不支持视图过渡动画 API 时,Astro 仍将提供基于浏览器的导航,使用其中一种回退选项来实现类似的效果。

你可以通过在 <ViewTransitions /> 组件上添加一个名为 fallback 的属性,并将其设置为 swapnone,来覆盖 Astro 的默认回退支持:

  • animate(默认,推荐使用)- Astro 将在更新页面内容之前使用自定义属性模拟视图过渡动画。
  • swap - Astro 不会尝试进行页面动画效果。相反,旧页面将立即被新页面替换。
  • none - Astro 将不进行任何页面过渡动画。在不支持的浏览器中,你将获得全量的页面导航。
---
import { ViewTransitions } from 'astro:transitions';
---
<title>My site</title>
<ViewTransitions fallback="swap" />

<ViewTransitions /> 路由器被使用时,以下步骤将会产生 Astro 的客户端导航:

  1. 一个访客通过以下任何操作触发导航:

    • 点击链接到站点上另一个页面的 <a> 标签。
    • 点击后退按钮。
    • 点击前进按钮。
  2. 路由器开始获取下一个页面。

  3. 路由器将 data-astro-transition 属性添加到 HTML 元素中,并将其值设置为 'forward''back'

  4. 路由器调用 document.startViewTransition。这将触发浏览器自己的 视图过渡动画流程。重要的是,浏览器会对页面的当前状态进行截图。

  5. startViewTransition 回调中,路由器执行 swap,包括以下事件序列:

    • <head> 的内容被交换,其中一些元素被保留:

      • 如果新页面上存在,则保留样式表 DOM 节点,以防止 FOUC。
      • 如果新页面上存在,则保留脚本。
      • 如果新页面上存在对应的元素,则保留任何其他带有 transition:persist 的 head 元素。
    • <body> 被新页面的 <body> 完全替换。

    • 任何带有 transition:persist 的元素(如果存在)都会被移动到新的 DOM 中。

    • 如果需要,将恢复滚动位置。

    • astro:after-swap 事件在 document 上触发。这是 swap 过程的结束。

  6. 路由器等待任何新样式表加载完成,然后才会解析过渡动画。

  7. 路由器执行添加到页面的任何新脚本。

  8. astro:page-load 事件触发。这是导航流程的结束。

在视图过渡中的脚本行为

段落标题 在视图过渡中的脚本行为

当你在现有的 Astro 项目中添加视图过渡时,一些脚本可能不会在页面导航后重新运行,就像它们在全页面浏览器刷新时所做的那样。使用以下信息来确保你的脚本按预期执行。

在使用 <ViewTransitions /> 组件导航页面时,脚本将按顺序运行以匹配浏览器行为。

Astro 默认的打包模块脚本在初始执行之后将只会被执行一次。即使在页面过渡后,该脚本存在于新页面上,它也会被忽略。

与打包的模块脚本不同,内联脚本 在用户访问网站期间,如果它们存在于被多次访问的页面上,就有可能被重新执行。当访客导航到一个没有该脚本的页面,然后再回到一个有该脚本的页面时,内联脚本也可能会重新执行。

添加于: astro@4.5.0

要强制内联脚本在每次过渡后重新执行,请添加 data-astro-rerun 属性。向脚本添加任何属性也会隐式地添加 is:inline,因此这只适用于未被 Astro 打包和处理的脚本。

<script is:inline data-astro-rerun>...</script>

为了确保脚本在客户端导航期间每次加载页面时都能运行,它应该由一个生命周期事件执行。例如,DOMContentLoaded 的事件监听器可以被 astro:page-load 生命周期事件替换。

如果你有在内联脚本中设置了全局状态的代码,那么这个状态需要考虑到脚本可能会被执行多次的情况。在你的 <script> 标签中检查全局状态,并在可能的情况下有条件地执行你的代码。这是因为 window 对象是被保留的。

<script is:inline>
if (!window.SomeGlobal) {
window.SomeGlobal = {} // ....
}
</script>

有关在项目中更新现有脚本的示例,请参阅 添加视图过渡动画教程

<ViewTransition /> 路由器在导航期间在 document 上触发了许多事件。这些事件提供了导航生命周期的钩子,允许你做一些事情,例如显示新页面正在加载的指示器、覆盖默认行为以及在导航完成时恢复状态。

导航过程包括一个准备阶段,在此阶段加载新内容;一个DOM 交换阶段,在此阶段旧页面的内容被新页面的内容替换;以及一个完成阶段,在此阶段执行脚本,报告加载完成,并进行清理工作。

Astro 的视图过渡动画 API 生命周期事件顺序如下:

虽然某些动作可以在任何事件中触发,但是某些任务只能在特定事件中执行,以获得最佳结果,例如在准备阶段之前显示加载指示器或在交换内容之前覆盖动画对。

添加于: astro@3.6.0

在准备阶段开始时触发的事件,导航开始后(例如,用户点击链接后),但在加载内容之前。

此事件用于:

  • 在加载开始之前做一些事情,例如显示加载指示器。
  • 更改加载内容,例如从模板中加载而不是从外部 URL 加载。
  • 在加载时更改 direction(通常为 forwardbackward)以进行自定义动画。

以下示例展示了如何在内容加载前使用 astro:before-preparation 事件加载一个加载指示器,并在加载完成后立即停止它。请注意,以这种方式使用加载器回调允许异步执行代码。

<script is:inline>
document.addEventListener('astro:before-preparation', ev => {
const originalLoader = ev.loader;
ev.loader = async function() {
const { startSpinner } = await import('./spinner.js');
const stop = startSpinner();
await originalLoader();
stop();
};
});
</script>

添加于: astro@3.6.0

在准备阶段结束时触发的事件,新页面的内容在加载并解析为文档后。此事件发生在视图过渡阶段之前。

此示例使用 astro:before-preparation 事件来启动加载指示器,使用 astro:after-preparation 事件来停止它:

<script is:inline>
document.addEventListener('astro:before-preparation', () => {
document.querySelector('#loading').classList.add('show');
});
document.addEventListener('astro:after-preparation', () => {
document.querySelector('#loading').classList.remove('show');
});
</script>

这是一个比上面显示的加载 spinner 更简单的版本:如果所有监听器的代码都可以同步执行,则无需进行加载器的回调。

添加于: astro@3.6.0

在新文档(在准备阶段填充)替换当前文档之前触发的事件。此事件发生在视图过渡中,用户仍然可以看到旧页面的快照。

此事件可用于在交换发生之前进行更改。事件上的 newDocument 属性表示传入的文档。以下示例确保浏览器中的浅色或深色模式偏好设置在新页面中得到保留:

<script is:inline>
function setDarkMode(document) {
let theme = localStorage.darkMode ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
}
setDarkMode(document);
document.addEventListener('astro:before-swap', ev => {
// Pass the incoming document to set the theme on it
setDarkMode(ev.newDocument);
});
</script>

astro:before-swap 事件还可用于更改交换的实现。默认的交换实现会对 head 内容进行 diff,将 persistent 元素从旧文档移动到 newDocument,然后用新文档的 body 替换整个 body

在生命周期的这一点上,你可以选择定义自己的交换实现,例如对现有文档的整个内容进行 diff(某些其他路由器会这样做):

<script is:inline>
document.addEventListener('astro:before-swap', ev => {
ev.swap = () => {
diff(document, ev.newDocument);
};
});
</script>

在新页面替换旧页面后立即触发的事件。你可以在 document 上监听此事件,并触发在新页面的 DOM 元素渲染和脚本运行之前发生的操作。

此事件在旧页面上监听时,可用于传递和恢复需要转移到新页面的 DOM 上的任何状态。

这是生命周期中的最后一个点,仍然可以安全地在其中添加一个深色模式类名(<html class="dark-mode">),尽管你可能希望在更早的事件中这样做。

astro:after-swap 事件发生在浏览器历史记录已更新并设置了滚动位置之后。因此,可以使用此事件的一个用途是覆盖默认的历史记录导航滚动恢复。以下示例将水平和垂直滚动位置重置为页面的左上角:

document.addEventListener('astro:after-swap',
() => window.scrollTo({ left: 0, top: 0, behavior: 'instant' }))

在页面导航结束时触发的事件,在新页面对用户可见且阻塞样式和脚本加载完成后。你可以在 document 上监听此事件。

<ViewTransitions /> 组件在预渲染页面的初始页面导航以及任何后续导航(向前或向后)时都会触发此事件。

你可以使用此事件在每次页面导航时运行代码,或者只运行一次:

<script>
document.addEventListener('astro:page-load', () => {
// This only runs once.
setupStuff();
}, { once: true });
</script>

启用客户端路由和动画页面过渡都会面临着无障碍挑战,而 Astro 旨在使启用了视图过渡动画的网站尽可能地具备默认的无障碍特性。

添加于: astro@3.2.0

<ViewTransitions /> 组件包含一个用于在客户端路由间进行页面导航的路由读出器。你无需配置或操作即可启用此功能。

辅助技术通过在导航后读出新页面标题来告知访客页面已被更改。在使用传统的全页面浏览器刷新服务端路由时,则会默认在新页面加载后执行此操作。而在客户端路由中,则由 <ViewTransitions /> 组件执行此操作。

为在客户端路由中添加路由读出,该组件会向新页面添加一个带有 aria-live 属性设置为 assertive 的元素。这告诉 AT(assistive technology,辅助技术)立即读出。该组件还按照优先顺序检查以下内容来确定读出的文本:

  • 如果存在 <title> 标签,则使用该标签。
  • 使用第一个找到的 <h1> 标签。
  • 使用页面的 pathname

我们强烈建议你始终在每个页面中包含 <title> 标签以更好地支持无障碍。

Astro 的 <ViewTransitions /> 组件包含一个 CSS 媒体查询,当检测到 prefer-reduced-motion 设置时,禁用所有视图过渡动画,包括回退动画。相反,浏览器将不使用动画,简单直接的切换 DOM 元素。

贡献

你有什么想法?

社区