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 更新与视觉过渡。但它的目标更宏大:
- 拥抱 MPA: 正视 MPA 在 Web 生态中的重要地位,为其提供现代化的过渡体验。
- 声明式优先: 尽可能通过简单的 CSS(
@view-transition
规则)来启用跨文档转场,降低接入成本。 - 生命周期钩子: 在跨文档导航的关键节点(旧页面卸载前、新页面展现前)提供 JS 事件 (
pageswap
,pagereveal
),赋予开发者精细控制的能力。 - 能力增强: 不仅仅是解决跨文档问题,还基于实践反馈,加入了选择性转场、样式复用、自动命名、嵌套转场、分层捕获等一系列“武功秘籍”。
简而言之,Level 2 就是要在尊重并兼容传统 MPA 架构的前提下,将流畅转场的能力普及化、标准化,并让它变得更强大、更灵活。
二、入门:跨文档转场,只需“一句咒语”?
想让你的 MPA 页面跳转也动起来?Level 2 说,基础操作很简单:
核心开关:@view-transition
规则
你需要在跳转前和跳转后两个页面的 CSS 中,都加入这个新的 @
规则,并设置 navigation
描述符:
/* 在 page-a.html 和 page-b.html 的 CSS 里都要有 */
@view-transition {
navigation: auto; /* 关键先生!告诉浏览器,我想自动开启跨文档转场 */
}
只要加上这句,并且满足以下浏览器自动触发的条件:
- 同源(Same Origin):安全第一,必须的。
- 用户触发导航:点击链接、提交表单、浏览器前进/后退等。地址栏输入、书签等不算。
- 页面可见:导航期间页面得在前台。
- 无跨域重定向:中间不能有跨域跳转搅局。
- 双方同意:两个页面都得写上
navigation: auto;
。
那么,恭喜你!从 page-a.html
跳转到 page-b.html
时,默认的交叉淡入淡出 (cross-fade) 效果就会自动应用。是不是比 Level 1 的 document.startViewTransition
更“傻瓜化”?
当然,这只是起点。默认效果往往不能满足我们骚动的心,我们需要更精妙的控制。
三、进阶:掌控生命周期,定制跨文档之旅
Level 1 里我们有 startViewTransition
的回调和返回的 ViewTransition
对象(包含 ready
, finished
等 Promise)。在 Level 2 的跨文档场景下,流程变了,控制方式也随之升级:
跨文档生命周期 & JS 钩子:
用户操作 (Old Document): 点击链接/后退等。
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"); } });
- 你可以通过
捕获旧状态 & 卸载旧页面。
pagereveal
事件 (New Document -window
上监听): 新页面 DOM 加载完毕,首次渲染前触发。- 同样通过
event.viewTransition
(如果转场是从旧页面成功启动的,这里就会有值)来操作。 - 用途:
- 在新页面侧确认转场是否依然有效,或根据新页面的状态决定跳过。
- 可以在这里修改转场类型
event.viewTransition.types.add/remove/clear()
。 - 等待
event.viewTransition.ready
来执行需要新旧状态都捕获完成才能开始的 JS 动画(类似 Level 1)。 - 重要: Level 2 里,这个
event.viewTransition
的updateCallbackDone
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(...) });
- 同样通过
捕获新状态 & 执行动画 & 完成。
新状态何时稳定?靠“渲染阻塞”!
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 搬到了跨文档,还带来了许多激动人心的新功能:
选择性视图转场 (Selective View Transitions):
痛点: Level 1 里所有转场都一样,想根据不同交互(如导航 VS 卡片展开)应用不同动画比较麻烦。
Level 2 方案: 引入
types
的概念。- 设置类型:
- 通过 JS 在
pageswap
/pagereveal
里event.viewTransition.types.add('your-type')
。 - 或者直接在
@view-transition
规则里声明:@view-transition { navigation: auto; types: slide-nav card-expand; /* 声明默认类型 */ }
- 通过 JS 在
- 匹配类型: 使用新的 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 规则,为不同类型的转场定义不同的动画,逻辑分离。
样式复用 (
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!极大简化了对共享动画行为的元素的样式定义。
自动
view-transition-name
:- 痛点: 手动给列表项等大量元素起名字太烦。
- Level 2 方案:
view-transition-name: auto;
- 有
id
就用id
。 - 没
id
浏览器内部生成唯一标识。
- 有
- 跨文档注意!!!
auto
生成的、非id
的名字,在新旧文档间不会匹配!这意味着它们总是触发进入/退出动画,而不是平滑过渡。想让元素在新旧页面平滑连接,还是要确保它们有相同且稳定的view-transition-name
(用id
或手动指定相同 name)。
嵌套视图转场 (
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 的层级和效果(如剪裁、滤镜),实现更复杂的视觉效果。
分层捕获 (Layered Capture - 底层改进):
- 痛点: Level 1 的快照是扁平图片,
border
,background-gradient
,box-shadow
,filter
等效果无法独立动画,只能跟着图片一起变形或淡变,很生硬。 - Level 2 方案: 浏览器不再只拍一张扁平快照,而是捕获元素的多个 CSS 属性层(如背景、边框、阴影、滤镜、透明度、内边距等),并在动画时对这些属性本身进行插值。
- 好处: 动画效果大大丰富!边框可以平滑变色变形,渐变背景可以流畅过渡,阴影和滤镜也能动起来,视觉表现力提升一个档次!这使得转场感觉更“原生”,而不是简单的图片切换。
- 痛点: Level 1 的快照是扁平图片,
五、注意事项与未来展望
- 安全: 依然严格限制同源,并处理了跨域重定向问题。
- 兼容性: 划重点!Level 2 目前 (2025-4-7) 仍是 W3C 工作草案 (Working Draft),API 和行为可能变化,浏览器支持尚不完善或处于实验阶段。生产环境使用务必谨慎,关注 Can I use 和浏览器厂商动态。
- 性能: 复杂的嵌套、大量的独立元素、消耗大的动画依然需要关注性能测试和优化。
- 调试: 现代浏览器开发者工具正在逐步增强对 View Transitions(包括 Level 2 特性)的调试支持。
六、总结:MPA 的春天,体验的飞跃
CSS View Transitions Module Level 2 是对 Level 1 的一次意义重大的扩展和增强。它不仅将丝滑的转场体验带给了更广泛的 MPA 网站,还通过 types
、view-transition-class
、auto
name、view-transition-group
属性以及底层的分层捕获,极大地提升了转场的灵活性、开发效率和视觉表现力。
虽然还是草案,但它描绘的未来无疑是激动人心的。掌握了 View Transitions Level 1 和 Level 2,你就拥有了打造下一代 Web 流畅体验的利器。
希望这篇结合了 Level 1 回顾与 Level 2 深入的探索,能让你对 View Transitions 有一个更全面、更深入的认识。赶紧动手尝试(在支持的实验性浏览器中),感受这触手可及的未来吧!
参考资料: