KAIROS.WORKSPACE
技术宇宙 / ONLINE
返回文章列表
博客工程intermediate9 分钟

View Transitions 在多模板博客里的实战分层

用 Astro ClientRouter 做页面过渡时,如何按"全局底座 / 页面专属 / 组件内部"三层管理动画生命周期,避免监听器泄漏和动画叠加。

#Astro#动效#View Transitions#JavaScript
排版
字号
行宽
目录 · 8 节

以下内容为可公开验证的通用工程方法,示例均为“假设 / 演示”场景,不涉及真实生产数据。

一个容易被忽略的事实

Astro 的 <ClientRouter /> 开启后,页面切换不再触发完整的浏览器加载,而是走 View Transitions API(不支持的浏览器自动回退为 MPA 行为)。这带来一个经典问题:

同一个 <script> 不会因为“看起来重新渲染了”而重新执行。

  • 内联脚本(is:inline):随新页面 DOM 一起被换入,会再次执行。
  • 打包模块脚本(普通 <script>):ES Module 只执行一次,第二次进入页面只有事件不监听、DOM 是新的。

所以多模板博客的动效代码必须先回答一个问题:这段代码应该在哪一层运行、由谁清理?

三层分层模型

推荐把全站动效代码拆成三层,每层有明确的所有权:

归属 生命周期 典型内容
全局底座 基础布局 首次加载执行一次,跨页面存活 滚动揭示、涟漪反馈、命令面板
页面专属 单个页面 进入页面执行,离开时清理 星图物理模拟、文章专注模式
组件内部 单个组件 组件挂载后自管 计时器、Observer、浮层

全局底座:用“清理指针”覆盖旧实例

全局脚本必须能在 DOM 被替换后重建与当前 DOM 的联系。一个可靠的模式是把清理函数挂在 window 上,每次执行先调用旧的:

window.__dsScrollRevealCleanup?.();

const observer = new IntersectionObserver((entries) => {
  // ...
});
document.addEventListener('astro:before-swap', cleanup);
window.__dsScrollRevealCleanup = cleanup;

关键点有三个:

  1. 脚本开头先执行上一次留下的 cleanup,保证旧 Observer、旧计时器全部断开。
  2. astro:before-swap 是最后的清理窗口,此时旧 DOM 还在,移除文档级监听器最安全。
  3. 清理函数里既要 disconnect() Observer,也要 removeEventListener,缺一都会在长会话中累积。

页面专属:is:inline + 清理指针的组合

页面专属动效(比如力导向图)建议使用 is:inline 脚本:它随页面换入而重新执行,等于每次进入都得到全新实例。配合清理指针,可以覆盖掉上一次的循环:

window.__graphExplorerCleanup?.();
// ... 初始化画布、物理循环、监听器 ...
window.__graphExplorerCleanup = () => {
  window.cancelAnimationFrame(frame);
  observer.disconnect();
  document.removeEventListener('astro:before-swap', cleanup);
};
document.addEventListener('astro:before-swap', cleanup);

不这样做的话,一个正在 requestAnimationFrame 循环里跑的物理模拟,会在离开页面后继续烧 CPU——画面没了,计算还在。

组件内部:谁注册,谁注销

组件脚本最常见的泄漏是把匿名函数同时用作注册和注销:

// ❌ 这行什么都不做:箭头函数是新的引用
toggle.removeEventListener('click', () => setMode(false));

// ✅ 保存引用再注销
const handler = () => setMode(false);
toggle.addEventListener('click', handler);
// ...
toggle.removeEventListener('click', handler);

reduced-motion 不是开关,是分支

很多人把 prefers-reduced-motion 当成一个 CSS 媒体查询一刀切。对涉及 JS 驱动的动画,它是三条不同的代码路径:

  1. CSS 部分:用 token 化的 duration/easing,在媒体查询里归零。
  2. JS 循环部分:物理模拟、粒子、打字机这类逐帧逻辑,应该直接不启动 rAF,改为一次性计算终态(比如把力导向模拟同步跑 N 次迭代后画一帧)。
  3. 交互语义:拖拽、缩放、平移属于直接操作而非装饰动画,reduced-motion 用户仍然需要它们,只是不要“惯性滑动”式的持续运动。

第三点最容易被做错:把所有动画都关掉,等于把功能也关掉了。

自检清单

发布前可以按这个顺序过一遍:

  • 每个 requestAnimationFrame 循环都有停止条件(alpha 衰减、离开视口、页面隐藏)。
  • 每个全局脚本都有 window.__xxxCleanup 指针,并在开头调用。
  • 页面隐藏(document.hidden)时动画循环暂停。
  • astro:before-swap 里注销了所有文档级监听器。
  • Canvas 尺寸跟随容器(ResizeObserver 而不是只监听 window.resize)。
  • reduced-motion 下逐帧动画不启动,但拖拽/缩放/点击功能完整。

小结

View Transitions 时代的前端动效,核心工程问题不是“怎么让它动”,而是“它由谁拥有、什么时候停止”。三层分层 + 清理指针 + 三分支的 reduced-motion 处理,可以让全站动画在长会话里保持稳定,也让每个页面的专属效果都有明确的归属。

Conversation

评论与互动

正在加载评论…

提交后需审核,不会立即公开。

Keep exploring

继续探索