先看出事的那一刻
2026 年 8 月 2 日,站长请 AI 审查另一个 AI 刚做完的重构。读代码时,AI 已经怀疑章节页有一个会导致白屏的漏洞。为了验证,它在地址栏里凭印象输入了一个章节地址:#/mainline/perception。真实的地址要长得多,是 perception-and-scene-representation。
页面整个变白了:页眉、导航、正文全都没了。把地址改回正确的章节,还是白的。打开控制台,只看错误的过滤器里显示"没有日志";切到全部日志,只有 React 留下的一句提醒:建议加一个错误边界。只有刷新页面,才能恢复。
这节课要回答的是:一个地方出错时,你的网站是整页白屏,还是告诉访客发生了什么、给出一条回去的路?你又怎么知道它出过错?
装备几个词
为什么要懂
AI 替你做了
AI 写页面时,会把正常的路径做得很完整:加载数据、渲染内容、处理交互,一次跑通,截图也好看。
留给你的
想清楚出错时会怎样:数据不存在、接口失败、地址写错。这些路径,AI 很少主动去走一遍。
不懂的代价
一个打错的链接,就能让整个网站白屏;访客不知道发生了什么,你也不知道它发生过。
白屏是怎么发生的
单页应用的页面,是浏览器里的 JavaScript 画出来的。画的过程中,只要有一处代码出错、没有人接住,React 就会把整棵页面树卸载掉,只留下一个空的根节点。这就是白屏:不是"没加载出来",而是"加载出来又被整个拆掉了"。
这一次,出错的只有一行:
const stage = mainline.stages[index]; // 地址对不上时,这里是 undefined
const relatedTopics = (stage.topic_ids ?? []).map(…); // 读 undefined 的属性,直接崩溃
if (!stage) return <Missing title="这一章没有找到" />; // "存不存在"的判断,写在了崩溃之后
重构之前,判断写在最前面;重构之后,判断被挪到了后面,中间那行读取却没有跟着做判空。前后几行(stage ? … : null、stage?.body)都小心地处理了"这一章不存在",偏偏漏了这一行。
三道防线
出错这件事,要在三个地方各拦一次:
- 在源头判空。读取之前先确认它存在;对不上的地址,显示"这一章没有找到"和回到目录的入口。
- 错误边界。万一还是崩了,显示一个出错提示和一条回去的路,而不是一片空白。
- 日志与上报。前端的错误默认只留在访客自己的浏览器里;要把它们送到你看得见的地方,你才知道它发生过、发生了几次。
这次修复做了前两道:出错的那一行加上了判空,页面外层包了一个错误边界。第三道一直没做:错误边界接住了错误,却不上报任何东西。出了错,依然只有访客自己知道。
深潜从没被触发过的兜底,要手动触发一次+
修复之后,写错的章节地址会先被判空拦下,显示"这一章没有找到"的卡片,错误边界那一页至今没有人真正见过。
一个从没被触发过的兜底,很可能在真正需要它的那天出问题:按钮样式依赖的变量在它那一层有没有定义?它的注释说"点导航就能恢复",可兜底页会把导航一起替换掉。最稳妥的做法,是在开发环境里故意抛一个错,亲眼看一次兜底页长什么样、按钮能不能用。
另外,这张"这一章没有找到"的卡片上写着 404,服务器返回的却是 200:这是井号路由的单页应用说不出"没有"的老问题(第 5 课)。
找茬
下面是那次白屏的章节页代码,以及修复时加上的错误边界和外层结构(节选)。站长的要求是:任何一个地址出错,都不能让整个网站白屏;出了错,要有办法知道。
这段 JSX 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。
·第 5 行高危
读取一个可能不存在的对象
地址对不上时,
stage是 undefined,读它的topic_ids直接抛错,整个页面树被卸载。前后几行都做了判空,偏偏这一行没有。该问的话:这一行用到的数据,有没有可能不存在?不存在时会怎样?
·第 7 行中危
判断来得太晚
"这一章存不存在"的判断写在了第 5 行之后。代码跑不到这里,这句友好的"没有找到"就永远显示不出来。
该问的话:判断数据是否存在的代码,是写在第一次读取它之前吗?
·第 10 行中危
注释说的恢复方式并不存在
兜底页会把整个页面换掉,包括导航,所以"点导航就能恢复"做不到:访客只能点兜底页上的按钮,或者自己改地址。
该问的话:出错之后,访客在屏幕上能点到什么?实际试一次。
·第 11 行中危
接住了错误,却什么也没记下
这里只把状态改成"出错了",没有记录错误信息,也没有上报。错误就在访客的浏览器里静静消失,你永远不知道它发生过、发生过几次。
该问的话:这个错误边界接住的错误,会被记录或上报到哪里?
·第 17 行中危
导师对话框在保护范围之外
错误边界只包住了主体页面。导师对话框、登录窗口都在它外面,这里面的代码一出错,整个页面照样白屏。
该问的话:错误边界包住了哪些部分?没包住的部分出错时会怎样?
查看代码与答案(5 处问题)
1 // MintlifyExperience.jsx(章节页,节选)
2 const stage = mainline.stages[index];
3 const previous = stage ? mainline.stages[index - 1] ?? null : null;
4 const headings = stage?.body.split("\n") …
5 const relatedTopics = (stage.topic_ids ?? []).map(…);
6 useEffect(() => { … }, [chapterId]);
7 if (!stage) return <Missing title="这一章没有找到" … />;
8
9 // ErrorBoundary.jsx(修复时新增,节选)
10 // 路由变化时清除错误态,让用户点导航就能恢复
11 static getDerivedStateFromError() { return { failed: true }; }
12
13 // App.jsx(修复后的外层结构,节选)
14 <ErrorBoundary resetKey={`${route.view}/${route.id ?? route.topic ?? ""}`}>
15 <MintlifyExperience {...experienceProps} />
16 </ErrorBoundary>
17 <Tutor page={tutorPage} onClose={() => setTutorOpen(false)} onUnauthorized={handleUnauthorized} />- 第 5 行 · 高危 · 读取一个可能不存在的对象:地址对不上时,
stage是 undefined,读它的topic_ids直接抛错,整个页面树被卸载。前后几行都做了判空,偏偏这一行没有。 该问的话:这一行用到的数据,有没有可能不存在?不存在时会怎样? - 第 7 行 · 中危 · 判断来得太晚:"这一章存不存在"的判断写在了第 5 行之后。代码跑不到这里,这句友好的"没有找到"就永远显示不出来。 该问的话:判断数据是否存在的代码,是写在第一次读取它之前吗?
- 第 10 行 · 中危 · 注释说的恢复方式并不存在:兜底页会把整个页面换掉,包括导航,所以"点导航就能恢复"做不到:访客只能点兜底页上的按钮,或者自己改地址。 该问的话:出错之后,访客在屏幕上能点到什么?实际试一次。
- 第 11 行 · 中危 · 接住了错误,却什么也没记下:这里只把状态改成"出错了",没有记录错误信息,也没有上报。错误就在访客的浏览器里静静消失,你永远不知道它发生过、发生过几次。 该问的话:这个错误边界接住的错误,会被记录或上报到哪里?
- 第 17 行 · 中危 · 导师对话框在保护范围之外:错误边界只包住了主体页面。导师对话框、登录窗口都在它外面,这里面的代码一出错,整个页面照样白屏。 该问的话:错误边界包住了哪些部分?没包住的部分出错时会怎样?
快测
1. 页面突然整页空白,控制台的错误过滤里什么也没有。最可能是?
你可能是这么想的:白屏不是没加载出来,而是加载出来之后又被整个拆掉了。页眉、导航、正文都是先显示出来、后来才消失的。
你可能是这么想的:页面是先显示出来、又整页消失的,连页眉和导航都没了,并不是停在等数据的状态,而是整棵页面树被卸载了。
对了。画的过程中出了错、又没有错误边界接住,React 就把整棵页面树卸载掉。把控制台切到全部日志,往往能看到它留下的那句提醒。
白屏不是没加载,而是加载之后被整个拆掉了。
查看选项与答案
- A. 页面的脚本文件没有加载出来,所以整个页面什么也没画——白屏不是没加载出来,而是加载出来之后又被整个拆掉了。页眉、导航、正文都是先显示出来、后来才消失的。
- B. 接口没有返回数据,页面一直停在“加载中”的空白状态——页面是先显示出来、又整页消失的,连页眉和导航都没了,并不是停在等数据的状态,而是整棵页面树被卸载了。
- C. 渲染时有代码出错,没人接住,整个页面树被卸载了(正确)——画的过程中出了错、又没有错误边界接住,React 就把整棵页面树卸载掉。把控制台切到全部日志,往往能看到它留下的那句提醒。
白屏不是没加载,而是加载之后被整个拆掉了。
2. 加上错误边界之后,还需要做什么?
对了。兜底要看得见错误,也要确认它自己真的能用;从没被触发过的兜底,很可能在真正需要它的那天出问题。
你可能是这么想的:写错的地址现在会先被判空拦下,根本走不到错误边界。兜底页要在开发环境里故意抛一个错,才能亲眼看到。
你可能是这么想的:console.error 只留在访客自己的浏览器里,你看不到。要把错误送到你看得见的地方,才知道它发生过、发生了几次。
错误边界是最后一道防线,不是唯一一道。
查看选项与答案
- A. 把接住的错误上报到你看得见的地方,并故意触发一次兜底页(正确)——兜底要看得见错误,也要确认它自己真的能用;从没被触发过的兜底,很可能在真正需要它的那天出问题。
- B. 用写错的地址试了一遍,看到“没有找到”就算验证过了——写错的地址现在会先被判空拦下,根本走不到错误边界。兜底页要在开发环境里故意抛一个错,才能亲眼看到。
- C. 在错误边界里用
console.error记一下就行——console.error只留在访客自己的浏览器里,你看不到。要把错误送到你看得见的地方,才知道它发生过、发生了几次。
错误边界是最后一道防线,不是唯一一道。
3. 访客打开了一个写错的章节地址。最好的表现是?
你可能是这么想的:刷新之后地址还是那个地址,问题依旧。访客既不知道是这一章不存在,也没有一条回去的路。
你可能是这么想的:访客会以为链接带错了地方,也不知道自己要的那一章不存在(第 5 课)。
对了。说清楚发生了什么,再给一条回去的路。
失败路径也是产品的一部分:说清楚,给出路。
查看选项与答案
- A. 显示“出错了,请刷新页面重试”这样的通用提示语——刷新之后地址还是那个地址,问题依旧。访客既不知道是这一章不存在,也没有一条回去的路。
- B. 自动跳转回首页,让访客从头找起,不用看到错误页面——访客会以为链接带错了地方,也不知道自己要的那一章不存在(第 5 课)。
- C. 显示“这一章没有找到”,并给出一个回到目录的明显入口(正确)——说清楚发生了什么,再给一条回去的路。
失败路径也是产品的一部分:说清楚,给出路。
判断时刻
上线前,你发现一个写错的地址能让整个网站白屏。时间只够做一件事,AI 给了三个方案。
你会选哪一个?
考察:只修这一处
改动最小,这个地址不会再白屏了。但同类的漏洞可能还有好几处,下一个没想到的地方出错,照样整页空白。
考察:兜住所有
任何一处渲染出错,都至少能显示提示和返回的入口。但它治的是症状:出错的那一行还在;不加上报,你照样不知道它发生过。
考察:修根因加兜底
最完整:已知的错修掉了,没想到的错有兜底,发生过的错也看得见。多出来的只是上报那一步,而这个项目恰恰缺了它。
判空修的是已知的错,错误边界兜的是未知的错,日志告诉你错发生过。三件事各管一段,缺了哪一段,都会在某天变成一个说不清的白屏。
三个选项各自的代价
- A. 在出错的那一行加上判空——考察只修这一处:改动最小,这个地址不会再白屏了。但同类的漏洞可能还有好几处,下一个没想到的地方出错,照样整页空白。
- B. 在整个应用外层加一个错误边界——考察兜住所有:任何一处渲染出错,都至少能显示提示和返回的入口。但它治的是症状:出错的那一行还在;不加上报,你照样不知道它发生过。
- C. 两个都做,再加上错误上报——考察修根因加兜底:最完整:已知的错修掉了,没想到的错有兜底,发生过的错也看得见。多出来的只是上报那一步,而这个项目恰恰缺了它。
判空修的是已知的错,错误边界兜的是未知的错,日志告诉你错发生过。三件事各管一段,缺了哪一段,都会在某天变成一个说不清的白屏。
带走
下次让 AI 做这件事时,问它
- 这个页面依赖的数据不存在、接口失败、地址写错时,分别会显示什么?
- 渲染出错时,有没有错误边界接住?它包住了哪些部分、显示什么、怎么恢复?
- 前端的错误有没有上报到我看得到的地方?
- 请手动触发一次错误,把兜底页面截图给我看。
自己验证
- 在地址栏里故意输入一个不存在的章节、术语或文章地址,看页面怎么回应。
- 打开开发者工具的控制台,把日志级别切到"全部",看有没有被忽略的警告。
- 断开网络再操作一次,看接口失败时页面显示什么。
判空修已知的错,错误边界兜未知的错,日志告诉你错发生过。
