适用于多页面应用的跨文档视图过渡

两个不同文档之间的视图转换称为“跨文档视图转换”。多页应用 (MPA) 通常就属于这种情况。Chrome 126 及更高版本支持跨文档视图转换。

浏览器支持

  • Chrome:126。 <ph type="x-smartling-placeholder">
  • 边缘:126。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:不支持。 <ph type="x-smartling-placeholder">

跨文档视图转换依赖于与同文档视图转换相同的构建块和原则,这是意图非常明确的:

  1. 浏览器会截取新旧页面上具有唯一 view-transition-name 的元素快照。
  2. DOM 会在渲染被抑制时更新。
  3. 最后,转场效果由 CSS 动画提供支持。

与同文档视图转换相比,不同之处在于使用跨文档视图转换时,您无需调用 document.startViewTransition 来开始视图转换。而触发跨文档视图转换的触发器是从一个网页到另一个网页的同源导航,该操作通常是由您网站的用户点击链接执行的。

换言之,无需调用任何 API 即可在两个文档之间开始视图过渡。不过,您需要满足以下两个条件:

  • 两个文档必须位于同一源。
  • 这两个页面都需要选择启用,才能允许视图过渡。

本文档后面会介绍这两种情况。


跨文档视图转换仅限于同源导航

跨文档视图转换仅限于同源导航。如果两个参与网页的来源相同,则导航被视为同源。

网页的来源是所用架构、主机名和端口的组合,详情见 web.dev

<ph type="x-smartling-placeholder">
</ph> 示例网址,其中突出显示了架构、主机名和端口。它们组合在一起就构成了原点。
一个示例网址,其中突出显示了方案、主机名和端口。它们组合在一起就构成了原点。

例如,从 developer.chrome.com 导航到 developer.chrome.com/blog 时,您可以进行跨文档视图转场,因为这些内容同源。 从developer.chrome.com导航到www.chrome.com时无法进行这种转换,因为这些是跨源和同网站。


可选择启用跨文档视图转换

若要在两个文档之间实现跨文档视图转换,两个文档所在的页面都需要选择允许转换。可通过 CSS 中的 @view-transition at-rule 来实现。

@view-transition at-rule 中,将 navigation 描述符设置为 auto,以便为同源的跨文档导航启用视图转换。

@view-transition {
  navigation: auto;
}

navigation 描述符设置为 auto,即表示您选择允许以下 NavigationType 发生视图转换:

  • traverse
  • pushreplace(如果激活操作不是用户通过浏览器界面机制发起的)。

auto 中排除的导航包括(例如,使用网址地址栏或点击书签)以及任何形式的用户或脚本发起的重新加载。

如果导航用时过长(在 Chrome 中超过 4 秒),系统会跳过视图转换并显示 TimeoutError DOMException

跨文档视图转换演示

请查看以下演示,该演示使用视图转换来创建堆栈导航器演示。这里没有对 document.startViewTransition() 的调用,视图转换是通过从一个页面转到另一个页面触发的。

<ph type="x-smartling-placeholder">
</ph>
Stack Navigator 演示的录制。需要使用 Chrome 126 或更高版本。

自定义跨文档视图转换

如需自定义跨文档视图转换,您可以使用一些网络平台功能。

这些功能并非 View Transition API 规范本身的一部分,但旨在与其结合使用。

pageswappagereveal事件

浏览器支持

  • Chrome:124。 <ph type="x-smartling-placeholder">
  • 边缘:124。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:不支持。 <ph type="x-smartling-placeholder">

来源

为了让您能够自定义跨文档视图转换,HTML 规范添加了两个可供您使用的新事件:pageswappagereveal

每次同源跨文档导航都会触发这两个事件,无论是否即将发生视图转换。如果视图过渡即将在两个页面之间发生,您可以使用这些事件的 viewTransition 属性访问 ViewTransition 对象。

  • pageswap 事件在网页的最后一帧呈现之前触发。您可以使用此功能在传出页面(在获取旧快照之前)进行最后一刻的更改。
  • pagereveal 事件会在网页初始化或重新启用后但第一次呈现机会之前触发。借助此功能,您可以在截取新快照之前自定义新页面。

