JS前端白屏前世今生及解決方式
前言
白屏(Blank Screen),它無(wú)所不及,摧枯拉朽,令用戶體感全失、測(cè)試提 P0 相見(jiàn)、研發(fā)不寒而栗,膽戰(zhàn)心驚,只知匆忙回滾。
對(duì)于離用戶最近的前端,更是重災(zāi)區(qū),瀏覽器上只要出現(xiàn)白屏,先找前端準(zhǔn)沒(méi)錯(cuò)。
近期工作中頻頻遇到線上白屏事故,我借這個(gè)機(jī)遇,介紹為什么會(huì)產(chǎn)生白屏,以及應(yīng)對(duì)之道。
兵法著:知彼知己,百戰(zhàn)不殆;不知彼知己,一勝一負(fù),不知彼不知己,每戰(zhàn)必殆。
只有足夠了解白屏,了解自身代碼的局限性,才能云淡風(fēng)輕,編程游刃有余。
白屏從何而來(lái)
導(dǎo)致白屏的原因,大概率分為兩種:
- 資源訪問(wèn)錯(cuò)誤
- 代碼執(zhí)行錯(cuò)誤
兩者雖然“各有千秋”,但從現(xiàn)代前端視角來(lái)看,都和 SPA 框架的廣泛應(yīng)用逃不了干系。
資源訪問(wèn)錯(cuò)誤
這里的資源特指 JavaScript 腳本、樣式表、圖片等靜態(tài)資源,不包括服務(wù)調(diào)用等動(dòng)態(tài)資源。
最典型的例子莫過(guò)于 React、Vue 等 SPA 框架構(gòu)建的 Web 應(yīng)用,一旦 [bundle|app].js
因?yàn)榫W(wǎng)絡(luò)原因訪問(wèn)失敗,便會(huì)引發(fā)頁(yè)面白屏。
你可以訪問(wèn) https://vue-ebgcbmiy3-b2d1.vercel.app/,按照如下步驟復(fù)現(xiàn):打開(kāi) DevTools > Network,找到 app.3b315b6b.js,右鍵并選中 Block request URL,隨后刷新頁(yè)面。
代碼執(zhí)行出錯(cuò)
如果說(shuō)資源訪問(wèn)錯(cuò)誤還有回旋的余地,可能用戶的網(wǎng)絡(luò)不穩(wěn)定,重試幾次便能恢復(fù)正常。
那么在 render 過(guò)程中,代碼執(zhí)行出錯(cuò)無(wú)異于被宣判死刑,包括但不限于:
- 讀取 undefined null 的屬性,
null.a;
- 對(duì)普通對(duì)象進(jìn)行函數(shù)調(diào)用,
const o = {}; o();
- 將 null undefined 傳遞給 Object.keys,
Object.keys(null);
- JSON 反序列化接受到非法值,
JSON.parse({});
你必須經(jīng)歷本地調(diào)試,CI、CD,構(gòu)建部署等一系列措施、或者直接 rollback.
為什么 read properties of undefined 就白屏了?
先請(qǐng)教一個(gè)問(wèn)題,試問(wèn)以下代碼的執(zhí)行是否會(huì)導(dǎo)致頁(yè)面白屏?
為了擺脫框架的約束,我特意使用原生 JavaScript、以命令式的編程范式動(dòng)態(tài)渲染一個(gè)網(wǎng)頁(yè)。
<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>
瀏覽器的真實(shí)表現(xiàn)是 ul 被正常渲染,而 h1 直接不渲染,兩者互不影響,更不會(huì)導(dǎo)致白屏。
把視角切回 React,我們將渲染 ul h1 的過(guò)程類比為渲染 <Ul />
組件 和 <H1 />
組件,看看會(huì)發(fā)生什么?
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"));
毫無(wú)意外,頁(yè)面呈現(xiàn)白屏狀態(tài),<H1 />
的渲染錯(cuò)誤致使整個(gè) <App />
都崩潰了。
根本原因是自 React 16 起,任何未被錯(cuò)誤邊界捕獲的錯(cuò)誤將會(huì)導(dǎo)致整個(gè) React 組件樹(shù)被卸載。
翻譯一下就是如果在組件的渲染期間內(nèi),發(fā)生了 Uncaught Errors,而又未被 Error Boundaries 捕獲,整個(gè) <App />
所表示的 DOM 結(jié)構(gòu)都被會(huì)移除,如下所示:
ReactDOM.render(null, document.getElementById("root"));
React 用白屏真正詮釋了什么叫唇寒齒亡,牽一發(fā)而動(dòng)全身,這也驗(yàn)證了我之前的說(shuō)法,現(xiàn)代 Web 應(yīng)用頻繁白屏和 SPA 框架逃不了干系。
但你能說(shuō)這個(gè)機(jī)制是負(fù)向優(yōu)化的嗎?官方說(shuō)法是:
我們對(duì)這一決定有過(guò)一些爭(zhēng)論,但根據(jù)我們的經(jīng)驗(yàn),把一個(gè)錯(cuò)誤的 UI 留在那比完全移除它要更糟糕。例如,在類似 Messenger 的產(chǎn)品中,把一個(gè)異常的 UI 展示給用戶可能會(huì)導(dǎo)致用戶將信息錯(cuò)發(fā)給別人。同樣,對(duì)于支付類應(yīng)用而言,顯示錯(cuò)誤的金額也比不呈現(xiàn)任何內(nèi)容更糟糕。
我越來(lái)越相信,前端層出不窮的框架或是新技術(shù),雖然它的 leverage 足夠大,但背后隱含著 trade-off,在絕大多數(shù)場(chǎng)景下表現(xiàn)優(yōu)異,在另一些場(chǎng)景下你也必須要接受它的“規(guī)則”。
為什么不能是 ? 屏、?? 屏?
既然 DOM 都被移除了,只剩下個(gè)光禿禿的 div#app
節(jié)點(diǎn),加上 body 的默認(rèn)背景顏色是 #FFF
,理所應(yīng)當(dāng)白屏。
<body> <div id="app"></div> <script src="/js/chunk-vendors.61a12961.js"></script> <script src="/js/app.3b315b6b.js"></script> </body>
因此,不僅黑屏、藍(lán)屏可以實(shí)現(xiàn),只要將 body 的背景顏色稍作調(diào)整,彩虹屏也可以實(shí)現(xiàn),彼時(shí)復(fù)盤文檔的標(biāo)題名為 「XXX 引發(fā)彩虹屏」,活成了前端喜劇人的樣子。
我認(rèn)為白屏只是一種代號(hào),引申的含義是頁(yè)面無(wú)內(nèi)容渲染。
我還想強(qiáng)調(diào),白屏只是一種外在表現(xiàn)形式,內(nèi)在錯(cuò)誤已經(jīng)發(fā)生,不可挽回,肯定會(huì)給用戶帶來(lái)功能上的影響,只不過(guò)白屏的視覺(jué)沖擊力最強(qiáng),大腦直覺(jué)反饋十分嚴(yán)重。
如何降低白屏的“破壞力”
不再贅述如何避免白屏,因?yàn)殄e(cuò)誤時(shí)時(shí)刻刻會(huì)發(fā)生,我們能做的是盡人事,遵循以下原則:
- 依賴不可信,npm 的 Breaking Change
- 調(diào)用不可信,HTTP/RPC 等 API 調(diào)用不僅會(huì)失敗,還會(huì)返回約定之外的數(shù)據(jù),不兼容過(guò)時(shí)版本
- 輸入不可信,用戶常常會(huì)輸入一些邊界值、非法值 能盡可能避免異常。
我們關(guān)注的是錯(cuò)誤已經(jīng)發(fā)生的窘境下,如何及時(shí)補(bǔ)救,把外在的不良表現(xiàn)弱化成用戶可以接受,或者無(wú)感知的狀態(tài)。
借助于 ErrorBoundary,它能捕獲任意子組件在渲染期間發(fā)生的 Uncaught Errors,從而避免整體組件樹(shù)的卸載,把白屏扼殺在搖籃中。
除此之外,它還能對(duì)渲染錯(cuò)誤的組件做兜底,具體的處理措施有兩種:熔斷和降級(jí)。
組件 “熔斷”
熔斷機(jī)制指的是在股票市場(chǎng)的交易時(shí)間中,當(dāng)價(jià)格波動(dòng)的幅度達(dá)到某一個(gè)限定的目標(biāo)(熔斷點(diǎn))時(shí),對(duì)其暫停交易一段時(shí)間的機(jī)制。 此機(jī)制如同保險(xiǎn)絲在電流過(guò)大時(shí)候熔斷,避免引發(fā)更大的事故,因此得名。
它被大量應(yīng)用于容災(zāi)體系,對(duì)應(yīng) React 體系中,熔斷點(diǎn)等同于渲染錯(cuò)誤發(fā)生,暫定交易等同于卸載組件,直接不渲染,舍車保帥。
直接看例子:
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> </> );
組件優(yōu)雅降級(jí)
優(yōu)雅降級(jí)指使用 替代渲染出錯(cuò)的組件,并做符合功能場(chǎng)景,用戶心智的提示。
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 所選擇的錯(cuò)誤邊界庫(kù)為 https://github.com/bvaughn/react-error-boundary,可在生產(chǎn)環(huán)境中投入使用。
前提是大家都要有對(duì)每個(gè)組件加上錯(cuò)誤邊界的共識(shí),配合團(tuán)隊(duì)內(nèi)部的監(jiān)控上報(bào)和 Lint 檢測(cè),才能最大限度降低白屏的“破壞力”,打造一個(gè)穩(wěn)定性更強(qiáng)的線上環(huán)境。
題外話:主動(dòng) throw error 導(dǎo)致白屏
我寧愿犯錯(cuò),也不愿什么也不做。
這一點(diǎn)我和 React Team 的觀點(diǎn)相同,與其展示錯(cuò)誤的 UI,不如不展示。
錯(cuò)誤的 UI,隨時(shí)是個(gè)定時(shí)炸彈,在特定情況下就會(huì)爆炸,試想用戶在錯(cuò)誤的界面進(jìn)行操作,小則造成 BUG,大則造成經(jīng)濟(jì)損失、安全泄露,會(huì)帶來(lái)不可損失的影響,所以遇到對(duì)于非預(yù)期的行為,一定要主動(dòng) throw error,并做好組件熔斷及降級(jí)。
以上就是JS前端白屏前世今生及解決方式的詳細(xì)內(nèi)容,更多關(guān)于JS前端白屏解決的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解
這篇文章主要為大家介紹了JavaScript復(fù)原何同學(xué)B站頭圖細(xì)節(jié)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07微信小程序中button組件的邊框設(shè)置的實(shí)例詳解
這篇文章主要介紹了微信小程序中button組件的邊框設(shè)置的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文大家能夠掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09深入講解JavaScript之繼承的多種方式和優(yōu)缺點(diǎn)
本文講主要解JavaScript各種繼承方式和優(yōu)缺點(diǎn),文章將六種繼承方式說(shuō)明,分別有原型鏈繼承、借用構(gòu)造函數(shù)(經(jīng)典繼承)、組合繼承、原型式繼承、寄生式繼承、 寄生組合式繼承,這六種方式,需要的朋友可以參考一下2021-10-10npm?start運(yùn)行項(xiàng)目過(guò)程package.json字段詳解
這篇文章主要為大家介紹了npm?start運(yùn)行項(xiàng)目過(guò)程package.json字段詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02JavaScript?Canvas實(shí)現(xiàn)兼容IE的兔子發(fā)射爆破動(dòng)圖特效
這篇文章主要為大家介紹了JavaScript?Canvas實(shí)現(xiàn)兼容IE的兔子發(fā)射爆破動(dòng)圖特效示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01手機(jī)瀏覽器 后退按鈕強(qiáng)制刷新頁(yè)面方法總結(jié)
這篇文章主要介紹了手機(jī)瀏覽器 后退按鈕強(qiáng)制刷新頁(yè)面方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2016-10-10微信小程序 轉(zhuǎn)發(fā)功能的實(shí)現(xiàn)
這篇文章主要介紹了微信小程序 轉(zhuǎn)發(fā)功能的實(shí)現(xiàn)的相關(guān)資料,這里提供實(shí)現(xiàn)方法及實(shí)例幫助大家學(xué)習(xí)理解,需要的朋友可以參考下2017-08-08詳解requestAnimationFrame和setInterval該如何選擇
這篇文章主要為大家介紹了requestAnimationFrame和setInterval該如何選擇示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2023-03-03微信小程序 高德地圖SDK詳解及簡(jiǎn)單實(shí)例(源碼下載)
這篇文章主要介紹了微信小程序 高德地圖詳解及簡(jiǎn)單實(shí)例(源碼下載)的相關(guān)資料,需要的朋友可以參考下2017-01-01