剖析前端異常及其降級處理和防范方案
點(diǎn)擊上方 前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
一、導(dǎo)讀
“異常”一詞出自《后漢書.卷一.皇后紀(jì)上.光烈陰皇后紀(jì)》,表示非正常的,不同于平常的。在我們現(xiàn)實(shí)生活中同樣處處存在著異常,比如小縣城里的路燈年久失修...,上下班高峰期深圳的地鐵總是那么的擁擠...,人也總是時(shí)不時(shí)會生病等等; 由此可見,這個(gè)世界錯(cuò)誤無處不在,這是一個(gè)基本的事實(shí)。
而在計(jì)算機(jī)的世界中,異常指的是在程序運(yùn)行過程中發(fā)生的異常事件,有些錯(cuò)誤是由于外部環(huán)境導(dǎo)致的,有些錯(cuò)誤是由于開發(fā)人員疏忽所導(dǎo)致的,有效的處理這些錯(cuò)誤,保證計(jì)算機(jī)世界的正常運(yùn)轉(zhuǎn)是我們開發(fā)人員必不可少的一環(huán)。
二、背景
隨著項(xiàng)目的不斷壯大,客戶的不斷接入,項(xiàng)目的穩(wěn)定性成為團(tuán)隊(duì)的一大挑戰(zhàn)。
當(dāng)用戶或者團(tuán)隊(duì)測試人員遇到問題時(shí),大概率是直接丟給開發(fā)人員一張白屏頁面或錯(cuò)誤UI的截圖,且該錯(cuò)誤并不是必現(xiàn)時(shí),讓前后端同學(xué)定位問題倍感頭痛。有沒有一種方式既能夠提升用戶體驗(yàn),又能夠幫助開發(fā)人員快速定位解決問題?
本著“客戶就是上帝”的商業(yè)準(zhǔn)則,為用戶創(chuàng)造良好的用戶體驗(yàn),是前端開發(fā)者職責(zé)之所在。當(dāng)頁面發(fā)生錯(cuò)誤的時(shí)候,相比于頁面崩潰或點(diǎn)不動,在適當(dāng)?shù)臅r(shí)機(jī),以一種適當(dāng)?shù)姆绞饺ヌ嵝延脩舢?dāng)前發(fā)生了什么,無疑是一種更友好的處理方式。
項(xiàng)目中面臨下面幾種異常場景,需要處理:
語法錯(cuò)誤 事件異常 HTTP請求異常 靜態(tài)資源加載異常 Promise 異常 Iframe 異常 頁面崩潰
整體異常處理方案需要實(shí)現(xiàn)二方面的效果:
提升用戶體驗(yàn) 上報(bào)監(jiān)控系統(tǒng),能及時(shí)早發(fā)現(xiàn)、定位、解決問題
下面我們先從幾個(gè)異常場景出發(fā),逐步探討如何解決這些異常并給予更好的用戶體驗(yàn)。
三、錯(cuò)誤類型
在探討具體的解決方案之前,我們先來認(rèn)識和熟悉一下前端的各種錯(cuò)誤類型。
ECMA-262規(guī)范定義的七種錯(cuò)誤類型:
Error EvalError RangeError ReferenceError SyntaxError TypeError URIError
Error
Error是所有錯(cuò)誤的基類,其他錯(cuò)誤都繼承自該類型
EvalError
EvalError對象表示全局函數(shù)eval()中發(fā)生的錯(cuò)誤。如果eval()中沒有錯(cuò)誤,則不會拋出該錯(cuò)誤。可以通過構(gòu)造函數(shù)創(chuàng)建這個(gè)對象的實(shí)例

RangeError
RangeError對象表示當(dāng)一個(gè)值不在允許值的集合或范圍內(nèi)時(shí)出現(xiàn)錯(cuò)誤。

ReferenceError
當(dāng)引用不存在的變量時(shí),該對象表示錯(cuò)誤:

SyntaxError
當(dāng)JavaScript引擎在解析代碼時(shí)遇到不符合該語言語法的標(biāo)記或標(biāo)記順序時(shí),將引發(fā)該異常:

