前端开发踩坑实录:14 个真实 Bug 的根因与解法
来源:tiyukains-blog 项目实战。反复犯同样错误或踩已知坑时,可参考这篇。
坑 1:CSS 镂空效果(椭圆卡片窗口)
目标是白色卡片,中间挖椭圆洞,透出背后的 3D 模型。
试了 5 种方案全挂了:mask-image + inline SVG、mask-image + radial-gradient、clip-path + evenodd、SVG <mask> + CSS 引用(坐标错)、还有用 border-radius 双卡片拼椭圆(两条曲线对不上)。
最后靠两张 SVG 解决:同一段 path,fill-rule="evenodd",用不同 viewBox 切出上下两半,再用不同 z-index 把 Canvas 夹在中间。
CSS 遮罩/裁剪跨浏览器 unstable。SVG 原生能力(viewBox + path)更稳。不要死磕 CSS mask。
坑 2:CSS transform 互相覆盖
.r-pop 上写了 transform: scale(.88),结果把另一个 translateY(-50%) 定位给覆盖了。那个 blob 直接飞到页面顶部。
如果元素靠 transform 定位,不要再套带 transform 的动效 class。
坑 3:IntersectionObserver 边界抖动
元素刚露出 8% 就 toggle .on 开始动画。结果在边缘处 isIntersecting 反复横跳,抖得厉害。
后来改成 reveal-once + 动画锁:阈值 0.25,触发后锁定 1.7s,期间不再响应 observer。
坑 4:npm uninstall 删掉运行时依赖
代码里没 import 不代表包没用。shadcn 组件内部依赖 @base-ui/react、class-variance-authority,顺手 uninstall 后页面白屏。
删包前先用 npx depcheck 扫一遍,拿不准的先留着。
坑 5:CSP 安全头在 dev 模式拦截 HMR
CSP 只在生产开,开发环境跳过。但上线后还是接连踩坑:
script-src 'self'把 Next.js 内联脚本拦了 → 页面空白style-src 'self'把 Google Fonts CSS 拦了 → 中文字体加载失败
CSP 策略必须在 production build 本地跑一遍验证。dev 模式触发不了这些问题。
坑 6:字体选择的 AI 惯性
AI 默认推荐 Inter / Roboto / Space Grotesk,全站容易同质化。Fraunces 的 SOFT 轴让衬线显软,这里换成 Playfair Display。
字体是品牌主要视觉信号之一。避免 AI 默认字体。
坑 7:配置文件分散
链接、名字散落在 5+ 个文件里,改一处漏一处。
后来把所有配置塞进一个 site.config.ts,所有页面都读它。footerLinks 引用 links 对象,不再重复写 URL。
坑 8:ClientShell 移到 layout.tsx 后页面空白
layout.tsx 里的 Client Component 在页面导航时不会卸载重挂。useReveal() 的 useEffect([]) 空依赖导致 IntersectionObserver 只初始化一次,新页面的 .reveal 元素永远进不来。
改成依赖 usePathname(),每次路由变化自动重建 observer。
坑 9:Next.js <Link> vs 浏览器原生后退
文章页点「← Back」时,Next.js <Link href="/"> 是正向导航,会滚到页面顶部。
后来单独做了 BackLink 组件,用 router.back() 触发真正的浏览器后退。
坑 10:文章目录式结构
MDX 平铺 vs 文件夹自包含。图片放 public/ 和文章分两处,删文章时图片总是漏删。
改成 content/posts/YYYY-MM-DD-slug/slug.mdx + 同目录 assets/,图片用相对路径,通过动态路由服务。
坑 11:ArrowLink 装饰线影响居中
装饰线作为 inline-flex 子元素时,整体被居中,导致文字偏左。
改成对称双线 — Text —,左线 origin-right 往左伸,右线 origin-left 往右伸。
坑 12:hydration mismatch — useEffect DOM 操作时机
useReveal() 的 observer 在水合期间给可见元素加 data-locked 和 .on class。requestAnimationFrame 不够,setTimeout(0) 也不够。
最后用了 setTimeout + requestAnimationFrame 双层异步,延迟到水合完成后再动手。
坑 13:配置文件重构后字段未消费
aboutBlurb.paragraph1/2 定义好了,但页面还在用硬编码 JSX。改 config 不生效。
配置重构完,必须逐个字段检查消费者在哪。
坑 14:跨模块函数复制粘贴
parseFrontmatter、parseTagList、calcReadingTime 在 posts.ts 和 projects.ts 里一模一样。
抽了一个 frontmatter.ts 当单一真相源,两个模块和测试都 import 它。