1795 字
9 分钟
一次 Swup 客户端导航的踩坑:从整页刷新到目录消失
非 AI 创作

总所不周知,我的博客自从上线了banner图功能之后就出现神必bug,具体表现为在文章界面点击导航栏按钮回到主页时会自动触发刷新

由于Fuwari这个博客主题使用了Swup这个东西,这是一个为服务端渲染(SSR)网站设计的页面过渡库,它能让你的网站在页面切换时不再“刷新闪白”,而是平滑过渡。所以按照正常逻辑,点击按钮返回到主页时应该是直接过渡的,所以很大概率是DS魔改的时候改出bug了

第一章:整页刷新#

症状:在文章页点顶部导航栏的「主页」返回时,页面会先平滑过渡一下,紧接着整个页面又重新加载了一遍(明显的白屏/重绘)。其他跳转(比如主页点进文章)反而正常。

DS给我的说法是由于Swup拦截失败导致直接触发访问 / 目录导致的,但经过DS的神秘修理问题仍然存在,于是我果断寻求免费打白工的hy3排查一下问题

第二章:hy3神力#

排查很简单——这种「Swup 过渡后又来一次硬刷新」的现象,八成是某段 JS 里写了 location.reload()。全局搜了一下,整个 src 里只有一处 window.location.reload(),在 src/components/widget/SideBar.astro 的脚本里:

let wasPostPage = isPostPath(window.location.pathname);
function updateSidebar() {
const sticky = document.getElementById('sidebar-sticky');
if (!sticky) return;
const nowIsPost = isPostPath(window.location.pathname);
if (nowIsPost) {
// 进入文章页:从 DOM 提取标题,手动重建目录
// ...
wasPostPage = true;
} else {
if (wasPostPage) {
window.location.reload(); // ← 罪魁祸首
return;
}
}
}
window.swup?.hooks?.on('page:view', updateSidebar);

逻辑很清楚:Swup 完成一次客户端导航后会触发 page:view 事件,这段钩子检查一下「现在是不是文章页」:

  • 主页 → 文章:nowIsPost=true,把 wasPostPage 置 true,不刷新;
  • 文章 → 文章:nowIsPost=true,不刷新;
  • 文章 → 主页/归档/关于nowIsPost=falsewasPostPage=true,命中 else,执行 reload()

所以它只在「从文章页离开」时触发——这也正好解释了为什么我最先注意到的是「返回主页会刷新」。

第一段修复:删掉那段脚本#

一开始我以为这段脚本完全是多余的。理由也很充分:

  • 侧边栏 SideBar 是渲染在 <main> 内部的,而 main 正是 Swup 的容器之一,每次导航 Swup 应该已经把侧栏整段替换成目标页面服务端渲染好的版本了;
  • @swup/astro 自带的 scripts 插件会自动重跑脚本、重新 hydrate 那些 client:only 的 Svelte 组件(侧栏里的 RouteSwitch / Categories / UmamiStats 都是),不需要硬刷新兜底。

于是我直接把整个 <script> 块删了。刷新问题确实消失了——然后新 bug 出现了。

第二段:目录(TOC)消失了#

症状:删掉脚本后,点进任何一篇文章,左边侧栏不再显示目录了,而是继续显示主页那一套 widgets(分类、统计等)。

这时候我才重新仔细看了布局文件 MainGridLayout.astro,发现一个之前被我忽略的事实:

**#sidebar-sticky 并不在 Swup 的容器列表里。**(DS干的好事)

Swup 当时只配置了这两个容器(astro.config.mjs):

swup({
containers: ["main", "#toc"],
// ...
})

也就是说,Swup 在客户端导航时只替换 main#toc 两块内容,侧边栏压根不参与替换。它永远停留在首次整页加载时的状态:

  • 首次从主页加载 → 侧栏是 widgets;
  • 之后无论你 Swup 导航到哪,侧栏都还是那套 widgets,不会变成文章的目录。

