
全局样式泄漏排查:PptLayout 的黑底为何污染了 PPT 列表页
一、现象:列表页下半截变成了黑色
/ppt/ 列表页上线后出现了一个诡异的样式问题:页面上半部分是正常的浅灰渐变 + 白色卡片,但往下滚动,渐变结束后突然变成纯黑背景,白色卡片浮在黑底上,看起来像是样式乱了。
更奇怪的是,这个页面的样式代码里根本没有写过任何黑色背景。
二、线索:黑底样式只可能来自一个地方
全站只有放映页是黑底,样式定义在 src/layouts/PptLayout.astro:
<style is:global>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000;
}
.reveal {
width: 100vw !important;
height: 100vh !important;
}
</style>
但列表页 src/pages/ppt/index.astro 根本没有渲染 PptLayout,它只渲染了 Header、Footer 和卡片列表。黑底样式是怎么跑过来的?
三、根因:eager glob 把组件(含全局样式)一起打进了列表页
列表页为了自动提取每套 PPT 的 meta,用了 eager 模式的 glob:
const pptModules = import.meta.glob('../../ppts/*.astro', { eager: true });
而每一套 PPT(src/ppts/*.astro)的 frontmatter 里都 import PptLayout。eager import 会执行这些模块,于是 PptLayout 的 <style is:global> 也被 Vite 打进了列表页的 CSS 包。构建产物证实了这一点:dist/ppt/index.html 引用了 PptLayout.*.css。
这就解释了所有症状:
- 下半截黑底:
global.css的 body 渐变只有顶部 600px(background-size: 100% 600px),600px 之后露出的是 body 底色——正常是白色,但被泄漏的background-color: #000改成了黑色。 - 页面无法正常滚动:泄漏的
overflow: hidden同样作用在 body 上。
四、排除:reveal.js 自带的全局规则 harmless
在修之前还排查了一个嫌疑人:PptLayout import 的 reveal.js/reveal.css 里也有裸 html/body 选择器,比如:
html:not(.print-pdf) body { margin: 0; padding: 0; overflow: visible }
html:not(.print-pdf) .reveal { background: #fff; font-size: 20pt }
逐条核对后确认无害:前者在列表页命中的声明(margin: 0; overflow: visible)与全局样式一致,属于默认值;后者只匹配 .reveal 元素,而列表页没有该元素。真正的凶手只有我们自己写的那段 html, body 黑底。
五、修复:用 body:has() 把作用域收敛到 deck 自身
裸 html/body 选择器的问题在于:只要 CSS 文件被加载,规则就对全页生效,与组件是否渲染无关。修复思路是让选择器与组件的实际渲染挂钩:
<div
class={`ppt-deck reveal ${className}`}
...
>
<style is:global>
/* 仅在实际渲染了 deck 的页面生效,避免被 ppt 列表页的 eager glob 连带打包污染 */
body:has(.ppt-deck) {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #000;
}
.ppt-deck {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #000;
}
.ppt-deck.reveal {
width: 100vw !important;
height: 100vh !important;
}
</style>
CSS 文件依然会被打进列表页(eager glob 的机制决定了这点无法避免),但它的选择器在列表页没有任何匹配元素,自然就失效了。而放映页渲染了 .ppt-deck,黑底全屏效果原样保留。
六、验证:检查构建产物而非只看浏览器
npm run build 通过后,做了三层确认:
dist/ppt/index.html中background-color:#000出现 0 次;- 打包出的
PptLayout.*.css里已没有裸html/body黑底规则,只剩body:has(.ppt-deck); - 放映页(如
profit_by_indu)产物中仍包含ppt-deck容器与对应样式,黑底放映效果不受影响。
七、经验总结
is:global+ 裸元素选择器是高危组合:在 Astro 里,is:global的html/body样式一旦被任何页面间接 import,就会污染该页面。能用类选择器就不要碰元素选择器。- eager glob 会连带打包样式:
import.meta.glob(..., { eager: true })不只是读数据,模块的副作用(含全局 CSS)会一起进入产物。用它读meta时,要默认所有被 glob 模块的全局样式都会泄漏。 body:has()是收敛全局样式的利器:既保留了全屏场景下必须控制 body 的能力,又把生效条件绑定到组件实际渲染的 DOM 上,浏览器支持度如今也足够好。- 用构建产物验证样式问题:浏览器里看效果只能确认表象,
grep产物 CSS 才能确认泄漏是否真正消除。