前言:Localhost 是天堂,生产环境是地狱
很多兄弟写代码有个坏习惯:在自己电脑上(Localhost)跑通了,就觉得万事大吉了。
在你的电脑上:
- 网速是 1000M 光纤。
- 后端接口永远返回标准 JSON,从来不报错。
- 操作流程永远是按你设计的"快乐路径"走的。
但在生产环境(线上):
- 用户的网速可能还不如 2G,丢包率 50%。
- 后端服务偶尔会 502 Bad Gateway,或者心情不好给你返个
null。 - 用户可能会疯狂点击那个"提交"按钮,或者在数据没加载完时狂点 Tab 切换。
于是,你的 React 应用白屏了(White Screen of Death) 。 控制台一片血红:Cannot read properties of undefined (reading 'map')。
今天,我们要聊聊防御性编程。别指望环境完美,我们要假设世界下一秒就会毁灭,而我们的代码依然要屹立不倒。
第一层防御:不要相信任何人(尤其是后端)
后端老哥跟你说:"放心,这个字段我肯定返给你数组。" 千万别信。 在防御性编程的字典里,所有的外部输入都是有罪的。
❌ 裸奔写法:
tsx
const UserList = ({ data }) => {
// 如果后端脑抽返了个 null,或者 data 还没加载完
// 这里直接报错,整个组件树崩溃,页面白屏
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name.toUpperCase()}</li>
))}
</ul>
);
};
✅ 防弹写法(可选链 + 空值合并):
我们要把代码写得像个怕死鬼一样小心。
const
// 1. data 可能是 undefined
// 2. data.users 可能是 null
// 3. user.name 可能是 null (toUpperCase 会炸)
const safeUsers = data?.users ?? [];
return (
<ul>
{safeUsers.map(user => (
<li key={user?.id}>
{/* 如果 name 没有,给个默认值 '-',别让页面挂掉 */}
{user?.name?.toUpperCase() ?? '-'}
</li>
))}
</ul>
);
};
记住口诀: ?. 是你的保命符,?? 是你的底裤。多写几个问号,不会怀孕,但会救命。
第二层防御:Error Boundaries(错误边界)------ 熔断机制
即使你再小心,JS 还是可能会报错。 React 有一个特性:如果一个组件在渲染过程中抛出错误,整个组件树会被"卸载"(Unmount)。
这意味着,仅仅因为侧边栏的一个小图标渲染错了,你整个网页(包括顶部的导航、中间的内容)都会瞬间消失,变成一张白纸。用户除了刷新(然后再次崩溃),什么都做不了。
这就像家里厕所灯泡坏了,结果整栋楼自动引爆了一样离谱。
我们需要错误边界(Error Boundaries) ,它的作用就是:隔离错误。厕所灯坏了,就把厕所门关上,贴个条子"维修中",别的地方照常使用。
怎么用?推荐 react-error-boundary
React 官方目前还只支持用 Class 组件写 Error Boundary(这很复古),所以我强烈建议直接用 react-error-boundary 这个库,它不仅支持 Hooks,还更优雅。
import
// 1. 定义一个备用 UI(出错时显示这个)
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="p-4 bg-red-100 text-red-800">
<p>哎呀,这里好像出了点问题。</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试一下</button>
</div>
);
}
// 2. 把容易出事的组件包起来
const App = () => {
return (
<Layout>
<Sidebar />
<Main>
{/* 给核心内容区穿上防弹衣 */}
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 重试时的逻辑,比如重新请求数据
window.location.reload();
}}
>
<HighRiskComponent />
</ErrorBoundary>
</Main>
</Layout>
);
};
现在,如果 HighRiskComponent 炸了,用户只会看到一个友好的"出错了"提示框,而侧边栏和导航栏依然完好无损。用户体验从"灾难级"变成了"可接受级"。
第三层防御:API 请求的兜底
useEffect 里请求数据也是重灾区。很多人只写 then,不写 catch。 ❌ 乐观派写法:
useEffect(()
setLoading(true);
api.getData().then(res => {
setData(res);
setLoading(false); // 如果 api 报错了,loading 永远是 true,页面永远在转圈
});
}, []);
✅ 悲观派写法(Finally大法):
useEffect(()
let isMounted = true; // 防止组件卸载后还去 setState (这也是个常见警告)
const fetchData = async () => {
setLoading(true);
setError(null); // 每次请求前记得重置错误状态
try {
const res = await api.getData();
if (isMounted) setData(res);
} catch (err) {
if (isMounted) setError(err); // 捕获错误,展示 Error UI
// 甚至可以在这里上报错误日志到 Sentry
reportToSentry(err);
} finally {
// 无论成功失败,必须结束 loading
if (isMounted) setLoading(false);
}
};
fetchData();
return () => { isMounted = false; };
}, []);
总结:哪怕天塌下来,你的 App 也要优雅地死
防御性编程的核心心态就两个字:悲观。
- 数据层面 :默认所有数据都可能是空的。用
?.和??处理掉所有undefined。 - UI 层面 :用
ErrorBoundary包裹主要区域。局部坏死好过全身瘫痪。 - 交互层面:永远要有 Loading 状态,永远要有 Error 状态,永远要有重试按钮。
当你的应用在断网、接口报错、数据异常时,还能给用户显示一个漂亮的"请检查网络"或者"重试",而不是直接白屏或者卡死,这时候,你才算是一个成熟的前端工程师。
好了,我要去给那个没有任何空值判断的详情页加"防弹衣"了,祝大家的线上环境永远不死。

下期预告 :你有没有觉得现在的 React 项目越来越大,打包出来几 MB,首屏加载慢得像蜗牛? 下一篇,我们要聊聊 "代码分割(Code Splitting)"与 Lazy Loading。教你如何把你的应用切成小块,按需加载,让首屏速度起飞。