1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        務(wù)實(shí)用得上的前端異常的捕獲與處理

        共 12756字,需瀏覽 26分鐘

         ·

        2021-03-23 16:48

        按鍵無法點(diǎn)擊、元素不展示、頁面白屏,這些都是我們前端不想看到的場景。在計(jì)算機(jī)程序運(yùn)行的過程中,也總是會(huì)出現(xiàn)各種各樣的異常。下面就讓我們聊一聊有哪些異常以及怎么處理它們。

        一、前言

        什么是異常,異常就是預(yù)料之外的事件,往往影響了程序的正確運(yùn)行。例如下面幾種場景:

        • 頁面元素異常(例如按鈕無法點(diǎn)擊、元素不展示)

        • 頁面卡頓

        • 頁面白屏

        這些情況都是極其影響用戶體驗(yàn)的。對(duì)于前端來說,異常雖然不會(huì)導(dǎo)致計(jì)算機(jī)宕機(jī),但是往往會(huì)導(dǎo)致用戶的操作被阻塞。雖然異常不可完全杜絕,但是我們有充分的理由去理解異常、學(xué)習(xí)處理異常。

        異常處理在程序設(shè)計(jì)中的重要性是毋庸置疑的。任何有影響力的 Web 應(yīng)用程序都需要一套完善的異常處理機(jī)制,但實(shí)際上,通常只有服務(wù)端團(tuán)隊(duì)會(huì)在異常處理機(jī)制上投入較大精力。雖然客戶端應(yīng)用程序的異常處理也同樣重要,但真正受到重視,還是最近幾年的事。作為新世紀(jì)的杰出前端開發(fā)人員,我們必須理解有哪些異常,當(dāng)發(fā)生異常時(shí)我們有哪些手段和工具可以利用。

        二、異常分類

        從根本上來說,異常就是一個(gè)數(shù)據(jù)結(jié)構(gòu),它存了異常發(fā)生時(shí)相關(guān)信息,譬如錯(cuò)誤碼、錯(cuò)誤信息等。其中 message 屬性是唯一一個(gè)能夠保證所有瀏覽器都支持的屬性,除此之外,IE、Firefox、Safari、Chrome 以及 Opera 都為事件對(duì)象添加了其它相關(guān)信息。譬如 IE 添加了與 message 屬性完全相同的 description 屬性,還添加了保存這內(nèi)部錯(cuò)誤數(shù)量的 number 屬性。Firefox 添加了 fileName、lineNumber 和 stack(包含堆棧屬性)。所以,在考慮瀏覽器兼容性時(shí),最好還是只使用 message 屬性。

        執(zhí)行 JS 期間可能會(huì)發(fā)生的錯(cuò)誤有很多類型。每種錯(cuò)誤都有對(duì)應(yīng)的錯(cuò)誤類型,而當(dāng)錯(cuò)誤發(fā)生的時(shí)候就會(huì)拋出響應(yīng)的錯(cuò)誤對(duì)象。ECMA-262 中定義了下列 7 種錯(cuò)誤類型:

        • Error:錯(cuò)誤的基類,其他錯(cuò)誤都繼承自該類型
        • EvalError:Eval 函數(shù)執(zhí)行異常
        • RangeError:數(shù)組越界
        • ReferenceError:嘗試引用一個(gè)未被定義的變量時(shí),將會(huì)拋出此異常
        • SyntaxError:語法解析不合理
        • TypeError:類型錯(cuò)誤,用來表示值的類型非預(yù)期類型時(shí)發(fā)生的錯(cuò)誤
        • URIError:以一種錯(cuò)誤的方式使用全局 URI 處理函數(shù)而產(chǎn)生的錯(cuò)誤

        三、異常處理

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

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

        如果 try 塊中的任何代碼發(fā)生了錯(cuò)誤,就會(huì)立即退出代碼執(zhí)行過程,然后執(zhí)行 catch 塊。此時(shí) catch 塊會(huì)接收到一個(gè)包含錯(cuò)誤信息的對(duì)象,這個(gè)對(duì)象中包含的信息因?yàn)g覽器而異,但共同的是有一個(gè)保存著錯(cuò)誤信息的 message 屬性。

        finally 子句在 try-catch 語句中是可選的,但是 finally 子句一經(jīng)使用,其代碼無論如何都會(huì)執(zhí)行。換句話說,try 語句塊中代碼全部正常執(zhí)行,finally 子句會(huì)執(zhí)行;如果因?yàn)槌鲥e(cuò)執(zhí)行了 catch 語句,finally 子句照樣會(huì)執(zhí)行。只要代碼中包含 finally 子句,則無論 try 或 catch 語句中包含什么代碼——甚至是 return 語句,都不會(huì)阻止 finally 子句執(zhí)行。來看下面函數(shù)的執(zhí)行結(jié)果:

        function testFinally {
          try {
            return "出去玩";
          } catch (error) {
            return "看電視";
          } finally {
            return "做作業(yè)";
          }
          return "睡覺";
        }

        表面上調(diào)用這個(gè)函數(shù)會(huì)返回 "出去玩",因?yàn)榉祷?"出去玩" 的語句位于 try 語句塊中,而執(zhí)行此語句又不會(huì)出錯(cuò)。實(shí)際上返回 "做作業(yè)",因?yàn)樽詈筮€有 finally 子句,結(jié)果就會(huì)導(dǎo)致 try 塊里的 return 語句被忽略,也就是說調(diào)用的結(jié)果只能返回 "做作業(yè)"。如果把 finally 語句拿掉,這個(gè)函數(shù)將返回 "出去玩"。因此,在使用 finally 子句之前,一定要非常清楚你想讓代碼怎么樣。(思考一下如果 catch 塊和 finally 塊都拋出異常,catch 塊的異常是否能拋出)

        但令人遺憾的是,try-catch 無法處理異步代碼和一些其他場景。接下來讓我具體分析幾種異常場景及其處理方案。

        四、異常分析

        1. JS 代碼錯(cuò)誤

        下面為我司內(nèi)部錯(cuò)誤監(jiān)控平臺(tái)一次日常報(bào)錯(cuò)的調(diào)用堆棧截圖:

        錯(cuò)誤還是比較明顯的,this 指向?qū)е碌膯栴}。onOk 使用普通函數(shù)時(shí),函數(shù)內(nèi)執(zhí)行語句的 this 上下文為 Antd.Modal 組件的實(shí)例,而 Antd.Modal 組件不存在 changeFilterType 這個(gè)方法。將 onOK 方法像 onCancel 方法一樣改成箭頭函數(shù),將 this 指向父組件即可。

        TypeError 類型在 JavaScript 中會(huì)經(jīng)常遇到,在變量中保存著意外類型時(shí),或者在訪問不存在的方法時(shí),都會(huì)導(dǎo)致這種錯(cuò)誤。錯(cuò)誤的原因雖然多種多樣,但歸根結(jié)底還是由于在執(zhí)行特定類型的操作時(shí),變量的類型并不符合要求所致。再看幾個(gè)例子:

        class People {
          constructor(name) {
            this.name = name;
          }
          sing() {}
        }
        const xiaoming = new People("小明");
        xiaoming.dance(); // 拋出 TypeError
        xiaoming.girlfriend.name; // 拋出 TypeError

        代碼錯(cuò)誤一般在開發(fā)和測試階段就能發(fā)現(xiàn)。用 try-catch 也能捕獲到:

        // 代碼
        try {
          xiaoming.girlfriend.name;
        catch (error) {
          console.log(xiaoming.name + "沒有女朋友", error);
        }
        // 運(yùn)行結(jié)果
        // 小明沒有女朋友 TypeError: Cannot read property 'name' of undefined

        2. JS 語法錯(cuò)誤

        我們修改一下代碼,我們把英文分號(hào)改成中文分號(hào):

        try {
          xiaoming.girlfriend.name;// 結(jié)尾是中文分號(hào)
        catch(error) {
          console.log(xiaoming.name + "沒有女朋友", error);
        }
        // 運(yùn)行結(jié)果
        // Uncaught SyntaxError: Invalid or unexpected token

        SyntaxError 語法錯(cuò)誤我們無法通過 try-catch 捕獲到,不過語法錯(cuò)誤在我們開發(fā)階段就可以看到,應(yīng)該不會(huì)順利上到線上環(huán)境。

        不過凡事總有例外,線上還是能收到一些語法錯(cuò)誤的告警,但多半是 JSON 解析出錯(cuò)和瀏覽器兼容性導(dǎo)致。

        再看幾個(gè)例子:

        JSON.parse('{name:xiaoming}');      // Uncaught SyntaxError: Unexpected token n in JSON at position 1
        JSON.parse('{"name":xiaoming}');    // Uncaught SyntaxError: Unexpected token x in JSON at position 8
        JSON.parse('{"name":"xiaoming"}');  // 正常
        var testFunc () => { };             // 在 IE 下會(huì)拋出 SyntaxError,因?yàn)?nbsp;IE 不支持箭頭函數(shù),需要通過Babel等工具事先轉(zhuǎn)譯下

        使用 JSON.parse 解析時(shí)出現(xiàn)異常就是一個(gè)很好的使用 try-catch 的場景:

        try {
          JSON.parse(remoteData); // remoteData 為服務(wù)端返回的數(shù)據(jù)
        catch {
          console.error("服務(wù)端數(shù)據(jù)格式返回異常,無法解析", remoteData);
        }

        并不是捕獲到錯(cuò)誤就結(jié)束了,捕獲到錯(cuò)誤后,我們需要思考當(dāng)錯(cuò)誤發(fā)生時(shí):

        • 錯(cuò)誤是否是致命的,會(huì)不會(huì)導(dǎo)致其它連帶錯(cuò)誤

        • 后續(xù)的代碼邏輯還能不能繼續(xù)執(zhí)行,用戶還能不能繼續(xù)操作

        • 是不是需要將錯(cuò)誤信息反饋給用戶,提示用戶如何處理該錯(cuò)誤

        • 是不是需要將錯(cuò)誤上報(bào)服務(wù)端

        對(duì)應(yīng)上面的問題這里就會(huì)有很多解決方案了,譬如:

        1. 如果是服務(wù)器未知異常導(dǎo)致,可以阻塞用戶操作,彈窗提示用戶"服務(wù)器異常,請(qǐng)稍后重試"。并提供給用戶一個(gè)刷新的按鈕;
        try {
          return JSON.parse(remoteData);
        catch (error) {
          Modal.fail("服務(wù)器異常,請(qǐng)稍后重試");
          return false;
        }
        1. 如果是數(shù)據(jù)異常導(dǎo)致,可阻塞用戶操作,彈窗提示用戶"服務(wù)器異常,請(qǐng)聯(lián)系客服處理~",同時(shí)將錯(cuò)誤信息上報(bào)異常服務(wù)器,開發(fā)人員通過異常堆棧和用戶埋點(diǎn)定位問題原因;
        try {
          return JSON.parse(remoteData);
        catch (error) {
          Modal.fail("服務(wù)器異常,請(qǐng)聯(lián)系客服處理~");
          logger.error("JSON數(shù)據(jù)解析出現(xiàn)異常", error);
          return false;
        }
        1. 如果數(shù)據(jù)解析出錯(cuò)屬于預(yù)料之中的情況,也有替代的默認(rèn)值,那么當(dāng)解析出錯(cuò)時(shí)直接使用默認(rèn)值也可以;
        try {
          return JSON.parse(remoteData);
        catch (error) {
          console.error("服務(wù)端數(shù)據(jù)格式返回異常,使用本地緩存數(shù)據(jù)", erorr);
          return localData;
        }

        任何錯(cuò)誤處理策略中最重要的一個(gè)部分,就是確定錯(cuò)誤是否致命。

        3. 異步錯(cuò)誤

        try {
          setTimeout(() => {
            undefined.map(v => v);
          }, 1000)
        catch(e) {
          console.log("捕獲到異常:", e);
        }
         
        Uncaught TypeError: Cannot read property 'map' of undefined
          at <anonymous>:3:15

        并沒有捕獲到異常,try-catch 對(duì)語法和異步錯(cuò)誤卻無能為力,捕獲不到,這是需要我們特別注意的地方。

        五、異常捕獲

        5.1 window.onerror

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

        /**
         * @param {String}  message    錯(cuò)誤信息
         * @param {String}  source     出錯(cuò)文件
         * @param {Number}  lineno     行號(hào)
         * @param {Number}  colno      列號(hào)
         * @param {Object}  error      Error對(duì)象(對(duì)象)
         */

        window.onerror = function (message, source, lineno, colno, error{
          console.log("捕獲到異常:", { message, source, lineno, colno, error });
        };

        同步錯(cuò)誤可以捕獲到,但是,請(qǐng)注意 window.error 無法捕獲靜態(tài)資源異常和 JS 代碼錯(cuò)誤。

        5.2 靜態(tài)資源加載異常

        方法一:onerror 來捕獲

        <script>
          function errorHandler(error{
            console.log("捕獲到靜態(tài)資源加載異常", error);
          }
        </script>
        <script src="http://cdn.xxx.com/js/test.js" onerror="errorHandler(this)"></script>
        <link rel="stylesheet" href="http://cdn.xxx.com/styles/test.css" onerror="errorHandler(this)">

        這樣可以拿到靜態(tài)資源的錯(cuò)誤,但缺點(diǎn)很明顯,代碼的侵入性太強(qiáng)了,每一個(gè)靜態(tài)資源標(biāo)簽都要加上 onerror 方法。

        方法二:addEventListener("error")

        <!DOCTYPE html>
        <html lang="zh">
         
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>error</title>
          <script>
            window.addEventListener('error', (error) => {
              console.log('捕獲到異常:', error);
            }, true)
          
        </script>
        </head>
         
        <body>
          <img src="https://itemcdn.zcycdn.com/15af41ec-e6cb-4478-8fad-1a47402f0f25.png">
        </body>
         
        </html>

        由于網(wǎng)絡(luò)請(qǐng)求異常不會(huì)事件冒泡,因此必須在捕獲階段將其捕捉到才行,但是這種方式雖然可以捕捉到網(wǎng)絡(luò)請(qǐng)求的異常,但是無法判斷 HTTP 的狀態(tài)是 404 還是其他比如 500 等等,所以還需要配合服務(wù)端日志才進(jìn)行排查分析才可以。

        5.3 Promise 異常

        Promise 中的異常不能被 try-catch 和 window.onerror 捕獲,這時(shí)候我們就需要監(jiān)聽 unhandledrejection 來幫我們捕獲這部分錯(cuò)誤。

        window.addEventListener("unhandledrejection"function (e{
          e.preventDefault();
          console.log("捕獲到 promise 錯(cuò)誤了");
          console.log("錯(cuò)誤的原因是", e.reason);
          console.log("Promise 對(duì)象是", e.promise);
          return true;
        });

        Promise.reject("promise error");
        new Promise((resolve, reject) => {
          reject("promise error");
        });
        new Promise((resolve) => {
          resolve();
        }).then(() => {
          throw "promise error";
        });

        5.4 React 異常

        React 處理異常的方式不同。雖然 try-catch 適用于許多非普通 JavaScript 應(yīng)用程序,但它只適用于命令式代碼。因?yàn)?React 組件是聲明性的,所以 try-catch 不是一個(gè)可靠的選項(xiàng)。為了彌補(bǔ)這一點(diǎn),React 實(shí)現(xiàn)了所謂的錯(cuò)誤邊界。錯(cuò)誤邊界是 React 組件,它“捕獲子組件樹中的任何地方的 JavaScript 錯(cuò)誤”,同時(shí)還記錄錯(cuò)誤并顯示回退用戶界面。

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

          componentDidCatch(error, info) {
            // 展示出錯(cuò)的UI
            this.setState({ hasErrortrue });
            // 將錯(cuò)誤信息上報(bào)到日志服務(wù)器
            logErrorToMyService(error, info);
          }

          render() {
            if (this.state.hasError) {
              // 可以展示自定義的錯(cuò)誤樣式
              return <h1>Something went wrong.</h1>;
            }
            return this.props.children;
          }
        }

        但是需要注意的是, error boundaries 并不會(huì)捕捉下面這些錯(cuò)誤:

        • 事件處理器

        • 異步代碼

        • 服務(wù)端的渲染代碼

        • 在 error boundaries 區(qū)域內(nèi)的錯(cuò)誤

        我們可以這樣使用 ErrorBoundary:

        <ErrorBoundary>
          <MyWidget />
        </ErrorBoundary

        5.5 Vue 異常

        Vue.config.errorHandler = (err, vm, info) => {
          console.error("通過vue errorHandler捕獲的錯(cuò)誤");
          console.error(err);
          console.error(vm);
          console.error(info);
        };

        5.6 請(qǐng)求異常

        以最常用的 HTTP 請(qǐng)求庫 axios 為例,模擬接口響應(yīng) 401 的情況:

        // 請(qǐng)求
        axios.get(/api/test/401")
        // 結(jié)果
        Uncaught (in promise) Error: Request failed with status code 401
        at createError (axios.js:1207)
        at settle (axios.js:1177)
        at XMLHttpRequest.handleLoad (axios.js:1037)

        可以看出來 axios 的異常可以當(dāng)做 Promise 異常來處理:

        // 請(qǐng)求
        axios.get("http://localhost:3000/api/uitest/sentry/401")
        .then(data => console.log('接口請(qǐng)求成功', data))
        .catch(e => console.log('接口請(qǐng)求出錯(cuò)', e));
        // 結(jié)果
        接口請(qǐng)求出錯(cuò) Error: Request failed with status code 401
        at createError (createError.js:17)
        at settle (settle.js:18)
        at XMLHttpRequest.handleLoad (xhr.js:62)

        一般接口 401 就代表用戶未登錄,就需要跳轉(zhuǎn)到登錄頁,讓用戶進(jìn)行重新登錄,但如果每個(gè)請(qǐng)求方法都需要寫一遍跳轉(zhuǎn)登錄頁的邏輯就很麻煩了,這時(shí)候就會(huì)考慮使用 axios 的攔截器來做統(tǒng)一梳理,同理能統(tǒng)一處理的異常也可以在放在攔截器里處理。

        // Add a response interceptor
        axios.interceptors.response.use(
          function (response{
            // Any status codes that falls outside the range of 2xx cause this function to trigger
            // Do something with response error
          },
          function (error{
            if (error.response.status === 401) {
              goLogin(); // 跳轉(zhuǎn)登錄頁
            } else if (error.response.status === 502) {
              alert(error.response.data.message || "系統(tǒng)升級(jí)中,請(qǐng)稍后重試");
            }
            return Promise.reject(error.response);
          }
        );

        5.7 總結(jié)

        異常一共七大類,處理時(shí)需分清是致命錯(cuò)誤還是非致命錯(cuò)誤。

        • 可疑區(qū)域增加 try-catch

        • 全局監(jiān)控 JS 異常 window.onerror

        • 全局監(jiān)控靜態(tài)資源異常 window.addEventListener

        • 捕獲沒有 catchPromise 異常用 unhandledrejection

        • Vue errorHandlerReact componentDidCatch

        • Axios 請(qǐng)求統(tǒng)一異常處理用攔截器 interceptors

        • 使用日志監(jiān)控服務(wù)收集用戶錯(cuò)誤信息

        六、異常上報(bào)

        即使我們前端開發(fā)完成后,會(huì)有一系列的 Web 應(yīng)用的上線前的驗(yàn)證,如自測、QA 測試、code review 等,以確保應(yīng)用能在生產(chǎn)上沒有事故。

        但是事與愿違,很多時(shí)候我們都會(huì)接到客戶反饋的一些線上問題,這些問題有時(shí)候可能是你自己代碼的問題。這樣的問題一般能夠在測試環(huán)境重現(xiàn),我們很快的能定位到問題關(guān)鍵位置。但是,很多時(shí)候有一些問題,我們?cè)跍y試中并未發(fā)現(xiàn),可是在線上卻有部分人出現(xiàn)了,問題確確實(shí)實(shí)存在的,這個(gè)時(shí)候我們測試環(huán)境又不能重現(xiàn),還有一些偶現(xiàn)的生產(chǎn)的偶現(xiàn)問題,這些問題都很難定位到問題的原因,讓我們前端工程師頭疼不已。

        而我們不可能每次都遠(yuǎn)程給用戶解決問題,或者讓用戶按 F12 打開瀏覽器控制臺(tái)把錯(cuò)誤信息截圖給我們吧。這時(shí)候,我們不得不借助一些工具來解決這一系列令人頭疼的問題。

        前端錯(cuò)誤監(jiān)控日志系統(tǒng)就應(yīng)用而生。當(dāng)前端代碼在生產(chǎn)運(yùn)行中出現(xiàn)錯(cuò)誤的時(shí)候,第一時(shí)間傳遞給監(jiān)控系統(tǒng),從而第一時(shí)間定位并且解決問題。

        有很多成熟的方案可供選擇:ARMS、fundebug、BadJS、Sentry。政采云當(dāng)前使用的是 Sentry 的開源版本,并結(jié)合業(yè)務(wù)進(jìn)行一些改造:

        • 與構(gòu)建系統(tǒng)結(jié)合,構(gòu)建項(xiàng)目時(shí)自動(dòng)生成 Sentry 項(xiàng)目,注入 Sentry 腳本
        • 客服端注入 Sentry 客戶端腳本后,按項(xiàng)目、頁面等不同粒度配置告警事件的過濾規(guī)則
        • 對(duì)接釘釘消息系統(tǒng),將告警消息推送到訂閱群
        • 過濾接口錯(cuò)誤和優(yōu)化 Promise 錯(cuò)誤上報(bào)信息

        后續(xù)也可以單開一篇介紹介紹,如何結(jié)合開源的錯(cuò)誤監(jiān)控系統(tǒng),搭建具有公司特色的監(jiān)控體系。

        看完兩件事

        如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我兩件小事

        1.點(diǎn)個(gè)「在看」,讓更多人也能看到這篇內(nèi)容(點(diǎn)了在看」,bug -1 ??

        2.關(guān)注公眾號(hào)「前端技術(shù)江湖」,持續(xù)為你推送精選好文
        瀏覽 39
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            小骚穴视频| 国产男女猛烈无遮挡免费观看网站 | 交换做爰2 | 91三级大片视频 | 高黄无码| 四虎官方网站 | 玖玖精品在线观看 | 国产日产亚洲精华av | 国精产品一区二区三区福利姬 | 真实亲子乱一区二区 |