TypeError
傳遞給函數(shù)的操作數(shù)或?qū)崊⑴c該操作符或函數(shù)期望的類型不兼容:

URIError
當(dāng)全局URI處理函數(shù)以錯(cuò)誤的方式使用時(shí):

四、處理和防范
上文我們提到錯(cuò)誤和異常無處不在,存在于各式各樣的應(yīng)用場景中,那我們應(yīng)該如何有效的攔截異常,將錯(cuò)誤扼殺于搖籃之中,讓用戶無感呢?亦或者遇到致命錯(cuò)誤時(shí),進(jìn)行降級處理?
(1) try catch
1.語法
ECMA-262 第 3 版中引入了 try-catch作為 JavaScript 中處理異常的一種標(biāo)準(zhǔn)方式,基本的語法如下所示。
try {
// 可能會導(dǎo)致錯(cuò)誤的代碼
} catch (error) {
// 在錯(cuò)誤發(fā)生時(shí)怎么處理
}
復(fù)制代碼
2.動機(jī)
使用try...catch來捕獲異常,我歸納起來主要有兩個(gè)動機(jī):
1)是真真正正地想對可能發(fā)生錯(cuò)誤的代碼進(jìn)行異常捕獲;
2)我想保證后面的代碼繼續(xù)運(yùn)行。
動機(jī)一沒什么好講的,在這里,我們講講動機(jī)二。假如我們有以下代碼:
console.log(foo); //foo未定義
console.log('I want running')
復(fù)制代碼
代碼一執(zhí)行,你猜怎么著?第一行語句報(bào)錯(cuò)了,第二行語句的log也就沒打印出來。如果我們把代碼改成這樣:
try{
console.log(foo)
}catch(e){
console.log(e)
}
console.log('I want running');
復(fù)制代碼
以上代碼執(zhí)行之后,雖然還是報(bào)了個(gè)ReferenceError錯(cuò)誤,但是后面的log卻能夠被執(zhí)行。
從這個(gè)示例,我們可以看出,一旦前面的(同步)代碼出現(xiàn)了沒有被開發(fā)者捕獲的異常的話,那么后面的代碼就不會執(zhí)行了。所以,如果你希望當(dāng)前可能出錯(cuò)的代碼塊后續(xù)的代碼能夠正常運(yùn)行的話,那么你就得使用try...catch來主動捕獲異常。
擴(kuò)展:
實(shí)際上,出錯(cuò)代碼是如何干擾后續(xù)代碼的執(zhí)行,是一個(gè)值得探討的主題。下面進(jìn)行具體的探討。因?yàn)槭忻嫔蠟g覽器眾多,對標(biāo)準(zhǔn)的實(shí)現(xiàn)也不太一致。所以,這里的結(jié)論僅僅是基于Chromev91.0.4472.114。探討過程中,我們涉及到兩組概念:同步代碼與異步代碼,代碼書寫期和代碼運(yùn)行期。
場景一:同步代碼(出錯(cuò)) + 同步代碼

可以看到,出錯(cuò)的同步代碼后面的同步代碼不執(zhí)行了。
場景二:同步代碼(出錯(cuò)) + 異步代碼

跟上面的情況一下,異步代碼也受到影響,也不執(zhí)行了。
場景三:異步代碼(出錯(cuò)) + 同步代碼

可以看到,異步代碼出錯(cuò),并不會影響后面同步代碼的執(zhí)行。
場景四:異步代碼(出錯(cuò)) + 異步代碼

出錯(cuò)的異步代碼也不會影響后面異步代碼的執(zhí)行。
如果只看場景一二三,很容易得出如下結(jié)論:在代碼運(yùn)行期,同步代碼始終是先于異步代碼執(zhí)行的。如果先執(zhí)行的同步代碼沒有出錯(cuò)的話,那么后面的代碼就會正常執(zhí)行,否則后面的代碼就不會執(zhí)行。但場景四卻打破了這個(gè)結(jié)論。我們不妨繼續(xù)看看場景五。
場景五:異步代碼 + 同步代碼(出錯(cuò)) + 異步代碼

看到了沒?同樣是異步代碼,按理說,代碼運(yùn)行期,如果你是受出錯(cuò)的同步代碼的影響的話,那你要么是兩個(gè)都不執(zhí)行,或者兩個(gè)都執(zhí)行啊?憑什么寫在出錯(cuò)代碼代碼書寫期前面的異步代碼就能正常執(zhí)行,而寫在后面的就不執(zhí)行呢?經(jīng)過驗(yàn)證,在firefoxv75.0版本中也是同樣的表現(xiàn)。
所以,到了這里,我們基本上可以得出這樣的結(jié)論:運(yùn)行期,一先一后的兩個(gè)代碼中,出錯(cuò)的一方代碼是如何影響另外一方代碼繼續(xù)執(zhí)行的問題中,跟異步代碼沒關(guān)系,只跟同步代碼有關(guān)系;跟代碼執(zhí)行期沒關(guān)系,只跟代碼書寫期有關(guān)系。
說人話就是,異步代碼出錯(cuò)與否都不會影響其他代碼繼續(xù)執(zhí)行。而出錯(cuò)的同步代碼,如果它在代碼書寫期是寫在其他代碼之前,并且我們并沒有對它進(jìn)行手動地去異常捕獲的話,那么它就會影響其他代碼(不論它是同步還是異步代碼)的繼續(xù)執(zhí)行。
綜上所述,如果我們想要保證某塊可能出錯(cuò)的同步代碼后面的代碼繼續(xù)執(zhí)行的話,那么我們必須對這塊同步代碼進(jìn)行異常捕獲。
3.范圍
只能捕獲同步代碼所產(chǎn)生的運(yùn)行時(shí)錯(cuò)誤,對于語法錯(cuò)誤和異步代碼所產(chǎn)生的錯(cuò)誤是無能為力的。
當(dāng)遇到語法錯(cuò)誤時(shí):
當(dāng)遇到異步運(yùn)行時(shí)錯(cuò)誤時(shí):
(2) Promise.catch()
1.語法
const promise1 = new Promise((resolve, reject) => {
throw 'Uh-oh!';
});
promise1.catch((error) => {
console.error(error);
});
// expected output: Uh-oh!
復(fù)制代碼
2.動機(jī)
用來捕獲promise代碼中的錯(cuò)誤
3.范圍
使用Promise.prototype.catch()我們可以方便的捕獲到異常,現(xiàn)在我們來測試一下常見的語法錯(cuò)誤、代碼錯(cuò)誤以及異步錯(cuò)誤。
當(dāng)遇到代碼錯(cuò)誤時(shí),可以捕獲:
當(dāng)遇到語法錯(cuò)誤時(shí),不能捕獲:
當(dāng)遇到異步運(yùn)行時(shí)錯(cuò)誤時(shí),不能捕獲:

(3) unhandledrejection
1.用法
unhandledrejection:當(dāng)Promise 被 reject 且沒有 reject 處理器的時(shí)候,會觸發(fā) unhandledrejection 事件
window.addEventListener("unhandledrejection", function(e){
console.log(e);
});
復(fù)制代碼
2.動機(jī)
為了防止有漏掉的 Promise 異常,可以在全局增加一個(gè)對 unhandledrejection 的監(jiān)聽進(jìn)行兜底,用來全局監(jiān)聽Uncaught Promise Error。
3.范圍
window.addEventListener("unhandledrejection", function (e) {
console.log("捕獲到的promise異常:", e);
e.preventDefault();
});
new Promise((res) => {
console.log(a);
});
// 捕獲到的promise異常的: PromiseRejectionEvent
復(fù)制代碼
注意:此段代碼直接寫在控制臺是捕獲不到promise異常的,寫在html文件中可正常捕獲。
(4) window.onerror
1.用法
當(dāng) JS 運(yùn)行時(shí)錯(cuò)誤發(fā)生時(shí),window 會觸發(fā)一個(gè) ErrorEvent 接口的 error 事件,并執(zhí)行 window.onerror()。
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕獲到異常:',{message, source, lineno, colno, error});
}
復(fù)制代碼
2.動機(jī)
眾所周知,很多做錯(cuò)誤監(jiān)控和上報(bào)的類庫就是基于這個(gè)特性來實(shí)現(xiàn)的,我們期待它能處理那些try...catch不能處理的錯(cuò)誤。
3.范圍
根據(jù)MDN的說法,wondow.onerror能捕獲JavaScript運(yùn)行時(shí)錯(cuò)誤(包括語法錯(cuò)誤)或一些資源錯(cuò)誤。而在真正的測試過程中,wondow.onerror并不能捕獲語法錯(cuò)誤。