例如,您可以使用这些事件快速设置或更改某些 view-transition-name 值,或者通过从 sessionStorage 写入和读取数据,将数据从一个文档传递到另一个文档,从而在实际运行之前自定义视图过渡

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();
    }
  }
}

pageswappagereveal 中的 ViewTransition 对象是两个不同的对象。此外,它们会以不同方式处理各种 promise

  • pageswap:文档隐藏后,系统会跳过旧的 ViewTransition 对象。发生这种情况时,viewTransition.ready 会拒绝,viewTransition.finished 会进行解析。
  • pagereveal:此时 updateCallBack promise 已解析。您可以使用 viewTransition.readyviewTransition.finished promise。

浏览器支持

  • Chrome:123。 <ph type="x-smartling-placeholder">
  • 边缘:123。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:不支持。 <ph type="x-smartling-placeholder">

来源

pageswappagereveal 事件中,您还可以根据新旧网页的网址采取措施。

例如,在 MPA 堆栈导航器中,要使用的动画类型取决于导航路径:

  • 从概览页面导航到详情页面时,新内容需要从右向左滑入。
  • 从详情页面导航到概览页面时,旧内容需要从左向右滑出。

为此,您需要有关即将发生的导航或 pagereveal 刚刚发生的导航的信息。pageswap

为此,浏览器现在可以公开 NavigationActivation 对象,用于存储同源导航的相关信息。此对象公开了所使用的导航类型、当前和最终的目的地历史记录条目,如 Navigation API 的 navigation.entries() 中所示。

在激活的页面上,您可以通过 navigation.activation 访问此对象。在 pageswap 事件中,您可以通过 e.activation 访问此文件。

请参阅此配置文件演示,该演示使用 pageswappagereveal 事件中的 NavigationActivation 信息为需要参与视图过渡的元素设置 view-transition-name 值。

这样,您就不必预先使用 view-transition-name 装饰列表中的每一项。相反,它使用 JavaScript 即时执行,并且仅在需要它的元素上执行。

<ph type="x-smartling-placeholder">
</ph>
Profiles 演示录像。需要使用 Chrome 126 或更高版本。

代码如下所示:

// 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);
    }
  }
});

在阻塞渲染的情况下等待内容加载

浏览器支持

  • Chrome:124。 <ph type="x-smartling-placeholder">
  • 边缘:124。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:不支持。 <ph type="x-smartling-placeholder">

在某些情况下,您可能希望等到新 DOM 中出现特定元素后,暂缓网页的首次呈现。这样可以避免闪烁,并确保要添加动画效果的状态保持稳定。

<head> 中,使用以下元标记定义一个或多个在网页首次呈现之前需要存在的元素 ID。

<link rel="expect" blocking="render" href="#section1">

此元标记表示,相应元素应存在于 DOM 中,而不是要加载内容。例如,对于图片,DOM 树中仅存在具有指定 id<img> 标记就足以让条件求值为 true。图片本身可能仍处于加载状态。

在全面处理阻塞渲染之前,请注意增量渲染是 Web 的一个基本方面,因此在选择阻塞渲染时要谨慎。您需要根据具体情况评估阻塞渲染的影响。除非您可以主动衡量和评估该功能对用户核心网页指标的影响,否则请默认避免使用blocking=render


查看跨文档视图转换中的转换类型

跨文档视图过渡还支持视图过渡类型,以自定义动画以及要拍摄哪些元素。

例如,在分页中前往下一页或上一页时,您可能想要使用不同的动画,具体取决于您是从序列中转到更高页面还是更低页面。

如需预先设置这些类型,请在 @view-transition at-rule 中添加类型:

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

如需即时设置类型,请使用 pageswappagereveal 事件来操控 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;
  }
}

由于类型仅适用于 Active View 转换,因此当视图转换完成时,系统会自动清理类型。因此,类型可与 BFCache 等功能完美配合。

演示

在下面的分页演示中,网页内容会根据您要前往的页码向前或向后滑动。

<ph type="x-smartling-placeholder">
</ph>
录制分页演示 (MPA)。根据您要访问的页面,它会使用不同的转场效果。

要使用的转场类型通过查看来回网址和离开网址,在 pagerevealpageswap 事件中确定。

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]。 如果您遇到错误,请改为提交 Chromium 错误