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

当两个不同文档之间发生视图转换时,这种转换称为跨文档视图转换。多页面应用 (MPA) 通常就属于这种情况。从 Chrome 126 开始,Chrome 支持跨文档视图转换。

浏览器支持

  • 126
  • 126
  • x
  • x

来源

跨文档视图转换依赖于与同一文档视图转换完全相同的构建块和原则,这是非常有意为之:

  1. 浏览器会截取新旧页面上具有唯一 view-transition-name 的元素快照。
  2. DOM 会在渲染被禁用时进行更新。
  3. 最后,过渡要借助 CSS 动画。

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

换句话说,没有为了在两个文档之间开始视图转换而调用的 API。不过,您需要满足以下两个条件:

  • 两个文档必须位于同一源。
  • 这两个页面都需要用户选择启用才能进行视图转换。

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


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

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

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

突出显示 scheme、主机名和端口的示例网址。它们结合在一起就形成了来源。
突出显示架构、主机名和端口的示例网址。它们组合在一起,就形成了来源。

例如,在从 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() 的调用,视图转换是通过从一个页面导航到另一个页面触发的。

堆栈导航器演示的录像。需要安装 Chrome 126 或更高版本。

自定义跨文档视图过渡

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

这些功能本身不是 View Transition API 规范的一部分,但可以与 View Transition API 规范结合使用。

pageswappagereveal 事件

浏览器支持

  • 124
  • 124
  • x
  • x

来源

为了让您能够自定义跨文档视图转换,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。

浏览器支持

  • 123
  • 123
  • x
  • x

来源

pageswappagereveal 事件中,您还可以根据新旧网页的网址执行操作。

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

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

为此,您需要有关即将发生的导航的信息(就 pageswap 而言,在 pagereveal 刚刚发生的情况下)。

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

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

请查看此配置文件演示,该演示使用 pageswappagereveal 事件中的 NavigationActivation 信息,在需要参与视图转换的元素上设置 view-transition-name 值。

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

配置文件演示的录像。需要安装 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);
    }
  }
});

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

浏览器支持

  • 124
  • 124
  • x
  • x

来源

在某些情况下,您可能希望等到某个元素出现在新 DOM 中之后,暂缓网页首次渲染。这样可以避免闪烁,并确保您为其设置动画效果的状态是稳定的。

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

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

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

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


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

跨文档视图过渡还支持视图过渡类型,以自定义动画和捕获哪些元素。

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

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

@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 等功能很好地搭配使用。

演示

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

分页演示 (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] 前缀。 如果您遇到 bug,请改为提交 Chromium bug