經(jīng)測試,window.onerror并不能捕獲語法錯(cuò)誤和靜態(tài)資源的加載錯(cuò)誤。同樣也不能捕獲異步代碼的錯(cuò)誤,但是有一點(diǎn)值得注意的是,window.onerror能捕獲同樣是異步代碼的setTimeout和setInterval里面的錯(cuò)誤。
看來,寄予厚望的window.onerror并不是萬能的。
(5) window.addEventListener
1.用法
window.addEventListener('error',(error)=>{console.log(error)})
復(fù)制代碼
2.動機(jī)
當(dāng)然是希望用他來兜住window.onerror和try catch的底,希望他能捕獲到異步錯(cuò)誤和資源的加載錯(cuò)誤。
3.范圍
<body>
<img id="img" src="./fake.png" />
<iframe id="iframe" src="./test4.html"></iframe>
</body>
<script>
window.addEventListener(
"error",
function (error) {
console.log(error, "error");
},
true
);
setTimeout(() => {
console.log(a);
});
new Promise((resolve, reject) => {
console.log(a);
});
console.log(b)
var f=e, //語法異常
</script>
復(fù)制代碼
在此過程中,資源文件都是不存在的,我們發(fā)現(xiàn)window.addEventListener('error')依舊不能捕獲語法錯(cuò)誤,Promise異常和iframe異常。
對于語法錯(cuò)誤我們可以在編譯過程中捕獲,,Promise異常已在上文中給出解決方案,現(xiàn)在還剩下iframe異常需要單獨(dú)處理了。
(5) iframe異常
1.用法
window.frames[0].onerror = function (message, source, lineno, colno, error) {
console.log('捕獲到 iframe 異常:',{message, source, lineno, colno, error});
return true;
};
復(fù)制代碼
2.動機(jī)
用來專門捕獲iframe加載過程中的異常。
3.范圍
很遺憾,結(jié)果并不令人滿意,在實(shí)際的測試過程中,該方法未能捕獲到異常。
(6) React中捕獲異常
部分 UI 的 JavaScript 錯(cuò)誤不應(yīng)該導(dǎo)致整個(gè)應(yīng)用崩潰,為了解決這個(gè)問題,React 16 引入了一個(gè)新的概念 —— 錯(cuò)誤邊界。
錯(cuò)誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹任何位置的 JavaScript 錯(cuò)誤,并且,它會渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯(cuò)誤邊界在渲染期間、生命周期方法和整個(gè)組件樹的構(gòu)造函數(shù)中捕獲錯(cuò)誤。
注意:錯(cuò)誤邊界無法捕獲以下場景中產(chǎn)生的錯(cuò)誤
事件處理 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調(diào)函數(shù)) 服務(wù)端渲染 它自身拋出來的錯(cuò)誤(并非它的子組件)
如果一個(gè) class 組件中定義了 static getDerivedStateFromError() 或 componentDidCatch() 這兩個(gè)生命周期方法中的任意一個(gè)(或兩個(gè))時(shí),那么它就變成一個(gè)錯(cuò)誤邊界。當(dāng)拋出錯(cuò)誤后,請使用 static getDerivedStateFromError() 渲染備用 UI ,使用 componentDidCatch() 打印錯(cuò)誤信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能夠顯示降級后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同樣可以將錯(cuò)誤日志上報(bào)給服務(wù)器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定義降級后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
復(fù)制代碼
錯(cuò)誤邊界的工作方式類似于 JavaScript 的 catch {},不同的地方在于錯(cuò)誤邊界只針對 React 組件。只有 class 組件才可以成為錯(cuò)誤邊界組件。大多數(shù)情況下, 你只需要聲明一次錯(cuò)誤邊界組件, 并在整個(gè)應(yīng)用中使用它。
以上引用自React 官網(wǎng)。
(7) Vue中捕獲異常
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的錯(cuò)誤信息,比如錯(cuò)誤所在的生命周期鉤子
// 只在 2.2.0+ 可用
}
復(fù)制代碼
指定組件的渲染和觀察期間未捕獲錯(cuò)誤的處理函數(shù)。這個(gè)處理函數(shù)被調(diào)用時(shí),可獲取錯(cuò)誤信息和 Vue 實(shí)例。
從 2.2.0 起,這個(gè)鉤子也會捕獲組件生命周期鉤子里的錯(cuò)誤。同樣的,當(dāng)這個(gè)鉤子是 undefined 時(shí),被捕獲的錯(cuò)誤會通過 console.error 輸出而避免應(yīng)用崩潰。 從 2.4.0 起,這個(gè)鉤子也會捕獲 Vue 自定義事件處理函數(shù)內(nèi)部的錯(cuò)誤了。 從 2.6.0 起,這個(gè)鉤子也會捕獲 v-on DOM 監(jiān)聽器內(nèi)部拋出的錯(cuò)誤。另外,如果任何被覆蓋的鉤子或處理函數(shù)返回一個(gè) Promise 鏈 (例如 async 函數(shù)),則來自其 Promise 鏈的錯(cuò)誤也會被處理。
以上引用自Vue 官網(wǎng)。
(8) http請求異常
1.用法
以axios為例,添加響應(yīng)攔截器
axios.interceptors.response.use(function (response) {
// 對響應(yīng)數(shù)據(jù)做點(diǎn)什么
// response 是請求回來的數(shù)據(jù)
return response;
}, function (error) {
// 對響應(yīng)錯(cuò)誤做點(diǎn)什么
return Promise.reject(error)
}
)
復(fù)制代碼
2.動機(jī)
用來專門捕獲HTTP請求異常
五、項(xiàng)目實(shí)踐
在提出了這么多的解決方案之后,相信大家對具體怎么用還是存在一些疑惑。那么接下來,我們真正的進(jìn)入實(shí)踐階段吧!
我們再次回顧一下我們需要解決的問題是什么?
語法錯(cuò)誤 事件異常 HTTP請求異常 靜態(tài)資源加載異常 Promise 異常 Iframe 異常 頁面崩潰
捕獲異常是我們的最終目標(biāo)嗎?并不是,回到解決問題的背景下,相比于頁面崩潰或點(diǎn)不動,在適當(dāng)?shù)臅r(shí)機(jī),以一種適當(dāng)?shù)姆绞饺ヌ嵝延脩舢?dāng)前發(fā)生了什么,無疑是一種更友好的處理方式。
結(jié)合到項(xiàng)目中,具體實(shí)踐起來有如下兩種方案:
1.代碼中通過大量的try catch/Promise.catch來捕獲,捕獲不到的使用其他方式進(jìn)行兜底 2.通過框架提供的機(jī)制來做,再對不能捕獲的進(jìn)行兜底
方案一無疑不是很聰明的樣子...這意味著要去改大量的原有代碼,心智負(fù)擔(dān)成倍數(shù)增加。方案二則更加明智,通過在底層對錯(cuò)誤進(jìn)行統(tǒng)一處理,無需變更原有邏輯。
到項(xiàng)目中,使用的是React框架,React正好提供了一種捕獲異常的機(jī)制(上文已提及)并做降級處理,但是細(xì)心的小伙伴發(fā)現(xiàn)了,react并不能捕獲如下四種錯(cuò)誤:
事件處理 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調(diào)函數(shù)) 服務(wù)端渲染 它自身拋出來的錯(cuò)誤(并非它的子組件)
對于第三點(diǎn)服務(wù)端渲染錯(cuò)誤,項(xiàng)目中并沒有適用的場景,此次不做重點(diǎn)分析。我們重點(diǎn)分析第一點(diǎn)和第二點(diǎn)。
我在這里先拋出幾個(gè)問題,大家先做短暫的思考:
1.若事件處理和異步代碼的錯(cuò)誤導(dǎo)致頁面crash,我們該如何預(yù)防? 2.如何對ErrorBounary進(jìn)行兜底?相比一個(gè)按鈕點(diǎn)擊無效,如何更加友好的提示用戶?
先來看第一個(gè)問題,若事件處理和異步代碼的錯(cuò)誤導(dǎo)致頁面崩潰:
const Test = () => {
const [data, setData] = useState([]);
return (
<div
onClick={() => {
setData('');
}}
>
{data.map((s) => s.i)}
</div>
);
};
復(fù)制代碼
此段代碼在正常渲染期間是沒問題的,但在觸發(fā)了點(diǎn)擊事件之后會導(dǎo)致頁面異常白屏,如果在外面套上我們的ErrorBounday組件,情況會是怎么樣呢?
答案是依然能夠捕獲到錯(cuò)誤,并能夠?qū)υ摻M件進(jìn)行降級處理!
此時(shí)有些小伙伴已經(jīng)察覺到了,錯(cuò)誤邊界只要是在渲染期間都是可以捕獲錯(cuò)誤的,無論首次渲染還是二次渲染。流程圖如下:

