JS前端白屏前世今生及解决方式
前言
白屏(Blank Screen),它无所不及,摧枯拉朽,令用户体感全失、测试提 P0 相见、研发不寒而栗,胆战心惊,只知匆忙回滚。
对于离用户最近的前端,更是重灾区,浏览器上只要出现白屏,先找前端准没错。
近期工作中频频遇到线上白屏事故,我借这个机遇,介绍为什么会产生白屏,以及应对之道。
兵法著:知彼知己,百战不殆;不知彼知己,一胜一负,不知彼不知己,每战必殆。
只有足够了解白屏,了解自身代码的局限性,才能云淡风轻,编程游刃有余。
白屏从何而来
导致白屏的原因,大概率分为两种:
- 资源访问错误
- 代码执行错误
两者虽然“各有千秋”,但从现代前端视角来看,都和 SPA 框架的广泛应用逃不了干系。
资源访问错误
这里的资源特指 JavaScript 脚本、样式表、图片等静态资源,不包括服务调用等动态资源。
最典型的例子莫过于 React、Vue 等 SPA 框架构建的 Web 应用,一旦 [bundle|app].js
因为网络原因访问失败,便会引发页面白屏。
你可以访问 https://vue-ebgcbmiy3-b2d1.vercel.app/,按照如下步骤复现:打开 DevTools > Network,找到 app.3b315b6b.js,右键并选中 Block request URL,随后刷新页面。
代码执行出错
如果说资源访问错误还有回旋的余地,可能用户的网络不稳定,重试几次便能恢复正常。
那么在 render 过程中,代码执行出错无异于被宣判死刑,包括但不限于:
- 读取 undefined null 的属性,
null.a;
- 对普通对象进行函数调用,
const o = {}; o();
- 将 null undefined 传递给 Object.keys,
Object.keys(null);
- JSON 反序列化接受到非法值,
JSON.parse({});
你必须经历本地调试,CI、CD,构建部署等一系列措施、或者直接 rollback.
为什么 read properties of undefined 就白屏了?
先请教一个问题,试问以下代码的执行是否会导致页面白屏?
为了摆脱框架的约束,我特意使用原生 JavaScript、以命令式的编程范式动态渲染一个网页。
<body> <div id="root"></div> <script> const arr = ["webpack", "rollup", "parcel"]; const root = document.getElementById("root"); const ul = document.createElement("ul"); for (let i = 0; i <= arr.length - 1; i++) { const li = document.createElement("li"); li.innerHTML = arr[i]; ul.appendChild(li); } root.appendChild(ul); const h1 = document.createElement("h1"); // trigger read properties of undefined h1.textContent = document.createTextNode({}.a.b); root.appendChild(h1); </script> </body>
浏览器的真实表现是 ul 被正常渲染,而 h1 直接不渲染,两者互不影响,更不会导致白屏。
把视角切回 React,我们将渲染 ul h1 的过程类比为渲染 <Ul />
组件 和 <H1 />
组件,看看会发生什么?
const Ul = () => ( <ul> {["webpack", "rollup", "parcel"].map((v) => ( <li>{v}</li> ))} </ul> ); // trigger read properties of undefined const H1 = () => <h1>{{}.a.b}</h1>; const App = () => { return ( <> <Ul /> <H1 /> </> ); }; ReactDOM.render(<App />, document.getElementById("root"));
毫无意外,页面呈现白屏状态,<H1 />
的渲染错误致使整个 <App />
都崩溃了。
根本原因是自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。
翻译一下就是如果在组件的渲染期间内,发生了 Uncaught Errors,而又未被 Error Boundaries 捕获,整个 <App />
所表示的 DOM 结构都被会移除,如下所示:
ReactDOM.render(null, document.getElementById("root"));
React 用白屏真正诠释了什么叫唇寒齿亡,牵一发而动全身,这也验证了我之前的说法,现代 Web 应用频繁白屏和 SPA 框架逃不了干系。
但你能说这个机制是负向优化的吗?官方说法是:
我们对这一决定有过一些争论,但根据我们的经验,把一个错误的 UI 留在那比完全移除它要更糟糕。例如,在类似 Messenger 的产品中,把一个异常的 UI 展示给用户可能会导致用户将信息错发给别人。同样,对于支付类应用而言,显示错误的金额也比不呈现任何内容更糟糕。
我越来越相信,前端层出不穷的框架或是新技术,虽然它的 leverage 足够大,但背后隐含着 trade-off,在绝大多数场景下表现优异,在另一些场景下你也必须要接受它的“规则”。
为什么不能是 ⬛ 屏、🟦 屏?
既然 DOM 都被移除了,只剩下个光秃秃的 div#app
节点,加上 body 的默认背景颜色是 #FFF
,理所应当白屏。
<body> <div id="app"></div> <script src="/js/chunk-vendors.61a12961.js"></script> <script src="/js/app.3b315b6b.js"></script> </body>
因此,不仅黑屏、蓝屏可以实现,只要将 body 的背景颜色稍作调整,彩虹屏也可以实现,彼时复盘文档的标题名为 「XXX 引发彩虹屏」,活成了前端喜剧人的样子。
我认为白屏只是一种代号,引申的含义是页面无内容渲染。
我还想强调,白屏只是一种外在表现形式,内在错误已经发生,不可挽回,肯定会给用户带来功能上的影响,只不过白屏的视觉冲击力最强,大脑直觉反馈十分严重。
如何降低白屏的“破坏力”
不再赘述如何避免白屏,因为错误时时刻刻会发生,我们能做的是尽人事,遵循以下原则:
- 依赖不可信,npm 的 Breaking Change
- 调用不可信,HTTP/RPC 等 API 调用不仅会失败,还会返回约定之外的数据,不兼容过时版本
- 输入不可信,用户常常会输入一些边界值、非法值 能尽可能避免异常。
我们关注的是错误已经发生的窘境下,如何及时补救,把外在的不良表现弱化成用户可以接受,或者无感知的状态。
借助于 ErrorBoundary,它能捕获任意子组件在渲染期间发生的 Uncaught Errors,从而避免整体组件树的卸载,把白屏扼杀在摇篮中。
除此之外,它还能对渲染错误的组件做兜底,具体的处理措施有两种:熔断和降级。
组件 “熔断”
熔断机制指的是在股票市场的交易时间中,当价格波动的幅度达到某一个限定的目标(熔断点)时,对其暂停交易一段时间的机制。 此机制如同保险丝在电流过大时候熔断,避免引发更大的事故,因此得名。
它被大量应用于容灾体系,对应 React 体系中,熔断点等同于渲染错误发生,暂定交易等同于卸载组件,直接不渲染,舍车保帅。
直接看例子:
import { ErrorBoundary } from "react-error-boundary"; const Other = () => <h1>I AM OTHER</h1>; const Bug = () => { const [val, setVal] = useState({}); const triggerError = () => { setVal(undefined); }; return ( <> <button onClick={triggerError}>trigger render error</button> <h1>I HAVE BUG, DO NOT CLICK ME</h1> {Object.keys(val)} </> ); }; const App = () => ( <> <Other /> <ErrorBoundary fallbackRender={() => null}> <Bug /> </ErrorBoundary> </> );
组件优雅降级
优雅降级指使用 替代渲染出错的组件,并做符合功能场景,用户心智的提示。
import { ErrorBoundary } from "react-error-boundary"; function ErrorFallback({ error, resetErrorBoundary }) { return ( <div role="alert"> <p>Something went wrong:</p> <pre>{error.message}</pre> <button onClick={resetErrorBoundary}>Try again</button> </div> ); } const App = () => ( <> <Other /> <ErrorBoundary fallbackRender={ErrorFallback}> <Bug /> </ErrorBoundary> </> );
以上 demo 所选择的错误边界库为 https://github.com/bvaughn/react-error-boundary,可在生产环境中投入使用。
前提是大家都要有对每个组件加上错误边界的共识,配合团队内部的监控上报和 Lint 检测,才能最大限度降低白屏的“破坏力”,打造一个稳定性更强的线上环境。
题外话:主动 throw error 导致白屏
我宁愿犯错,也不愿什么也不做。
这一点我和 React Team 的观点相同,与其展示错误的 UI,不如不展示。
错误的 UI,随时是个定时炸弹,在特定情况下就会爆炸,试想用户在错误的界面进行操作,小则造成 BUG,大则造成经济损失、安全泄露,会带来不可损失的影响,所以遇到对于非预期的行为,一定要主动 throw error,并做好组件熔断及降级。
以上就是JS前端白屏前世今生及解决方式的详细内容,更多关于JS前端白屏解决的资料请关注脚本之家其它相关文章!
相关文章
Meta开源JavaScript内存泄漏监测工具MemLab安装使用
这篇文章主要为大家介绍了Meta开源JavaScript内存泄漏监测工具MemLab安装使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-09-09JS面试中你不知道的call apply bind方法及使用场景详解
这篇文章主要为大家介绍了JS面试中你不知道的call apply bind方法及使用场景详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-02-02
最新评论