Navigation API 的 intercept() 仅阻断跳转,不提供动画;startViewTransition() 必须在 DOM 同步更新时调用,若在 intercept 回调中异步执行(如 await fetch 后调用),快照将捕获旧状态导致动画失效或报错 InvalidStateError。
Navigation API 的本身不提供过渡动画能力,它只负责阻断默认跳转;自定义过渡必须由你手动控制 DOM 更新 + 触发 View Transition,且两者必须严格配合,否则动画会失效或卡顿。
为什么直接在 intercept 回调里 startViewTransition 会失败
常见错误现象:
后调用
,但页面“闪一下”就完成,没有过渡动画;或者报错
。
根本原因:View Transition 快照是在
调用**瞬间**拍的,而
回调里若包含异步操作(比如
、
),DOM 更新会延后,快照捕获的是旧状态甚至空容器。
浏览器不会等你 fetch 完再拍快照——它只认回调是否同步执行
回调必须立即返回,不能
,否则导航会被挂起,用户点击无响应
View Transition 要求“旧元素还在、新元素已挂载、二者有匹配的
”,缺一不可
正确链路:fetch → 挂载新 DOM → startViewTransition → updateHistory
必须把数据获取和 DOM 渲染拆开,确保
的回调内只做**同步、可见的 DOM 替换**。
立即学习
“
前端免费学习笔记(深入)
”;
示例逻辑(简化版):
使用HTML,CSS,JavaScript开发Android应用程序 英文文字pdf版附源文件
如果你了解HTML,CSS和JavaScript,您已经拥有所需的工具开发Android应用程序。本动手本书展示了如何使用这些开源web标准设计和建造,可适应任何Android设备的应用程序 - 无需使用Java。您将学习如何创建一个在您选择的平台的Android友好的网络应用程序,然后转换与自由PhoneGap框架到一个原生的Android应用程序。了解为什么设备无关的移动应用是未来的潮流,并开始构建应用程序,提供更
下载
所有
和解析必须在
回调外完成
必须已插入 DOM(哪怕暂时
),否则快照捕获不到
是关键:它保证旧节点未销毁前,新节点已存在,
才能配对
view-transition-name 配对失败的三个高发场景
90% 的 View Transition 不生效,问题不在 JS,而在 HTML/CSS 层面的
。
旧
和新
大小写不一致 → 不匹配
新内容里用了
,但
值变了 → 名称不恒定
旧元素被
或
隐藏,且样式在快照前已生效 → 浏览器拍到的是“不可见”,快照为空
验证方法:打开 Chrome DevTools → Layers 面板 → 看 transition 元素是否生成了独立合成层;若没有,加
强制提升。
兼容性兜底必须手动监听 a 标签
在 Firefox、Safari 和多数 WebView 中不可用,仅 Chrome/Edge 102+ 支持。检测失败后,不能只退回到
,因为
拿不到目标 URL 的结构化信息,也无法拦截
点击。
兜底方案要补两件事:
给所有内部链接加
,并监听
用
手动解析目标地址,模拟
行为
View Transition 兜底只能放弃,改用 CSS
+
/
做简易淡入
真正难的不是写 intercept,而是让整个导航链路——从点击、拦截、数据加载、DOM 替换、动画触发、history 更新——全部在毫秒级时间窗口内完成且不破坏语义。稍有延迟,用户就会感知为“卡住”或“闪退”。
intercept()event.intercept()document.startViewTransition()InvalidStateError: No view transition is activestartViewTransitionintercept()await fetch()router.resolve()intercept()awaitview-transition-namestartViewTransitionnavigator.navigation.addEventListener('navigate', async (event) => {
if (!event.canIntercept) return;
event.intercept(async ({ signal }) => {
// ✅ 第一步:先取数据(在 intercept 外或顶部 await)
const res = await fetch(event.destination.url, { signal });
const html = await res.text();
// ✅ 第二步:提前创建并挂载新容器(确保它在 document 中)
const newArticle = document.createElement('article');
newArticle.innerHTML = html;
document.body.appendChild(newArticle); // 必须 append,不能只 createElement
// ✅ 第三步:同步执行 transition —— 此时新旧元素都存在且可匹配
document.startViewTransition(() => {
// 移除旧内容,插入新内容(同步!)
document.querySelector('main').replaceWith(newArticle);
});
// ✅ 第四步:手动更新 history(intercept 不自动 push)
history.pushState({}, '', event.destination.url);
});
});fetchstartViewTransitionnewArticlevisibility: hiddenreplaceWithview-transition-nameview-transition-nameview-transition-name: title-${id}iddisplay: noneopacity: 0will-change: transformnavigator.navigationhistory.pushStatepopstatedata-nav="true"document.addEventListener('click', e => { if (e.target.matches('a[data-nav]')) { ... } })new URL(e.target.href)event.destination.urltransitionopacitytransform