第一個(gè)問題原來根本就不是問題,這本身就是一個(gè)閉環(huán),不用我們解決!
再來看看第二個(gè)問題:
對于事件處理和異步代碼中不會導(dǎo)致頁面崩潰的代碼:
const Test = () => {
return (
<button
onClick={() => {
[].map((s) => s.a.b);
}}
>
點(diǎn)擊
</button>
);
};
復(fù)制代碼
button按鈕可正常點(diǎn)擊,但是該點(diǎn)擊事件的內(nèi)部邏輯是有問題的,導(dǎo)致用戶點(diǎn)擊該按鈕本質(zhì)是無效的。此時(shí)若不及時(shí)給與友好提示,用戶只會陷入抓狂中....
那么有沒有辦法對ErrorBoundary進(jìn)行兜底呢?即可以捕獲異步代碼或事件處理中的錯(cuò)誤。
上文提到的window.addEventListener('error')正好可以解決這個(gè)問題。理想狀態(tài)下:
而真正的執(zhí)行順序確實(shí)這樣的:

在真正執(zhí)行的過程中,window.addEventListener('error')是先于ErrorBoundary捕獲到錯(cuò)誤的,這就導(dǎo)致當(dāng)error事件捕獲到錯(cuò)誤時(shí),他并不知道該錯(cuò)誤是否會導(dǎo)致頁面崩潰,不知道該給予怎樣的提示,到底是對頁面進(jìn)行降級處理還是只做簡單的報(bào)錯(cuò)提示?
問題似乎就卡在這了....
那能否通過一種有效的途徑告訴error事件:ErrorBoundary已經(jīng)捕獲到了錯(cuò)誤,你不需要處理!亦或者是ErrorBoundary未能捕獲到錯(cuò)誤,這是一個(gè)異步錯(cuò)誤/事件錯(cuò)誤,但不會引起頁面崩潰,你只需要提示用戶!
答案肯定是有的,比如建立一個(gè)nodeJs服務(wù)器,通過webSocket去通知,但是這樣做不僅麻煩,還會有一定的延遲。
在筆者苦思冥想之際,在某個(gè)靜悄悄的夜晚,突然靈感一現(xiàn)。為什么我們非要按照他規(guī)定的順序執(zhí)行呢?我們能不能嘗試改變他的執(zhí)行順序,讓錯(cuò)誤捕獲回到我們理想中的流程來呢?
改變思路之后,我們再思考有什么能改變代碼執(zhí)行順序嗎?沒錯(cuò),異步事件!
window.addEventListener('error', function (error) {
setTimeout(()=>{
console.log(error, 'error錯(cuò)誤');
})
});
復(fù)制代碼
當(dāng)給error事件的回調(diào)函數(shù)加入setTimeout后,捕獲異常的流程為:

現(xiàn)在就可以通知error事件到底頁面崩潰了沒有,到底需不需要它的處理!上代碼:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能夠顯示降級后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同樣可以將錯(cuò)誤日志上報(bào)給服務(wù)
logErrorToMyService(error, errorInfo);
//告訴error事件 ErrorBoundary已處理異常
localStorage.setItem("ErrorBoundary",true)
}
render() {
if (this.state.hasError) {
// 你可以自定義降級后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
復(fù)制代碼
window.addEventListener('error', function (error) {
setTimeout(() => {
//進(jìn)來代表一定有錯(cuò)誤 判斷ErrorBoundary中是否已處理異常
const flag = localStorage.getItem('ErrorBounary');
if (flag) {
//進(jìn)入了ErrorBounary 錯(cuò)誤已被處理 error事件不用處理該異常
localStorage.setItem('ErrorBounary', false); //重置狀態(tài)
} else {
//未進(jìn)入ErrorBounary 代表此錯(cuò)誤為異步錯(cuò)誤/事件錯(cuò)誤
logErrorToMyService(error, errorInfo); // 你可以將錯(cuò)誤日志上報(bào)給服務(wù)
//判斷具體錯(cuò)誤類型
if (error.message.indexOf('TypeError')) {
alert('這是一個(gè)TypeError錯(cuò)誤,請通知開發(fā)人員');
} else if (error.message.indexOf('SyntaxError')) {
alert('這是一個(gè)SyntaxError錯(cuò)誤,請通知開發(fā)人員');
} else {
//在此次給與友好提示
}
}
});
});
復(fù)制代碼
最后,通過我們的努力,當(dāng)頁面崩潰時(shí),及時(shí)進(jìn)行降級處理;當(dāng)頁面未崩潰,但有錯(cuò)誤時(shí),我們及時(shí)的告知用戶,并對錯(cuò)誤進(jìn)行上報(bào),達(dá)到預(yù)期的效果。
六、擴(kuò)展
1.設(shè)置采集率
若是錯(cuò)誤實(shí)在太多,比如有時(shí)候代碼進(jìn)入死循環(huán),錯(cuò)誤量過多導(dǎo)致服務(wù)器壓力大時(shí),可酌情降低采集率。比如采集30%:
if (Math.random() < 0.3) {
//上報(bào)錯(cuò)誤
logErrorToMyService(error, errorInfo);
}
復(fù)制代碼
2.提效
解決上面這些問題后,大家難免會有疑問:那每一個(gè)組件都要去套一層ErrorBoundary組件,這工作量是不是有點(diǎn)大....而且有一些老代碼,嵌套的比較深,改起來心理負(fù)擔(dān)也會比較大。那有沒有辦法將其作為一個(gè)配置項(xiàng),配置完之后,編譯時(shí)自動套上一層ErrorBoundary組件呢?這個(gè)我們下次在做探討!
3.可配置
能否將ErrorBoundary擴(kuò)展成可傳入自定義UI的組件呢?這樣大家通過定制化UI,在不同的場景進(jìn)行不同的降級處理。
同樣,這一塊我們下次再討論!
七、總結(jié)
異常處理是高質(zhì)量軟件開發(fā)中的一個(gè)基本部分,但是在許多情況下,它們會被忽略,或者是不正確的使用,而處理異常只是保證代碼流程不出錯(cuò),重定向到正確的程序流中去。
本文從前端錯(cuò)誤類型出發(fā),從try catch逐步揭開錯(cuò)誤異常神秘的面紗,再通過一系列的操作對異常進(jìn)行監(jiān)控和捕獲,最后達(dá)到提升用戶體驗(yàn),上報(bào)監(jiān)控系統(tǒng)的效果。
八、思考
Promise.catch 和 try catch 捕獲異常有什么區(qū)別? ErrorBounary內(nèi)部如何實(shí)現(xiàn)? 為什么unhandledrejection寫在控制臺是捕獲不到錯(cuò)誤的?而寫在HTML文件中就可以捕獲到? 服務(wù)端渲染錯(cuò)誤如何捕獲?
帶著這些思考,我們下次見~
關(guān)于本文
來源:縱有疾風(fēng)起
https://juejin.cn/post/6979564690787532814
內(nèi)推社群
我組建了一個(gè)氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時(shí)候隨時(shí)幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。
