View Transitions API (Level-2 cross-document)

上一篇文章咱们聊了 View Transitions API (Level-1 single-document) 如何优雅地解决了 SPA(单页应用)里那为了动画而扭曲 DOM、编写复杂 JS 的痛点。通过 document.startViewTransition、快照机制和神奇的伪元素树,它成功地将 DOM 状态更新视觉过渡动画 解耦,让开发者能轻松实现丝滑的同文档视图切换。

但是,Level 1 的能力仅限于“家里面”(同一个文档)。一旦涉及到“出门串门”(跨文档导航,比如从 a.html 跳到 b.html),那熟悉的白屏闪烁又回来了。MPA(多页应用)的用户体验难道就只能停留在“上古时代”吗?

W3C 的大佬们显然不满足于此。于是,CSS View Transitions Module Level 2 应运而生,它的核心使命,就是将 Level 1 的丝滑体验,延伸到传统的跨文档导航场景,并在此基础上增加更多强大的功能!

今天,咱们就接着上一篇的步伐,重点探索 Level 2 的世界,看看它是如何打通跨文档的“任督二脉”,以及它带来了哪些令人兴奋的新特性!

一、初心不改:Level 2 的核心目标与设计哲学

Level 2 继承并扩展了 Level 1 的核心哲学:解耦 DOM 更新与视觉过渡。但它的目标更宏大:

  1. 拥抱 MPA: 正视 MPA 在 Web 生态中的重要地位,为其提供现代化的过渡体验。
  2. 声明式优先: 尽可能通过简单的 CSS(@view-transition 规则)来启用跨文档转场,降低接入成本。
  3. 生命周期钩子: 在跨文档导航的关键节点(旧页面卸载前、新页面展现前)提供 JS 事件 (pageswap, pagereveal),赋予开发者精细控制的能力。
  4. 能力增强: 不仅仅是解决跨文档问题,还基于实践反馈,加入了选择性转场、样式复用、自动命名、嵌套转场、分层捕获等一系列“武功秘籍”。

简而言之,Level 2 就是要在尊重并兼容传统 MPA 架构的前提下,将流畅转场的能力普及化、标准化,并让它变得更强大、更灵活。

二、入门:跨文档转场,只需“一句咒语”?

想让你的 MPA 页面跳转也动起来?Level 2 说,基础操作很简单:

核心开关:@view-transition 规则

你需要在跳转前跳转后两个页面的 CSS 中,都加入这个新的 @ 规则,并设置 navigation 描述符:

/* 在 page-a.html 和 page-b.html 的 CSS 里都要有 */
@view-transition {
  navigation: auto; /* 关键先生!告诉浏览器,我想自动开启跨文档转场 */
}

只要加上这句,并且满足以下浏览器自动触发的条件

  1. 同源(Same Origin):安全第一,必须的。
  2. 用户触发导航:点击链接、提交表单、浏览器前进/后退等。地址栏输入、书签等不算。
  3. 页面可见:导航期间页面得在前台。
  4. 无跨域重定向:中间不能有跨域跳转搅局。
  5. 双方同意:两个页面都得写上 navigation: auto;

那么,恭喜你!从 page-a.html 跳转到 page-b.html 时,默认的交叉淡入淡出 (cross-fade) 效果就会自动应用。是不是比 Level 1 的 document.startViewTransition 更“傻瓜化”?

当然,这只是起点。默认效果往往不能满足我们骚动的心,我们需要更精妙的控制。

三、进阶:掌控生命周期,定制跨文档之旅

Level 1 里我们有 startViewTransition 的回调和返回的 ViewTransition 对象(包含 ready, finished 等 Promise)。在 Level 2 的跨文档场景下,流程变了,控制方式也随之升级:

跨文档生命周期 & JS 钩子:

  1. 用户操作 (Old Document): 点击链接/后退等。

  2. pageswap 事件 (Old Document - window 上监听): 这是旧页面被换掉前的最后机会

    • 你可以通过 event.viewTransition(如果转场条件满足,它就是个 ViewTransition 对象,否则为 null)来搞事情。
    • 用途:
      • 检查导航信息(event.activation),比如 navigationType 是不是 traverse (前进/后退)。
      • 动态给转场添加类型 event.viewTransition.types.add('my-type'),用于后续 CSS 选择性应用动画(见下文)。
      • 根据某些条件决定跳过转场 event.viewTransition.skipTransition()
      • event.viewTransition.finished Promise 中执行清理工作(注意,这可能在页面从 BFCache 恢复后才触发)。
    window.addEventListener("pageswap", (event) => {
      if (!event.viewTransition) return; // 不满足转场条件
    
      console.log("旧页面拜拜前,最后搞点事!");
      // 例如:给后退导航加个特殊类型
      if (event.activation.navigationType === "traverse") {
        event.viewTransition.types.add("going-back");
      }
    });
  3. 捕获旧状态 & 卸载旧页面。

  4. pagereveal 事件 (New Document - window 上监听): 新页面 DOM 加载完毕,首次渲染前触发。

    • 同样通过 event.viewTransition(如果转场是从旧页面成功启动的,这里就会有值)来操作。
    • 用途:
      • 在新页面侧确认转场是否依然有效,或根据新页面的状态决定跳过。
      • 可以在这里修改转场类型 event.viewTransition.types.add/remove/clear()
      • 等待 event.viewTransition.ready 来执行需要新旧状态都捕获完成才能开始的 JS 动画(类似 Level 1)。
      • 重要: Level 2 里,这个 event.viewTransitionupdateCallbackDone Promise 是一开始就 resolved 的(因为 DOM 更新是浏览器导航完成的,不是由你的回调触发)。
    window.addEventListener("pagereveal", async (event) => {
      if (!event.viewTransition) return; // 没有进行转场
    
      console.log("新页面来了,我瞅瞅!");
      // 例如:如果 URL 包含 #no-transition,就跳过
      if (location.hash.includes("no-transition")) {
        event.viewTransition.skipTransition();
        return;
      }
      // 可以等 ready 后用 JS 控制动画
      await event.viewTransition.ready;
      console.log("新旧状态都好了,准备浪起来!");
      // document.documentElement.animate(...)
    });
  5. 捕获新状态 & 执行动画 & 完成。

新状态何时稳定?靠“渲染阻塞”!

Level 2 没有 updateCallback Promise 了,浏览器怎么知道新页面何时“准备就绪”可以拍新照片了?答案是渲染阻塞机制 (Render-blocking mechanism)

开发者可以通过给 <link rel="stylesheet">, <script>, 甚至新增的 <link rel="expect" href="#element-id"> (等待特定元素出现) 添加 blocking="render" 属性,来告诉浏览器:“等这些关键资源加载/执行/元素就位后,再算我新页面稳定了,才能拍快照、启动动画!”

<head>
  <!-- 样式必须先应用 -->
  <link rel="stylesheet" href="style.css" />
  <!-- 默认就是 render-blocking -->
  <!-- 这个 JS 可能调整布局,等它执行完 -->
  <script src="layout-fix.js" blocking="render" async></script>
  <!-- 等主要内容区域加载并解析出来 -->
  <link rel="expect" href="#main-content" blocking="render" />
</head>

注意: 过度使用 blocking="render" 会让旧页面卡住太久,体验反而下降。要确保阻塞的资源能快速加载。

四、精通:Level 2 的独门绝技,让转场更溜!

