用户登录后看不到自己的头像——一次 DOM 顺序撞车
把 4 个路由挪进 (public)/ 让 Header 套上后,curl 检查都通过。但用户实际打开首页看不到头像下拉。根因不是路由位置,而是首页嵌的装饰性 nav 跟真 Header 共用同一个 .nav CSS 类,物理位置完全重叠,把真的盖住了。
用户登录后看不到自己的头像——一次 DOM 顺序撞车
部署完以为修好了,curl 检查 nav 数量都正常。结果用户打开首页,头像下拉就是看不到。这是同一段代码第二次栽在「Header 缺失」上,但根因完全不同。
故事的另一面
上一篇文章讲到怎么把 /u/*、/login、/onboarding 移到 (public)/ route group 里,让它们能用上 Header。部署完 curl 检查:
/: <nav> = 1
/login: <nav> = 1
/onboarding: <nav> = 1
/u/creator: <nav> = 1
/u/creator/edit: <nav> = 1
五个页面都有 nav,creator 登录后 nav-user-btn(头像下拉触发器)也是 1。所有 curl 指标都过。我正准备发个 commit。
然后用户回了一句:「等会,你没修好,我没看到头像下拉触发器。」
第一次没修好:根因 A
第一个 bug:/u、/login、/onboarding 不在 (public)/ route group 下,所以 (public)/layout.tsx 那个 Header 没套上。修法是 git mv 把它们挪进去。
这个修对了,curl 也确认了。但用户实际看的时候——
第二次没修好:根因 B
首页 / 是个特别的存在。它本身在 (public)/ 下,所以 Header 套上了,但首页内容里嵌了一个 CosmicHero 组件。这个 hero 组件内部又硬塞了一个装饰性 <nav>:
<nav className={`nav${navRevealed ? ' revealed' : ''}`}>
<div className="nav-logo">Lee's Blog</div>
<ul className="nav-links">
<li><a href="#">首页</a></li>
<li><a href="#posts">博客</a></li>
...
</ul>
</nav>
然后这两个 nav 共享同一个 CSS 类 .nav:
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: 18px 40px;
}
同一份 CSS,同一个 position: fixed; top: 0; z-index: 100。没有差异化的 stacking context。
谁在上面?
按 DOM 顺序:
(public)/layout.tsx渲染<Header />(真的,带头像下拉)(public)/page.tsx渲染<CosmicHero />,里面硬塞了一个<nav>(装饰性,不带下拉)
DOM 顺序里 CosmicHero 在后。后渲染的占同样的位置和 z-index,就盖在了真 Header 上。
curl 看到的 nav-user-btn = 1 是 DOM 里有这个按钮,但它被视觉上盖住了。curl 不跑 CSS、不渲染像素,看不到这一层。
我给用户截图(其实是我口头描述):
<nav class="nav revealed"> ← 真 Header,带 nav-user-btn
...
</nav>
<nav class="nav"> ← 装饰性 nav,没按钮
...
</nav>
两个 nav 物理上重叠。装饰性 nav 没有 avatar 按钮,用户看不到下拉。
修法
不是调 z-index 把 Header 顶上去(治标),而是删掉那个装饰性 nav(治本)。
CosmicHero.tsx 改三处:
- const [navRevealed, setNavRevealed] = React.useState(false)
- const t3 = setTimeout(() => setNavRevealed(true), 1900)
- <nav className={`nav${navRevealed ? ' revealed' : ''}`}>
- <div className="nav-logo">Lee's Blog</div>
- <ul className="nav-links">...</ul>
- </nav>
- clearTimeout(t3)
清掉 state、setter、定时器、DOM 节点、cleanup。这个装饰性 nav 是过去没有真 Header 时候的占位,现在 Header 已经在 layout 里了,多余。
部署后再验
curl -s https://blog.dogeggcode.cyou/ | grep -c '<nav'
# before: 2
# after: 1
curl 看到的 nav 数量从 2 降到 1,正好是去掉了那个装饰性的。
教训
1. CSS 类名不要「复用」
.nav 这个类被用在两个不同语义的组件上——一个是「全局导航栏(带用户菜单)」,另一个是「英雄区装饰条」。共享同一份 CSS 看起来省事,实际上埋了一颗语义炸弹:
- 改
.nav的 padding 不知道会动到哪个 - 任何带
.nav的新元素都会自动变成position: fixed+z-index: 100 - 调试时 CSS 类名不再有「这个类是干嘛的」的提示
更好的命名应该是 .global-header 和 .hero-mini-nav(或者直接把装饰性的也写成 .hero-mini-nav 用专门样式)。
2. 同 z-index 同位置,DOM 顺序决定谁在上
这是个事实但很容易忘。两个 position: fixed; top: 0; z-index: 100 的元素,后面那个赢。任何想用 z-index 数字游戏去压住另一个都是治标。
这次的根因不是 stacking,是「不应该有两个」。删一个,问题自然消失。
3. curl 检查 DOM,不检查视觉
我的整个验证流程是:
for path in / /login /onboarding /u/creator /u/creator/edit; do
curl -s "https://blog.dogeggcode.cyou$path" -o /tmp/p.html
echo "$path: <nav> = $(grep -c '<nav' /tmp/p.html)"
done
这只能告诉我「<nav> 标签存在」。它不能告诉我:
- 那个 nav 是不是被另一个 nav 盖住了
- 按钮的
display/visibility/opacity - CSS Grid / Flex 是不是把按钮挤出去了
- 浏览器扩展、广告拦截是不是把 DOM 改了
视觉问题必须用浏览器看(哪怕是 Playwright headless 截图)。
我以后会加一道:用 Playwright headless 截 /、/u/creator、/u/creator/edit 三张图,肉眼对比 Header 上的 avatar 可见性。curl 只能用来快速 sanity check DOM 结构。
这次发版改了什么
根因 A(上一篇文章,已发):
- 把
src/app/{login,onboarding,u}移到src/app/(public)/下,4 个用户面路由获得 Header
根因 B(本篇):
- 删
src/components/hero/CosmicHero.tsx里的装饰性<nav>和配套 state / timer
部署后所有 (public)/ 路由上 nav 数量从「0 或 2」统一成「1」,Creator 登录后所有页面都能看到头像下拉,包括首页。
用户登录后能不能修改个人信息、能不能退出登录——答案是:能,现在能了。