那原来的 updateSidebar 脚本到底是干嘛的?现在清楚了——它根本不是多余的,而是人工补齐了 Swup 没做好的事

  • 进文章页时,客户端从 DOM 现抠标题拼出目录;
  • 离开文章页时,因为 widgets 是 Svelte 岛屿不好重建,干脆用 reload() 让服务端重新渲染正确版本。

换句话说,那个烦人的刷新,是「侧栏不在 Swup 容器内」这个更底层问题的拙劣补丁。我之前删代码时只看到了补丁的丑,没看到补丁底下那个洞。

最终修复:把侧栏交给 Swup#

既然根因是「侧栏没被 Swup 接管」,正确的做法就不是用 JS 手动打补丁,而是让 Swup 自己来替换它。把 #sidebar-sticky 加进容器列表即可:

astro.config.mjs
swup({
containers: ["main", "#toc", "#sidebar-sticky"],
// ...
})

改完之后,每次导航 Swup 都会把 sticky 侧栏整段换成目标页面服务端渲染好的版本:

  • 文章页 → 显示 SideBarTOC(服务端按本页 headings 渲染,绝对不会错);
  • 非文章页 → 显示 RouteSwitch / Categories / UmamiStats;
  • 文章 ↔ 文章 → 目录自动更新为当前文章。

Profile 组件在 #sidebar-sticky 之外(在 #sidebar 里),所以不会跟着被重复替换,避免每次导航头像都闪一下。那段 updateSidebar 脚本至此可以彻底删掉,也不需要 reload() 了。

顺手的安全网:transition.css#

修这个 bug 的过程中还顺手处理了一个相关隐患。src/styles/transition.css 里原来有一条规则:

/* 之前 */
html.js-loaded .onload-animation {
opacity: 0;
}

它的本意是:元素默认透明,等 js-loaded 加到 <html> 上后,靠一次性的 fade-in-up 动画淡入。问题在于 js-loaded 只在首屏由 <head> 里的内联脚本加一次,而 Swup 只换容器、不重跑 <head>。所以客户端导航换进来的新内容,那个一次性动画不会可靠地重放,元素就卡在 opacity:0 看不见。

原来的 reload() 同样把这个问题盖住了——整页刷新会重置一切。既然现在不再依赖 reload,我就把这条静态 opacity:0 删了,让 .onload-animation 默认就是可见的,动画只在能跑的时候提供入场淡入:

/* 现在 */
.onload-animation {
animation: 300ms fade-in-up;
animation-fill-mode: forwards;
}
html:not(.js-loaded) .onload-animation {
animation: none;
opacity: 1;
}

这样即使某次 Swup 导航后动画没重放,内容也按自然状态可见,不会再出现「内容凭空消失」。

复盘#

一个 bug,三段修复,踩了两个坑:

  1. Swup 只替换 containers 里的内容。 任何在客户端导航后「状态没更新」的怪现象,先想一下它所在的 DOM 节点到底在不在 Swup 容器里。不在容器里,它就会永远停留在首次加载的状态——这正是目录消失的真正原因。
  2. 删代码前,先确认它「为什么存在」。 那段 reload() 看着像冗余的暴力兜底,实则是「侧栏不在容器内」这个架构问题的临时补丁。直接删掉而不解决根因,bug 只是换了个马甲(从「刷新」变成「目录不显示」)。
  3. 一次性入场动画 + 客户端路由 = 隐形风险。 凡是「靠 JS 加 class 才可见」的内容,都要想清楚在客户端导航后这个 class/动画是否还会按预期生效。最稳妥的做法是「默认可见,动画只是锦上添花」。

我想说的#

只能说主责在DS,我还是太相信Vibe coding神力了直接将博客全权交给Claude Code(DS风味),导致写出一堆bug

不过,能跑就行((

一次 Swup 客户端导航的踩坑:从整页刷新到目录消失
https://flygeon.top/posts/16/
作者
Flygeon
发布于
2026-07-14
许可协议
CC BY-NC-SA 4.0