当两个不同的文档之间发生视图过渡时,称为跨文档视图过渡。多页应用 (MPA) 通常就是这种情况。从 Chrome 126 开始,Chrome 支持跨文档视图过渡。
跨文档视图过渡所依赖的构建块和原则与同文档视图过渡完全相同,这是有意为之:
- 浏览器会拍摄新旧页面上具有唯一
view-transition-name的元素的快照。 - 在渲染被抑制时,DOM 会更新。
- 最后,过渡效果由 CSS 动画提供支持。
与同文档视图过渡相比,跨文档视图过渡的不同之处在于,您无需调用 document.startViewTransition 即可开始视图过渡。相反,跨文档视图过渡的触发因素是从一个网页到另一个网页的同源导航,这种操作通常由网站用户点击链接来执行。
换句话说,没有可调用的 API 来启动两个文档之间的视图过渡。不过,需要满足以下两个条件:
- 两个文档都需要位于同一来源。
- 两个网页都需要选择启用,才能允许视图过渡。
本文档后面部分将介绍这两种情况。
跨文档视图过渡仅限于同源导航
跨文档视图过渡仅限于同源导航。如果参与导航的两个网页的来源相同,则该导航被视为同源导航。
网页的来源是所用架构、主机名和端口的组合,如 web.dev 上所述。
例如,当从 developer.chrome.com 导航到 developer.chrome.com/blog 时,您可以进行跨文档视图过渡,因为它们是同源的。
从 developer.chrome.com 导航到 www.chrome.com 时,您无法进行该过渡,因为它们是跨源和同站点的。
跨文档视图过渡是选择启用的功能
如需在两个文档之间实现跨文档视图过渡,参与过渡的两个页面都需要选择启用此功能。这是通过 CSS 中的 @view-transition at 规则实现的。
在 @view-transition at-rule 中,将 navigation 描述符设置为 auto,以针对跨文档、同源导航启用视图转换。
@view-transition {
navigation: auto;
}
通过将 navigation 描述符设置为 auto,您可以选择允许以下 NavigationType 发生视图转换:
traversepush或replace,前提是激活不是由用户通过浏览器界面机制发起的。
从 auto 中排除的导航包括:使用网址地址栏或点击书签进行的导航,以及任何形式的用户或脚本发起的重新加载。
如果导航时间过长(在 Chrome 中超过 4 秒),则会跳过视图过渡,并显示 TimeoutError DOMException。
跨文档视图过渡演示
请查看以下演示,该演示使用视图转换创建了堆栈导航器演示。此处没有对 document.startViewTransition() 的调用,视图转换是通过从一个页面导航到另一个页面来触发的。
自定义跨文档视图过渡
如需自定义跨文档视图过渡效果,您可以使用一些 Web 平台功能。
这些功能本身不属于视图过渡 API 规范,但旨在与该规范搭配使用。
pageswap和pagereveal活动
为了让您能够自定义跨文档视图过渡效果,HTML 规范新增了两个可供您使用的事件:pageswap 和 pagereveal。
无论是否即将发生视图过渡,这两个事件都会在每次同源跨文档导航时触发。如果两个页面之间即将发生视图过渡,您可以使用这些事件的 viewTransition 属性访问 ViewTransition 对象。
pageswap事件在网页的最后一个帧渲染之前触发。您可以使用此功能在拍摄旧快照之前对即将退出的网页进行一些临时更改。pagereveal事件会在网页初始化或重新激活后,但在首次渲染机会之前触发。借助此功能,您可以在拍摄新快照之前自定义新页面。
例如,您可以利用这些事件通过写入和读取 sessionStorage 中的数据来快速设置或更改某些 view-transition-name 值,或者将数据从一个文档传递到另一个文档,从而在视图过渡实际运行之前自定义视图过渡。
let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
if (event.target.tagName.toLowerCase() === 'a') return;
lastClickX = event.clientX;
lastClickY = event.clientY;
});
// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
if (event.viewTransition && lastClick) {
sessionStorage.setItem('lastClickX', lastClickX);
sessionStorage.setItem('lastClickY', lastClickY);
}
});
// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
if (event.viewTransition) {
lastClickX = sessionStorage.getItem('lastClickX');
lastClickY = sessionStorage.getItem('lastClickY');
}
});
您可以选择在两个活动中都跳过过渡。
window.addEventListener("pagereveal", async (e) => {
if (e.viewTransition) {
if (goodReasonToSkipTheViewTransition()) {
e.viewTransition.skipTransition();
}
}
}
pageswap 和 pagereveal 中的 ViewTransition 对象是两个不同的对象。它们处理各种 promise 的方式也不同:
pageswap:隐藏文档后,系统会跳过旧的ViewTransition对象。发生这种情况时,viewTransition.ready会拒绝,而viewTransition.finished会解析。pagereveal:此时updateCallBackpromise 已解析。您可以使用viewTransition.ready和viewTransition.finishedpromise。
导航激活信息
在 pageswap 和 pagereveal 事件中,您还可以根据旧网页和新网页的网址采取相应措施。
例如,在 MPA 堆栈导航器中,要使用的动画类型取决于导航路径:
- 从概览页面导航到详情页面时,新内容需要从右侧滑动到左侧。
- 从详情页面导航到概览页面时,旧内容需要从左向右滑出。
为此,您需要了解导航的相关信息,对于 pageswap,这些信息与即将发生的导航有关;对于 pagereveal,这些信息与刚刚发生的导航有关。
为此,浏览器现在可以公开 NavigationActivation 对象,其中包含有关同源导航的信息。此对象会公开所用的导航类型、当前目的地和最终目的地历史记录条目(如 navigation.entries() 中所示,来自 Navigation API)。
在已激活的页面上,您可以通过 navigation.activation 访问此对象。在 pageswap 事件中,您可以通过 e.activation 访问此信息。
请查看此个人资料演示,其中使用 pageswap 和 pagereveal 事件中的 NavigationActivation 信息来设置需要参与视图过渡的元素的 view-transition-name 值。
这样一来,您便无需预先为列表中的每个项目都添加 view-transition-name 装饰器。相反,这种情况会使用 JavaScript 及时发生,仅在需要它的元素上发生。
代码如下所示:
// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
if (e.viewTransition) {
const targetUrl = new URL(e.activation.entry.url);
// Navigating to a profile page
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the clicked row
document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove view-transition-names after snapshots have been taken
// (this to deal with BFCache)
await e.viewTransition.finished;
document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
}
}
});
// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
if (e.viewTransition) {
const fromURL = new URL(navigation.activation.from.url);
const currentURL = new URL(navigation.activation.entry.url);
// Navigating from a profile page back to the homepage
if (isProfilePage(fromURL) && isHomePage(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elements in the list
document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove names after snapshots have been taken
// so that we're ready for the next navigation
await e.viewTransition.ready;
document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
}
}
});
该代码还会在视图过渡运行后移除 view-transition-name 值,从而自行清理。这样,页面便可为后续导航做好准备,同时还能处理历史记录的遍历。
为了帮助实现这一点,请使用此实用程序函数来临时设置 view-transition-name。
const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
for (const [$el, name] of entries) {
$el.style.viewTransitionName = name;
}
await vtPromise;
for (const [$el, name] of entries) {
$el.style.viewTransitionName = '';
}
}
现在,前面的代码可以简化为如下形式:
// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
if (e.viewTransition) {
const targetUrl = new URL(e.activation.entry.url);
// Navigating to a profile page
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the clicked row
// Clean up after the page got replaced
setTemporaryViewTransitionNames([
[document.querySelector(`#${profile} span`), 'name'],
[document.querySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.finished);
}
}
});
// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
if (e.viewTransition) {
const fromURL = new URL(navigation.activation.from.url);
const currentURL = new URL(navigation.activation.entry.url);
// Navigating from a profile page back to the homepage
if (isProfilePage(fromURL) && isHomePage(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elements in the list
// Clean up after the snapshots have been taken
setTemporaryViewTransitionNames([
[document.querySelector(`#${profile} span`), 'name'],
[document.querySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.ready);
}
}
});
等待内容加载(阻塞渲染)
Browser Support
在某些情况下,您可能希望延迟网页的首次渲染,直到新 DOM 中出现某个特定元素。这样可以避免闪烁,并确保动画所要达到的状态是稳定的。
在 <head> 中,使用以下元标记定义在网页首次呈现之前需要存在的一个或多个元素 ID。
<link rel="expect" blocking="render" href="#section1">
此元标记表示元素应存在于 DOM 中,而不是表示应加载内容。例如,对于图片,DOM 树中只要存在具有指定 id 的 <img> 标记,条件就会评估为 true。图片本身可能仍在加载。
在完全依赖渲染阻塞之前,请注意增量渲染是 Web 的一个基本方面,因此在选择阻塞渲染时要谨慎。需要根据具体情况评估阻塞渲染的影响。默认情况下,请避免使用 blocking=render,除非您能通过衡量对 Core Web Vitals 的影响,主动衡量并评估它对用户的影响。
跨文档视图过渡中的视图过渡类型
跨文档视图过渡还支持视图过渡类型,以自定义动画和捕获的元素。
例如,在分页中前往下一页或上一页时,您可能希望根据您是要前往序列中编号较高的页面还是编号较低的页面来使用不同的动画。
如需预先设置这些类型,请在 @view-transition at 规则中添加相应类型:
@view-transition {
navigation: auto;
types: slide, forwards;
}
如需动态设置类型,请使用 pageswap 和 pagereveal 事件来操纵 e.viewTransition.types 的值。
window.addEventListener("pagereveal", async (e) => {
if (e.viewTransition) {
const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
e.viewTransition.types.add(transitionType);
}
});
类型不会自动从旧网页上的 ViewTransition 对象转移到新网页的 ViewTransition 对象。您需要确定至少在新网页上使用的类型,以便动画按预期运行。
若要响应这些类型,请使用 :active-view-transition-type() 伪类选择器,方式与使用同文档视图过渡相同
/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
:root {
view-transition-name: none;
}
article {
view-transition-name: content;
}
.pagination {
view-transition-name: pagination;
}
}
/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-left;
}
&::view-transition-new(content) {
animation-name: slide-in-from-right;
}
}
/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-right;
}
&::view-transition-new(content) {
animation-name: slide-in-from-left;
}
}
/* Animation styles for reload type only */
html:active-view-transition-type(reload) {
&::view-transition-old(root) {
animation-name: fade-out, scale-down;
}
&::view-transition-new(root) {
animation-delay: 0.25s;
animation-name: fade-in, scale-up;
}
}
由于类型仅适用于有效的视图过渡,因此当视图过渡完成时,类型会自动清理。因此,类型可以很好地与 BFCache 等功能搭配使用。
演示
在下面的分页演示中,页面内容会根据您前往的页码向前或向后滑动。
要使用的过渡类型在 pagereveal 和 pageswap 事件中通过查看来源网址和目标网址来确定。
const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
const currentURL = new URL(fromNavigationEntry.url);
const destinationURL = new URL(toNavigationEntry.url);
const currentPathname = currentURL.pathname;
const destinationPathname = destinationURL.pathname;
if (currentPathname === destinationPathname) {
return "reload";
} else {
const currentPageIndex = extractPageIndexFromPath(currentPathname);
const destinationPageIndex = extractPageIndexFromPath(destinationPathname);
if (currentPageIndex > destinationPageIndex) {
return 'backwards';
}
if (currentPageIndex < destinationPageIndex) {
return 'forwards';
}
return 'unknown';
}
};
反馈
我们随时欢迎开发者的反馈。如需分享,请在 GitHub 上向 CSS 工作组提交问题,并附上建议和问题。在问题前添加 [css-view-transitions] 前缀。
如果您遇到 bug,请改为提交 Chromium bug。