写了八年 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。

落地方案(直接抄)

  1. 双方页面都加 @view-transition { navigation: auto; }
  2. 列表页 pageswap:根据 target URL 判断方向,设 view-transition-name
  3. 详情页 pagereveal:直接设 view-transition-name,页面加载即触发
  4. 动画:CSS ::view-transition-* 伪元素覆盖默认过渡
  5. 清除:.ready 后全部设回 none,防止 BFCache 冲突

评论区

0 条评论

登录后可评论。

阿柯·前端架构 1037 阅读