<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          剖析前端異常及降級(jí)處理

          共 16379字,需瀏覽 33分鐘

           ·

          2021-07-29 12:26

          點(diǎn)擊上方關(guān)注 前端技術(shù)江湖一起學(xué)習(xí),天天進(jìn)步


          一、導(dǎo)讀

          “異常”一詞出自《后漢書(shū).卷一.皇后紀(jì)上.光烈陰皇后紀(jì)》,表示非正常的,不同于平常的。在我們現(xiàn)實(shí)生活中同樣處處存在著異常,比如小縣城里的路燈年久失修...,上下班高峰期深圳的地鐵總是那么的擁擠...,人也總是時(shí)不時(shí)會(huì)生病等等; 由此可見(jiàn),這個(gè)世界錯(cuò)誤無(wú)處不在,這是一個(gè)基本的事實(shí)。

          而在計(jì)算機(jī)的世界中,異常指的是在程序運(yùn)行過(guò)程中發(fā)生的異常事件,有些錯(cuò)誤是由于外部環(huán)境導(dǎo)致的,有些錯(cuò)誤是由于開(kāi)發(fā)人員疏忽所導(dǎo)致的,有效的處理這些錯(cuò)誤,保證計(jì)算機(jī)世界的正常運(yùn)轉(zhuǎn)是我們開(kāi)發(fā)人員必不可少的一環(huán)。

          二、背景

          隨著項(xiàng)目的不斷壯大,客戶的不斷接入,項(xiàng)目的穩(wěn)定性成為團(tuán)隊(duì)的一大挑戰(zhàn)。

          當(dāng)用戶或者團(tuán)隊(duì)測(cè)試人員遇到問(wèn)題時(shí),大概率是直接丟給開(kāi)發(fā)人員一張白屏頁(yè)面或錯(cuò)誤UI的截圖,且該錯(cuò)誤并不是必現(xiàn)時(shí),讓前后端同學(xué)定位問(wèn)題倍感頭痛。有沒(méi)有一種方式既能夠提升用戶體驗(yàn),又能夠幫助開(kāi)發(fā)人員快速定位解決問(wèn)題?

          本著“客戶就是上帝”的商業(yè)準(zhǔn)則,為用戶創(chuàng)造良好的用戶體驗(yàn),是前端開(kāi)發(fā)者職責(zé)之所在。當(dāng)頁(yè)面發(fā)生錯(cuò)誤的時(shí)候,相比于頁(yè)面崩潰或點(diǎn)不動(dòng),在適當(dāng)?shù)臅r(shí)機(jī),以一種適當(dāng)?shù)姆绞饺ヌ嵝延脩舢?dāng)前發(fā)生了什么,無(wú)疑是一種更友好的處理方式。

          項(xiàng)目中面臨下面幾種異常場(chǎng)景,需要處理:

          • 語(yǔ)法錯(cuò)誤
          • 事件異常
          • HTTP請(qǐng)求異常
          • 靜態(tài)資源加載異常
          • Promise 異常
          • Iframe 異常
          • 頁(yè)面崩潰

          整體異常處理方案需要實(shí)現(xiàn)二方面的效果:

          1. 提升用戶體驗(yàn)
          2. 上報(bào)監(jiān)控系統(tǒng),能及時(shí)早發(fā)現(xiàn)、定位、解決問(wèn)題

          下面我們先從幾個(gè)異常場(chǎng)景出發(fā),逐步探討如何解決這些異常并給予更好的用戶體驗(yàn)。

          三、錯(cuò)誤類型

          在探討具體的解決方案之前,我們先來(lái)認(rèn)識(shí)和熟悉一下前端的各種錯(cuò)誤類型。

          ECMA-262規(guī)范定義的七種錯(cuò)誤類型:

          • Error
          • EvalError
          • RangeError
          • ReferenceError
          • SyntaxError
          • TypeError
          • URIError

          Error

          Error是所有錯(cuò)誤的基類,其他錯(cuò)誤都繼承自該類型

          EvalError

          EvalError對(duì)象表示全局函數(shù)eval()中發(fā)生的錯(cuò)誤。如果eval()中沒(méi)有錯(cuò)誤,則不會(huì)拋出該錯(cuò)誤。可以通過(guò)構(gòu)造函數(shù)創(chuàng)建這個(gè)對(duì)象的實(shí)例

          image.png

          RangeError

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

          image.png

          ReferenceError

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

          image.png

          SyntaxError

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

          image.png

          TypeError

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

          image.png

          URIError

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

          image.png

          四、處理和防范

          上文我們提到錯(cuò)誤和異常無(wú)處不在,存在于各式各樣的應(yīng)用場(chǎng)景中,那我們應(yīng)該如何有效的攔截異常,將錯(cuò)誤扼殺于搖籃之中,讓用戶無(wú)感呢?亦或者遇到致命錯(cuò)誤時(shí),進(jìn)行降級(jí)處理?

          (1) try catch

          1.語(yǔ)法

          ECMA-262 第 3 版中引入了 try-catch作為 JavaScript 中處理異常的一種標(biāo)準(zhǔn)方式,基本的語(yǔ)法如下所示。

          try {
            // 可能會(huì)導(dǎo)致錯(cuò)誤的代碼
          } catch (error) {
            // 在錯(cuò)誤發(fā)生時(shí)怎么處理
          }
          復(fù)制代碼

          2.動(dòng)機(jī)

          使用try...catch來(lái)捕獲異常,我歸納起來(lái)主要有兩個(gè)動(dòng)機(jī):

          1)是真真正正地想對(duì)可能發(fā)生錯(cuò)誤的代碼進(jìn)行異常捕獲;

          2)我想保證后面的代碼繼續(xù)運(yùn)行。

          動(dòng)機(jī)一沒(méi)什么好講的,在這里,我們講講動(dòng)機(jī)二。假如我們有以下代碼:

          console.log(foo); //foo未定義
          console.log('I want running')
          復(fù)制代碼

          代碼一執(zhí)行,你猜怎么著?第一行語(yǔ)句報(bào)錯(cuò)了,第二行語(yǔ)句的log也就沒(méi)打印出來(lái)。如果我們把代碼改成這樣:

          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)了沒(méi)有被開(kāi)發(fā)者捕獲的異常的話,那么后面的代碼就不會(huì)執(zhí)行了。所以,如果你希望當(dāng)前可能出錯(cuò)的代碼塊后續(xù)的代碼能夠正常運(yùn)行的話,那么你就得使用try...catch來(lái)主動(dòng)捕獲異常。

          擴(kuò)展:

          實(shí)際上,出錯(cuò)代碼是如何干擾后續(xù)代碼的執(zhí)行,是一個(gè)值得探討的主題。下面進(jìn)行具體的探討。因?yàn)槭忻嫔蠟g覽器眾多,對(duì)標(biāo)準(zhǔn)的實(shí)現(xiàn)也不太一致。所以,這里的結(jié)論僅僅是基于Chromev91.0.4472.114。探討過(guò)程中,我們涉及到兩組概念:同步代碼與異步代碼,代碼書(shū)寫(xiě)期和代碼運(yùn)行期。

          場(chǎng)景一:同步代碼(出錯(cuò)) + 同步代碼

          1625024247(1).png

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

          場(chǎng)景二:同步代碼(出錯(cuò)) + 異步代碼

          1625024396(1).png

          跟上面的情況一下,異步代碼也受到影響,也不執(zhí)行了。

          場(chǎng)景三:異步代碼(出錯(cuò)) + 同步代碼

          image.png

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

          場(chǎng)景四:異步代碼(出錯(cuò)) + 異步代碼

          image.png

          出錯(cuò)的異步代碼也不會(huì)影響后面異步代碼的執(zhí)行。

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

          場(chǎng)景五:異步代碼 + 同步代碼(出錯(cuò)) + 異步代碼

          image.png

          看到了沒(méi)?同樣是異步代碼,按理說(shuō),代碼運(yùn)行期,如果你是受出錯(cuò)的同步代碼的影響的話,那你要么是兩個(gè)都不執(zhí)行,或者兩個(gè)都執(zhí)行啊?憑什么寫(xiě)在出錯(cuò)代碼代碼書(shū)寫(xiě)期前面的異步代碼就能正常執(zhí)行,而寫(xiě)在后面的就不執(zhí)行呢?經(jīng)過(guò)驗(yàn)證,在firefoxv75.0版本中也是同樣的表現(xiàn)。

          所以,到了這里,我們基本上可以得出這樣的結(jié)論:運(yùn)行期,一先一后的兩個(gè)代碼中,出錯(cuò)的一方代碼是如何影響另外一方代碼繼續(xù)執(zhí)行的問(wèn)題中,跟異步代碼沒(méi)關(guān)系,只跟同步代碼有關(guān)系;跟代碼執(zhí)行期沒(méi)關(guān)系,只跟代碼書(shū)寫(xiě)期有關(guān)系。

          說(shuō)人話就是,異步代碼出錯(cuò)與否都不會(huì)影響其他代碼繼續(xù)執(zhí)行。而出錯(cuò)的同步代碼,如果它在代碼書(shū)寫(xiě)期是寫(xiě)在其他代碼之前,并且我們并沒(méi)有對(duì)它進(jìn)行手動(dòng)地去異常捕獲的話,那么它就會(huì)影響其他代碼(不論它是同步還是異步代碼)的繼續(xù)執(zhí)行。

          綜上所述,如果我們想要保證某塊可能出錯(cuò)的同步代碼后面的代碼繼續(xù)執(zhí)行的話,那么我們必須對(duì)這塊同步代碼進(jìn)行異常捕獲。

          3.范圍

          只能捕獲同步代碼所產(chǎn)生的運(yùn)行時(shí)錯(cuò)誤,對(duì)于語(yǔ)法錯(cuò)誤和異步代碼所產(chǎn)生的錯(cuò)誤是無(wú)能為力的。

          當(dāng)遇到語(yǔ)法錯(cuò)誤時(shí):

          當(dāng)遇到異步運(yùn)行時(shí)錯(cuò)誤時(shí):

          (2) Promise.catch()

          1.語(yǔ)法

          const promise1 = new Promise((resolve, reject) => {
            throw 'Uh-oh!';
          });

          promise1.catch((error) => {
            console.error(error);
          });
          // expected output: Uh-oh!
          復(fù)制代碼

          2.動(dòng)機(jī)

          用來(lái)捕獲promise代碼中的錯(cuò)誤

          3.范圍

          使用Promise.prototype.catch()我們可以方便的捕獲到異常,現(xiàn)在我們來(lái)測(cè)試一下常見(jiàn)的語(yǔ)法錯(cuò)誤、代碼錯(cuò)誤以及異步錯(cuò)誤。

          當(dāng)遇到代碼錯(cuò)誤時(shí),可以捕獲:

          當(dāng)遇到語(yǔ)法錯(cuò)誤時(shí),不能捕獲:

          當(dāng)遇到異步運(yùn)行時(shí)錯(cuò)誤時(shí),不能捕獲:

          1625033576(1).png

          (3) unhandledrejection

          1.用法

          unhandledrejection:當(dāng)Promise 被 reject 且沒(méi)有 reject 處理器的時(shí)候,會(huì)觸發(fā) unhandledrejection 事件

          window.addEventListener("unhandledrejection"function(e){
            console.log(e);
          });
          復(fù)制代碼

          2.動(dòng)機(jī)

          為了防止有漏掉的 Promise 異常,可以在全局增加一個(gè)對(duì) unhandledrejection 的監(jiān)聽(tīng)進(jìn)行兜底,用來(lái)全局監(jiān)聽(tīng)Uncaught Promise Error。

          3.范圍

              window.addEventListener("unhandledrejection"function (e) {
                console.log("捕獲到的promise異常:", e);
                e.preventDefault();
              });
              new Promise((res) => {
                console.log(a);
              });
              // 捕獲到的promise異常的: PromiseRejectionEvent
          復(fù)制代碼

          注意:此段代碼直接寫(xiě)在控制臺(tái)是捕獲不到promise異常的,寫(xiě)在html文件中可正常捕獲。

          (4) window.onerror

          1.用法

          當(dāng) JS 運(yùn)行時(shí)錯(cuò)誤發(fā)生時(shí),window 會(huì)觸發(fā)一個(gè) ErrorEvent 接口的 error 事件,并執(zhí)行 window.onerror()。

          window.onerror = function(message, source, lineno, colno, error) {
             console.log('捕獲到異常:',{message, source, lineno, colno, error});
          }
          復(fù)制代碼

          2.動(dòng)機(jī)

          眾所周知,很多做錯(cuò)誤監(jiān)控和上報(bào)的類庫(kù)就是基于這個(gè)特性來(lái)實(shí)現(xiàn)的,我們期待它能處理那些try...catch不能處理的錯(cuò)誤。

          3.范圍

          根據(jù)MDN的說(shuō)法,wondow.onerror能捕獲JavaScript運(yùn)行時(shí)錯(cuò)誤(包括語(yǔ)法錯(cuò)誤)或一些資源錯(cuò)誤。而在真正的測(cè)試過(guò)程中,wondow.onerror并不能捕獲語(yǔ)法錯(cuò)誤。

          image.png

          經(jīng)測(cè)試,window.onerror并不能捕獲語(yǔ)法錯(cuò)誤和靜態(tài)資源的加載錯(cuò)誤。同樣也不能捕獲異步代碼的錯(cuò)誤,但是有一點(diǎn)值得注意的是,window.onerror能捕獲同樣是異步代碼的setTimeout和setInterval里面的錯(cuò)誤。

          看來(lái),寄予厚望的window.onerror并不是萬(wàn)能的。

          (5) window.addEventListener

          1.用法

          window.addEventListener('error',(error)=>{console.log(error)})
          復(fù)制代碼

          2.動(dòng)機(jī)

          當(dāng)然是希望用他來(lái)兜住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, //語(yǔ)法異常
            </script>
          復(fù)制代碼

          在此過(guò)程中,資源文件都是不存在的,我們發(fā)現(xiàn)window.addEventListener('error')依舊不能捕獲語(yǔ)法錯(cuò)誤,Promise異常和iframe異常。

          對(duì)于語(yǔ)法錯(cuò)誤我們可以在編譯過(guò)程中捕獲,,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.動(dòng)機(jī)

          用來(lái)專門捕獲iframe加載過(guò)程中的異常。

          3.范圍

          很遺憾,結(jié)果并不令人滿意,在實(shí)際的測(cè)試過(guò)程中,該方法未能捕獲到異常。

          (6) React中捕獲異常

          部分 UI 的 JavaScript 錯(cuò)誤不應(yīng)該導(dǎo)致整個(gè)應(yīng)用崩潰,為了解決這個(gè)問(wèn)題,React 16 引入了一個(gè)新的概念 —— 錯(cuò)誤邊界。

          錯(cuò)誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹(shù)任何位置的 JavaScript 錯(cuò)誤,并且,它會(huì)渲染出備用 UI,而不是渲染那些崩潰了的子組件樹(shù)。錯(cuò)誤邊界在渲染期間、生命周期方法和整個(gè)組件樹(shù)的構(gòu)造函數(shù)中捕獲錯(cuò)誤。

          注意:錯(cuò)誤邊界無(wú)法捕獲以下場(chǎng)景中產(chǎn)生的錯(cuò)誤

          • 事件處理
          • 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調(diào)函數(shù))
          • 服務(wù)端渲染
          • 它自身拋出來(lái)的錯(cuò)誤(并非它的子組件)

          如果一個(gè) class 組件中定義了 static getDerivedStateFromError() 或 componentDidCatch() 這兩個(gè)生命周期方法中的任意一個(gè)(或兩個(gè))時(shí),那么它就變成一個(gè)錯(cuò)誤邊界。當(dāng)拋出錯(cuò)誤后,請(qǐng)使用 static getDerivedStateFromError() 渲染備用 UI ,使用 componentDidCatch() 打印錯(cuò)誤信息。

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasError: false };
            }

            static getDerivedStateFromError(error) {
              // 更新 state 使下一次渲染能夠顯示降級(jí)后的 UI
              return { hasError: true };
            }

            componentDidCatch(error, errorInfo) {
              // 你同樣可以將錯(cuò)誤日志上報(bào)給服務(wù)器
              logErrorToMyService(error, errorInfo);
            }

            render() {
              if (this.state.hasError) {
                // 你可以自定義降級(jí)后的 UI 并渲染
                return <h1>Something went wrong.</h1>;
              }

              return this.props.children; 
            }
          }
          復(fù)制代碼

          錯(cuò)誤邊界的工作方式類似于 JavaScript 的 catch {},不同的地方在于錯(cuò)誤邊界只針對(duì) 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è)鉤子也會(huì)捕獲組件生命周期鉤子里的錯(cuò)誤。同樣的,當(dāng)這個(gè)鉤子是 undefined 時(shí),被捕獲的錯(cuò)誤會(huì)通過(guò) console.error 輸出而避免應(yīng)用崩潰。
          • 從 2.4.0 起,這個(gè)鉤子也會(huì)捕獲 Vue 自定義事件處理函數(shù)內(nèi)部的錯(cuò)誤了。
          • 從 2.6.0 起,這個(gè)鉤子也會(huì)捕獲 v-on DOM 監(jiān)聽(tīng)器內(nèi)部拋出的錯(cuò)誤。另外,如果任何被覆蓋的鉤子或處理函數(shù)返回一個(gè) Promise 鏈 (例如 async 函數(shù)),則來(lái)自其 Promise 鏈的錯(cuò)誤也會(huì)被處理。

          以上引用自Vue 官網(wǎng)。

          (8) http請(qǐng)求異常

          1.用法

          以axios為例,添加響應(yīng)攔截器

          axios.interceptors.response.use(function (response) {
              // 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
              // response 是請(qǐng)求回來(lái)的數(shù)據(jù)
              return response;
            }, function (error) {
              // 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么
              return Promise.reject(error)
            }
          )

          復(fù)制代碼

          2.動(dòng)機(jī)

          用來(lái)專門捕獲HTTP請(qǐng)求異常

          五、項(xiàng)目實(shí)踐

          在提出了這么多的解決方案之后,相信大家對(duì)具體怎么用還是存在一些疑惑。那么接下來(lái),我們真正的進(jìn)入實(shí)踐階段吧!

          我們?cè)俅位仡櫼幌挛覀冃枰鉀Q的問(wèn)題是什么?

          • 語(yǔ)法錯(cuò)誤
          • 事件異常
          • HTTP請(qǐng)求異常
          • 靜態(tài)資源加載異常
          • Promise 異常
          • Iframe 異常
          • 頁(yè)面崩潰

          捕獲異常是我們的最終目標(biāo)嗎?并不是,回到解決問(wèn)題的背景下,相比于頁(yè)面崩潰或點(diǎn)不動(dòng),在適當(dāng)?shù)臅r(shí)機(jī),以一種適當(dāng)?shù)姆绞饺ヌ嵝延脩舢?dāng)前發(fā)生了什么,無(wú)疑是一種更友好的處理方式。

          結(jié)合到項(xiàng)目中,具體實(shí)踐起來(lái)有如下兩種方案:

          • 1.代碼中通過(guò)大量的try catch/Promise.catch來(lái)捕獲,捕獲不到的使用其他方式進(jìn)行兜底
          • 2.通過(guò)框架提供的機(jī)制來(lái)做,再對(duì)不能捕獲的進(jìn)行兜底

          方案一無(wú)疑不是很聰明的樣子...這意味著要去改大量的原有代碼,心智負(fù)擔(dān)成倍數(shù)增加。方案二則更加明智,通過(guò)在底層對(duì)錯(cuò)誤進(jìn)行統(tǒng)一處理,無(wú)需變更原有邏輯。

          到項(xiàng)目中,使用的是React框架,React正好提供了一種捕獲異常的機(jī)制(上文已提及)并做降級(jí)處理,但是細(xì)心的小伙伴發(fā)現(xiàn)了,react并不能捕獲如下四種錯(cuò)誤:

          • 事件處理
          • 異步代碼(例如 setTimeout 或 requestAnimationFrame 回調(diào)函數(shù))
          • 服務(wù)端渲染
          • 它自身拋出來(lái)的錯(cuò)誤(并非它的子組件)

          對(duì)于第三點(diǎn)服務(wù)端渲染錯(cuò)誤,項(xiàng)目中并沒(méi)有適用的場(chǎng)景,此次不做重點(diǎn)分析。我們重點(diǎn)分析第一點(diǎn)和第二點(diǎn)。

          我在這里先拋出幾個(gè)問(wèn)題,大家先做短暫的思考:

          • 1.若事件處理和異步代碼的錯(cuò)誤導(dǎo)致頁(yè)面crash,我們?cè)撊绾晤A(yù)防?
          • 2.如何對(duì)ErrorBounary進(jìn)行兜底?相比一個(gè)按鈕點(diǎn)擊無(wú)效,如何更加友好的提示用戶?

          先來(lái)看第一個(gè)問(wèn)題,若事件處理和異步代碼的錯(cuò)誤導(dǎo)致頁(yè)面崩潰:

          const Test = () => {
            const [data, setData] = useState([]);
            return (
              <div
                onClick={() => {
                  setData('');
                }}
              >
                {data.map((s) => s.i)}
              </div>
            );
          };
          復(fù)制代碼

          此段代碼在正常渲染期間是沒(méi)問(wèn)題的,但在觸發(fā)了點(diǎn)擊事件之后會(huì)導(dǎo)致頁(yè)面異常白屏,如果在外面套上我們的ErrorBounday組件,情況會(huì)是怎么樣呢?

          答案是依然能夠捕獲到錯(cuò)誤,并能夠?qū)υ摻M件進(jìn)行降級(jí)處理!

          此時(shí)有些小伙伴已經(jīng)察覺(jué)到了,錯(cuò)誤邊界只要是在渲染期間都是可以捕獲錯(cuò)誤的,無(wú)論首次渲染還是二次渲染。流程圖如下:

          image.png

          第一個(gè)問(wèn)題原來(lái)根本就不是問(wèn)題,這本身就是一個(gè)閉環(huán),不用我們解決!

          再來(lái)看看第二個(gè)問(wèn)題:

          對(duì)于事件處理和異步代碼中不會(huì)導(dǎo)致頁(yè)面崩潰的代碼:

          const Test = () => {
            return (
              <button
                onClick={() => {
                  [].map((s) => s.a.b);
                }}
              >
                點(diǎn)擊
              </button>
            );
          };
          復(fù)制代碼

          button按鈕可正常點(diǎn)擊,但是該點(diǎn)擊事件的內(nèi)部邏輯是有問(wèn)題的,導(dǎo)致用戶點(diǎn)擊該按鈕本質(zhì)是無(wú)效的。此時(shí)若不及時(shí)給與友好提示,用戶只會(huì)陷入抓狂中....

          那么有沒(méi)有辦法對(duì)ErrorBoundary進(jìn)行兜底呢?即可以捕獲異步代碼或事件處理中的錯(cuò)誤。

          上文提到的window.addEventListener('error')正好可以解決這個(gè)問(wèn)題。理想狀態(tài)下:

          而真正的執(zhí)行順序確實(shí)這樣的:

          1625105438(1).png

          在真正執(zhí)行的過(guò)程中,window.addEventListener('error')是先于ErrorBoundary捕獲到錯(cuò)誤的,這就導(dǎo)致當(dāng)error事件捕獲到錯(cuò)誤時(shí),他并不知道該錯(cuò)誤是否會(huì)導(dǎo)致頁(yè)面崩潰,不知道該給予怎樣的提示,到底是對(duì)頁(yè)面進(jìn)行降級(jí)處理還是只做簡(jiǎn)單的報(bào)錯(cuò)提示?

          問(wèn)題似乎就卡在這了....

          那能否通過(guò)一種有效的途徑告訴error事件:ErrorBoundary已經(jīng)捕獲到了錯(cuò)誤,你不需要處理!亦或者是ErrorBoundary未能捕獲到錯(cuò)誤,這是一個(gè)異步錯(cuò)誤/事件錯(cuò)誤,但不會(huì)引起頁(yè)面崩潰,你只需要提示用戶!

          答案肯定是有的,比如建立一個(gè)nodeJs服務(wù)器,通過(guò)webSocket去通知,但是這樣做不僅麻煩,還會(huì)有一定的延遲。

          在筆者苦思冥想之際,在某個(gè)靜悄悄的夜晚,突然靈感一現(xiàn)。為什么我們非要按照他規(guī)定的順序執(zhí)行呢?我們能不能嘗試改變他的執(zhí)行順序,讓錯(cuò)誤捕獲回到我們理想中的流程來(lái)呢?

          改變思路之后,我們?cè)偎伎加惺裁茨芨淖兇a執(zhí)行順序嗎?沒(méi)錯(cuò),異步事件!

                window.addEventListener('error'function (error) {
                  setTimeout(()=>{
                    console.log(error, 'error錯(cuò)誤');
                  })
                });
          復(fù)制代碼

          當(dāng)給error事件的回調(diào)函數(shù)加入setTimeout后,捕獲異常的流程為:

          image.png

          現(xiàn)在就可以通知error事件到底頁(yè)面崩潰了沒(méi)有,到底需不需要它的處理!上代碼:

          class ErrorBoundary extends React.Component {
            constructor(props) {
              super(props);
              this.state = { hasError: false };
            }

            static getDerivedStateFromError(error) {
              // 更新 state 使下一次渲染能夠顯示降級(jí)后的 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) {
                // 你可以自定義降級(jí)后的 UI 并渲染
                return <h1>Something went wrong.</h1>;
              }

              return this.props.children; 
            }
          }
          復(fù)制代碼
            window.addEventListener('error'function (error) {
                  setTimeout(() => {
                    //進(jìn)來(lái)代表一定有錯(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ò)誤,請(qǐng)通知開(kāi)發(fā)人員');
                      } else if (error.message.indexOf('SyntaxError')) {
                        alert('這是一個(gè)SyntaxError錯(cuò)誤,請(qǐng)通知開(kāi)發(fā)人員');
                      } else {
                        //在此次給與友好提示
                      }
                    }
                  });
                });
          復(fù)制代碼

          最后,通過(guò)我們的努力,當(dāng)頁(yè)面崩潰時(shí),及時(shí)進(jìn)行降級(jí)處理;當(dāng)頁(yè)面未崩潰,但有錯(cuò)誤時(shí),我們及時(shí)的告知用戶,并對(duì)錯(cuò)誤進(jìn)行上報(bào),達(dá)到預(yù)期的效果。

          六、擴(kuò)展

          1.設(shè)置采集率

          若是錯(cuò)誤實(shí)在太多,比如有時(shí)候代碼進(jìn)入死循環(huán),錯(cuò)誤量過(guò)多導(dǎo)致服務(wù)器壓力大時(shí),可酌情降低采集率。比如采集30%:

                if (Math.random() < 0.3) {
                  //上報(bào)錯(cuò)誤
                  logErrorToMyService(error, errorInfo);
                }
          復(fù)制代碼

          2.提效

          解決上面這些問(wèn)題后,大家難免會(huì)有疑問(wèn):那每一個(gè)組件都要去套一層ErrorBoundary組件,這工作量是不是有點(diǎn)大....而且有一些老代碼,嵌套的比較深,改起來(lái)心理負(fù)擔(dān)也會(huì)比較大。那有沒(méi)有辦法將其作為一個(gè)配置項(xiàng),配置完之后,編譯時(shí)自動(dòng)套上一層ErrorBoundary組件呢?這個(gè)我們下次在做探討!

          3.可配置

          能否將ErrorBoundary擴(kuò)展成可傳入自定義UI的組件呢?這樣大家通過(guò)定制化UI,在不同的場(chǎng)景進(jìn)行不同的降級(jí)處理。

          同樣,這一塊我們下次再討論!

          七、總結(jié)

          異常處理是高質(zhì)量軟件開(kāi)發(fā)中的一個(gè)基本部分,但是在許多情況下,它們會(huì)被忽略,或者是不正確的使用,而處理異常只是保證代碼流程不出錯(cuò),重定向到正確的程序流中去。

          本文從前端錯(cuò)誤類型出發(fā),從try catch逐步揭開(kāi)錯(cuò)誤異常神秘的面紗,再通過(guò)一系列的操作對(duì)異常進(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寫(xiě)在控制臺(tái)是捕獲不到錯(cuò)誤的?而寫(xiě)在HTML文件中就可以捕獲到?
          • 服務(wù)端渲染錯(cuò)誤如何捕獲?

          帶著這些思考,我們下次見(jiàn)~

          關(guān)于本文

          來(lái)源:縱有疾風(fēng)起

          https://juejin.cn/post/6979564690787532814

          The End

          歡迎自薦投稿到《前端技術(shù)江湖》,如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),記得點(diǎn)個(gè) 「在看」


          點(diǎn)個(gè)『在看』支持下 

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  先锋影院亚洲无码 | 亚洲无码一区二区电影 | 大香蕉欧美在线观看不卡视频 | 夜夜干夜夜操 | 亚洲最大的激情4438 |