Level 2 不仅仅是把 Level 1 搬到了跨文档,还带来了许多激动人心的新功能:

  1. 选择性视图转场 (Selective View Transitions):

    • 痛点: Level 1 里所有转场都一样,想根据不同交互(如导航 VS 卡片展开)应用不同动画比较麻烦。

    • Level 2 方案: 引入 types 的概念。

      • 设置类型:
        • 通过 JS 在 pageswap / pagerevealevent.viewTransition.types.add('your-type')
        • 或者直接在 @view-transition 规则里声明:
          @view-transition {
            navigation: auto;
            types: slide-nav card-expand; /* 声明默认类型 */
          }
      • 匹配类型: 使用新的 CSS 伪类:
        • :active-view-transition: 匹配有任何转场活动时的 <html>
        • :active-view-transition-type(type1, type2...): 匹配活动转场的 types 包含括号里至少一个类型时的 <html>
    • 示例:

      /* 默认淡入淡出 */
      ::view-transition-old(root) {
        animation: fade-out 0.3s;
      }
      ::view-transition-new(root) {
        animation: fade-in 0.3s;
      }
      
      /* 如果是导航滑动类型 */
      :root:active-view-transition-type(slide-nav) ::view-transition-old(root) {
        animation-name: slide-left-out;
      }
      :root:active-view-transition-type(slide-nav) ::view-transition-new(root) {
        animation-name: slide-right-in;
      }
    • 好处: 可以用清晰的 CSS 规则,为不同类型的转场定义不同的动画,逻辑分离。

  2. 样式复用 (view-transition-class):

    • 痛点: 很多元素 view-transition-name 不同,但想用同一套动画,写一堆 ::view-transition-group(name1), ::view-transition-group(name2)... 太累。

    • Level 2 方案: view-transition-class CSS 属性 + 类选择器语法。

      /* 给所有卡片加上 class */
      .card {
        view-transition-class: my-card;
        /* name 还是需要的,比如用 auto */
        view-transition-name: auto;
      }
      
      /* 用类选择器选中所有这些卡片的 group */
      ::view-transition-group(*.my-card) {
        /* 注意这个 *.classname 语法 */
        animation-timing-function: ease-in-out;
      }
    • 好处: DRY!极大简化了对共享动画行为的元素的样式定义。

  3. 自动 view-transition-name:

    • 痛点: 手动给列表项等大量元素起名字太烦。
    • Level 2 方案: view-transition-name: auto;
      • id 就用 id
      • id 浏览器内部生成唯一标识。
    • 跨文档注意!!! auto 生成的、非 id 的名字,在新旧文档间不会匹配!这意味着它们总是触发进入/退出动画,而不是平滑过渡。想让元素在新旧页面平滑连接,还是要确保它们有相同且稳定view-transition-name(用 id 或手动指定相同 name)。
  4. 嵌套视图转场 (view-transition-group CSS 属性):

    • 痛点: Level 1 的扁平伪元素树无法处理父元素的 clip-path, overflow: hidden, filter, opacity 或复杂的 3D 变换,导致动画效果失真。

    • Level 2 方案: view-transition-group CSS 属性,允许你指定一个父级 view-transition-name (或 nearest, contain),从而构建嵌套的伪元素树。

      .container {
        view-transition-name: container;
        clip-path: circle(50%);
      }
      .content-inside {
        view-transition-name: content;
        /* 让 content 的 group 成为 container group 的子元素 */
        view-transition-group: container;
      }
      
      /* 现在可以给 container group 加 clip-path 动画 */
      ::view-transition-group(container) {
        animation: clip-reveal 0.5s;
      }
    • 好处: 动画能更好地反映 DOM 的层级和效果(如剪裁、滤镜),实现更复杂的视觉效果。

  5. 分层捕获 (Layered Capture - 底层改进):

    • 痛点: Level 1 的快照是扁平图片,border, background-gradient, box-shadow, filter 等效果无法独立动画,只能跟着图片一起变形或淡变,很生硬。
    • Level 2 方案: 浏览器不再只拍一张扁平快照,而是捕获元素的多个 CSS 属性层(如背景、边框、阴影、滤镜、透明度、内边距等),并在动画时对这些属性本身进行插值。
    • 好处: 动画效果大大丰富!边框可以平滑变色变形,渐变背景可以流畅过渡,阴影和滤镜也能动起来,视觉表现力提升一个档次!这使得转场感觉更“原生”,而不是简单的图片切换。

五、注意事项与未来展望

六、总结:MPA 的春天,体验的飞跃

CSS View Transitions Module Level 2 是对 Level 1 的一次意义重大的扩展和增强。它不仅将丝滑的转场体验带给了更广泛的 MPA 网站,还通过 typesview-transition-classauto name、view-transition-group 属性以及底层的分层捕获,极大地提升了转场的灵活性、开发效率和视觉表现力。

虽然还是草案,但它描绘的未来无疑是激动人心的。掌握了 View Transitions Level 1 和 Level 2,你就拥有了打造下一代 Web 流畅体验的利器。

希望这篇结合了 Level 1 回顾与 Level 2 深入的探索,能让你对 View Transitions 有一个更全面、更深入的认识。赶紧动手尝试(在支持的实验性浏览器中),感受这触手可及的未来吧!

参考资料: