写了八年 MPA,今天终于把页面跳转的过渡动画做完整了——pageswap + pagereveal 把这件事彻底原生化了
用户在列表页点了一个商品,整页闪一下,新页面从头出现——这种体验我忍了八年。SPA 靠路由劫持能做过渡动画,但你要的是原生浏览器导航、前进后退、滚动位置恢复,放弃这些换流畅动画,值吗?
现在不用选了。Chrome 126 起,View Transitions API 支持跨文档(MPA)过渡,pageswap 和 pagereveal 两个事件把整个链路串起来了。
先声明 opt-in
旧页面和新页面都要先声明 opt-in,一行 CSS:
/* 两个页面都要写 */
@view-transition {
navigation: auto;
}
同源限制:跨域导航不触发过渡。
pageswap:离开前给元素打标签
pageswap 在旧文档即将卸载时触发,拿到 ViewTransition 对象,给要参与过渡的元素动态设 view-transition-name:
window.addEventListener("pageswap", async (e) => {
if (!e.viewTransition) return;
const fromUrl = e.activation?.from ? new URL(e.activation.from.url) : null;
const toUrl = new URL(e.activation.entry.url);
// 列表页 → 详情页:给商品图和标题打标签
if (isListPage(fromUrl) && isDetailPage(toUrl)) {
const item = extractItemFromUrl(toUrl);
document.querySelector(`#${item} img`).style.viewTransitionName = "product-img";
document.querySelector(`#${item} h2`).style.viewTransitionName = "product-title";
// 快照拍完后清除,防止 BFCache 恢复时冲突
await e.viewTransition.finished;
document.querySelector(`#${item} img`).style.viewTransitionName = "none";
document.querySelector(`#${item} h2`).style.viewTransitionName = "none";
}
});
e.viewTransition 就是那个熟悉的 ViewTransition 实例,.ready / .finished 承诺和 SPA 里用法一样。e.activation 包含 from(来源历史条目)和 entry(目标历史条目),同源导航才有值,否则 null。
pagereveal:进入后补上目标页的标签
pagereveal 在新文档首次渲染时触发,在目标页给详情内容打标签:
window.addEventListener("pagereveal", async (e) => {
if (!e.viewTransition) return;
const fromUrl = e.activation?.from ? new URL(e.activation.from.url) : null;
const currentUrl = new URL(e.activation.entry.url);
// 列表页 → 详情页:在目标页给大图和标题打标签
if (isListPage(fromUrl) && isDetailPage(currentUrl)) {
document.querySelector("article .product-image").style.viewTransitionName = "product-img";
document.querySelector("article h1").style.viewTransitionName = "product-title";
// 等快照拍完即可清除
await e.viewTransition.ready;
document.querySelector("article .product-image").style.viewTransitionName = "none";
document.querySelector("article h1").style.viewTransitionName = "none";
}
});
注意:pageswap 在旧页面跑,给旧页元素打标签;pagereveal 在新页面跑,给新页元素打标签。两边打的 view-transition-name 要配对,名字相同浏览器才会把它们串成「元素从 A 位置飞到 B 位置」的动效。
为什么打完标签又要立刻清除?
BFCache(后退-forward 缓存)会把页面完整保存在内存里。如果标签没清除,用户点后退回到列表页,那些元素还带着 view-transition-name,再次导航时就会和下一张页面的标签冲突,ViewTransition.ready 被 reject,过渡直接跳过。
所以标准做法是:快照一拍完(.ready 或 .finished)立即设回 none,让页面回到干净状态。
自定义动画:你想怎么飞它就怎么飞
默认是 opacity 交叉淡入淡出。想自定义?CSS 搞定:
/* 旧页面元素飞出去:缩小 + 淡出 */
::view-transition-old(product-img) {
animation: zoom-out 300ms ease-out forwards;
}
/* 新页面元素飞进来:放大 + 淡入 */
::view-transition-new(product-img) {
animation: zoom-in 300ms ease-in forwards;
}
@keyframes zoom-out {
to { transform: scale(0.8); opacity: 0; }
}
@keyframes zoom-in {
from { transform: scale(1.1); opacity: 0; }
}
伪元素树结构和 SPA 过渡完全一样 ::view-transition-group(name) / ::view-transition-old(name) / ::view-transition-new(name),只是 MPA 下这个树只出现在目标文档里。
这个方案适合什么场景
新闻站点、博客、商品列表→详情、文档导航——凡是你有「列表跳详情」结构、又不想放弃原生浏览器导航的 MPA,都可以直接上。SPA 框架里做这个要配路由、要劫持 pushState,MPA 里只要两个事件加一行 CSS。
落地方案(直接抄)
- 双方页面都加 @view-transition { navigation: auto; }
- 列表页 pageswap:根据 target URL 判断方向,设 view-transition-name
- 详情页 pagereveal:直接设 view-transition-name,页面加载即触发
- 动画:CSS ::view-transition-* 伪元素覆盖默认过渡
- 清除:.ready 后全部设回 none,防止 BFCache 冲突
评论区
登录后可评论。