跳到主内容

附录 D

实战避坑录

本书编写过程中积累的真枪实战踩坑记录。按网络 / 框架 / 工具链 / 部署 / 可访问性 五类组织,每条包含症状、原因、修复——方便你 Ctrl+F 快速定位。

升级标准:同一坑被踩 ≥ 2 次从 journal 升级到这里(SPEC-0005)。当前共 13 条。

D.1 网络与代理

中文开发环境中特有的网络问题——代理、镜像源、SSL 证书——在英文文档里几乎不会提到。

TUN 模式代理拦截本地开发服务器

症状:
pnpm test:e2e 卡死或返回 ERR_CONNECTION_REFUSED,但 curl 手动请求正常
原因:
Clash / Surge 等代理 APP 的 TUN 模式在网络栈 L3/L4 层拦截流量,绕开所有应用层 NO_PROXY / unset http_proxy 配置。CI 容器环境没有 TUN 所以能正常跑。
修复:
验证用 pnpm smoke(纯 node:http 打 127.0.0.1,绕开 L7 代理)。完整 E2E 时暂时关闭代理 APP 的 TUN 模式。详见 GOTCHAS G.3。

D.2 框架特定坑

Next.js / React / TypeScript 等技术栈自身的版本敏感问题。随版本升级可能过时,标注了发现时的版本号。

Next.js 16 Cache Components 必须使用 use cache

症状:
pnpm build 报 Uncached data was accessed outside of <Suspense>
原因:
next.config.ts 启用了 cacheComponents: true。数据读取函数(如 getAllDocs)未加 use cache 指令,被视为动态数据与 prerender 冲突。
修复:
给所有异步数据加载函数顶部加 use cache;。GOTCHAS G.1 有完整事故记录。

Edge Runtime 与 Cache Components 互斥

症状:
pnpm build 报 Route segment config runtime is not compatible with nextConfig.cacheComponents
原因:
cacheComponents: true 要求服务端 Runtime 支持静态缓存,Edge Runtime 的轻量环境不提供这一能力。两者不能同时开启。
修复:
去掉 export const runtime = 'edge',改用默认 Node.js Runtime。对静态站而言性能差异可忽略——CDN 缓存后首次请求后的响应一样快。GOTCHAS G.7。

编辑 MDX 内容后 dev server 不热更新

症状:
修改 content/ 下的 .mdx 文件后浏览器不自动刷新,必须手动硬刷新
原因:
src/lib/content.ts 的函数使用了 use cache 指令,文件读取被缓存在内存中。Turbopack 不监听 content/ 目录。
修复:
硬刷新(Cmd+Shift+R)。内容编辑频率远低于组件开发,可以接受。GOTCHAS G.6 记录了 Fumadocs 的替代方案。

Markdown 粗体 + CJK 括号解析错误

症状:
**中文(English)** 渲染异常——粗体未闭合、括号丢失、后续文字被吞
原因:
Markdown 解析器遇到 )**(全角右括号紧接两个星号)误判为星号属于括号内容的一部分,导致粗体边界识别错误。
修复:
用 HTML <strong> 标签替代 Markdown ** 语法。GOTCHAS G.5。

D.3 工具链协同

构建工具、测试框架、校验库之间的协作问题。往往不是单个工具的 bug,是搭配方式导致。

Zod + gray-matter YAML 自动日期转换

症状:
校验 content frontmatter 时 ZodError: expected string, received Date
原因:
gray-matter 遵循 YAML 1.1,会把裸 YYYY-MM-DD 自动解析为 JS Date 对象。同类转换:yes/no/on/off→boolean、null/~→null。
修复:
用防御性的 union + transform coerce:z.union([z.iso.date(), z.date()]).transform(v => v instanceof Date ? v.toISOString().slice(0,10) : v)。GOTCHAS G.2。

oxlint --fix 静默吞掉不可修复的错误

症状:
提交时 lint 通过,CI 上 lint 报错——同一份代码不同结果
原因:
lint-staged 配置为 oxlint --fix,但 --fix 对不可自动修复的错误(如未使用的 import)不报错退出。CI 上 oxlint(无 --fix)会报错。
修复:
lint-staged 拆为两步:oxlint(先检查拦截)+ oxlint --fix(再自动修复)。本项目已改为三阶段 oxlint → oxlint --fix → oxfmt。

frontmatter sequential 编号冲突

症状:
章节之间的"上一节/下一节"导航跳转错误——§4.6 的下一节显示 §5.1 标题
原因:
每篇内容文件的 frontmatter 里 paths.sequential 编号手工维护,新章编号与旧章尾部重叠。
修复:
scripts/check-sequential.mjs 自动检测重复和间隔,接入 pnpm ci 管道。新写内容时如果编号冲突构建直接失败。

D.4 部署与跨平台

生产环境部署、不同设备/浏览器上的表现差异。

iOS Safari 输入框自动缩放

症状:
iPhone 上点击搜索框后页面自动放大,键盘收起后不恢复
原因:
iOS Safari 默认行为:<input> 获得焦点时若 font-size < 16px,浏览器自动放大页面以便阅读——但用户期望的是不缩放。
修复:
设置输入框 font-size: 16px(刚好在阈值线上,性价比最高——0.5 个点的差异无关大局)。不要用 maximum-scale=1.0 禁止缩放——那会阻止所有用户手动缩放,影响可访问性。

微信内置浏览器 Web Share API 不完整

症状:
navigator.share() 弹窗后无法跳转到会话选择——分享流程中断
原因:
微信内置浏览器的 Web Share API 实现不完整,弹窗能出现但无法完成实际的分享操作。微信需要自己的 JS-SDK(wx.config())才能正常分享。
修复:
检测 UA 中 MicroMessenger → 降级到 clipboard.copy。非微信移动端继续使用原生分享。详见 ShareButton 组件注释。

移动端 tap 触发 mouse 事件序列

症状:
移动端点击按钮首次无效,第二次才响应——特别是 tooltip/dropdown 类交互
原因:
移动端浏览器在 tap 时会模拟完整鼠标事件序列:touchstart → touchend → mouseenter → focus → click。如果组件同时绑定了 mouseenter 和 click handler,它们会相互抵消。
修复:
用 (hover: hover) 媒体查询检测设备 hover 能力,在触屏设备上屏蔽 onMouseEnter/onFocus,只靠 onClick 处理交互。不要用 max-width 判断移动端——iPad + 键盘有 hover,窄桌面窗口没有。

移动端布局验证 gap

症状:
组件在桌面端正常,移动端出现布局溢出、热区缺失、表格列过窄换行
原因:
开发时只在桌面端验证,没有在真实窄屏上检查。pnpm build 通过 ≠ 体验合格。
修复:
提交 UI 组件前在 375px 视口目视验证 + 交互组件三件套(点击外部关闭/ESC 关闭/显式关闭按钮)+ 表格横向滚动检查。GOTCHAS G.4。

D.5 性能与可访问性

影响用户体验但非功能性 bug 的隐性陷阱。

链接 text-decoration 跨浏览器不一致

症状:
字母 descender(g/y/p)会切断下划线——尤其是中文混排时更明显
原因:
text-decoration-skip-ink 在不同浏览器中对 ink 边界的判断不一致。部分渲染引擎的降部仍会与下划线重叠。
修复:
用 border-bottom 替代 text-decoration。边框始终绘制在元素 box 下方,不受字形影响。本站已在全局链接样式中采用此方案。

避坑录持续更新中。发现新坑请 提交 Issue(选"内容建议"模板)。重复出现 ≥ 2 次后升级至此。