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