← 学习路线

失败路径:白屏、错误边界与兜底

一个地方出错时,你的网站是整页白屏,还是告诉访客发生了什么、给出一条回去的路?你又怎么知道它出过错?

课型:鉴别·约 20 分钟·5 个术语

先看出事的那一刻

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)都小心地处理了"这一章不存在",偏偏漏了这一行。

三道防线

出错这件事,要在三个地方各拦一次:

  1. 在源头判空。读取之前先确认它存在;对不上的地址,显示"这一章没有找到"和回到目录的入口。
  2. 错误边界。万一还是崩了,显示一个出错提示和一条回去的路,而不是一片空白。
  3. 日志与上报。前端的错误默认只留在访客自己的浏览器里;要把它们送到你看得见的地方,你才知道它发生过、发生了几次。

这次修复做了前两道:出错的那一行加上了判空,页面外层包了一个错误边界。第三道一直没做:错误边界接住了错误,却不上报任何东西。出了错,依然只有访客自己知道。

深潜从没被触发过的兜底,要手动触发一次+

修复之后,写错的章节地址会先被判空拦下,显示"这一章没有找到"的卡片,错误边界那一页至今没有人真正见过。

一个从没被触发过的兜底,很可能在真正需要它的那天出问题:按钮样式依赖的变量在它那一层有没有定义?它的注释说"点导航就能恢复",可兜底页会把导航一起替换掉。最稳妥的做法,是在开发环境里故意抛一个错,亲眼看一次兜底页长什么样、按钮能不能用。

另外,这张"这一章没有找到"的卡片上写着 404,服务器返回的却是 200:这是井号路由的单页应用说不出"没有"的老问题(第 5 课)。

找茬

下面是那次白屏的章节页代码,以及修复时加上的错误边界和外层结构(节选)。站长的要求是:任何一个地址出错,都不能让整个网站白屏;出了错,要有办法知道。

这段 JSX 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。

查看代码与答案(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. 页面突然整页空白,控制台的错误过滤里什么也没有。最可能是?

查看选项与答案
  • A. 页面的脚本文件没有加载出来,所以整个页面什么也没画——白屏不是没加载出来,而是加载出来之后又被整个拆掉了。页眉、导航、正文都是先显示出来、后来才消失的。
  • B. 接口没有返回数据,页面一直停在“加载中”的空白状态——页面是先显示出来、又整页消失的,连页眉和导航都没了,并不是停在等数据的状态,而是整棵页面树被卸载了。
  • C. 渲染时有代码出错,没人接住,整个页面树被卸载了(正确)——画的过程中出了错、又没有错误边界接住,React 就把整棵页面树卸载掉。把控制台切到全部日志,往往能看到它留下的那句提醒。

白屏不是没加载,而是加载之后被整个拆掉了。

2. 加上错误边界之后,还需要做什么?

查看选项与答案
  • A. 把接住的错误上报到你看得见的地方,并故意触发一次兜底页(正确)——兜底要看得见错误,也要确认它自己真的能用;从没被触发过的兜底,很可能在真正需要它的那天出问题。
  • B. 用写错的地址试了一遍,看到“没有找到”就算验证过了——写错的地址现在会先被判空拦下,根本走不到错误边界。兜底页要在开发环境里故意抛一个错,才能亲眼看到。
  • C. 在错误边界里用 console.error 记一下就行——console.error 只留在访客自己的浏览器里,你看不到。要把错误送到你看得见的地方,才知道它发生过、发生了几次。

错误边界是最后一道防线,不是唯一一道。

3. 访客打开了一个写错的章节地址。最好的表现是?

查看选项与答案
  • A. 显示“出错了,请刷新页面重试”这样的通用提示语——刷新之后地址还是那个地址,问题依旧。访客既不知道是这一章不存在,也没有一条回去的路。
  • B. 自动跳转回首页,让访客从头找起,不用看到错误页面——访客会以为链接带错了地方,也不知道自己要的那一章不存在(第 5 课)。
  • C. 显示“这一章没有找到”,并给出一个回到目录的明显入口(正确)——说清楚发生了什么,再给一条回去的路。

失败路径也是产品的一部分:说清楚,给出路。

判断时刻

上线前,你发现一个写错的地址能让整个网站白屏。时间只够做一件事,AI 给了三个方案。

你会选哪一个?

三个选项各自的代价
  • A. 在出错的那一行加上判空——考察只修这一处:改动最小,这个地址不会再白屏了。但同类的漏洞可能还有好几处,下一个没想到的地方出错,照样整页空白。
  • B. 在整个应用外层加一个错误边界——考察兜住所有:任何一处渲染出错,都至少能显示提示和返回的入口。但它治的是症状:出错的那一行还在;不加上报,你照样不知道它发生过。
  • C. 两个都做,再加上错误上报——考察修根因加兜底:最完整:已知的错修掉了,没想到的错有兜底,发生过的错也看得见。多出来的只是上报那一步,而这个项目恰恰缺了它。

判空修的是已知的错,错误边界兜的是未知的错,日志告诉你错发生过。三件事各管一段,缺了哪一段,都会在某天变成一个说不清的白屏。

带走

下次让 AI 做这件事时,问它

  1. 这个页面依赖的数据不存在、接口失败、地址写错时,分别会显示什么?
  2. 渲染出错时,有没有错误边界接住?它包住了哪些部分、显示什么、怎么恢复?
  3. 前端的错误有没有上报到我看得到的地方?
  4. 请手动触发一次错误,把兜底页面截图给我看。

自己验证

  • 在地址栏里故意输入一个不存在的章节、术语或文章地址,看页面怎么回应。
  • 打开开发者工具的控制台,把日志级别切到"全部",看有没有被忽略的警告。
  • 断开网络再操作一次,看接口失败时页面显示什么。

判空修已知的错,错误边界兜未知的错,日志告诉你错发生过。