<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>

          搭建前端監(jiān)控,如何采集異常數(shù)據(jù)?

          共 13439字,需瀏覽 27分鐘

           ·

          2022-06-13 19:31

          作者:楊成功

          簡介:專注前端工程與架構(gòu)產(chǎn)出

          來源:SegmentFault  思否社區(qū) 


          大家好,我是楊成功。


          前兩篇,我們介紹了為什么前端應(yīng)該有監(jiān)控系統(tǒng),以及搭建前端監(jiān)控的總體步驟,前端監(jiān)控的 Why 和 What 想必你已經(jīng)明白了。接下來我們解決 How 如何實(shí)現(xiàn)的問題。


          如果不了解前端監(jiān)控,建議先看前兩篇:



          本篇我們介紹,前端如何采集數(shù)據(jù),先從收集異常數(shù)據(jù)開始。

          什么是異常數(shù)據(jù)?



          異常數(shù)據(jù),是指前端在操作頁面的過程中,觸發(fā)的執(zhí)行異?;蚣虞d異常,此時瀏覽器會拋出來報錯信息。

          比如說你的前端代碼用了個未聲明的變量,此時控制臺會打印出紅色錯誤,告訴你報錯原因。或者是接口請求出錯了,在網(wǎng)絡(luò)面板內(nèi)也能查到異常情況,是請求發(fā)送的異常,還是接口響應(yīng)的異常。

          在我們實(shí)際的開發(fā)場景中,前端捕獲的異常主要是分兩個大類,接口異常 和 前端異常,我們分別看下這兩大類異常怎么捕獲。

          接口異常



          接口異常一定是在請求的時候觸發(fā)。前端目前大部分的請求是用 axios 發(fā)起的,所以只要獲取 axios 可能發(fā)生的異常即可。

          如果你用 Promise 的寫法,則用 .catch 捕獲:

          axios
            .post('/test')
            .then((res) => {    console.log(res);
            })
            .catch((err) => {    // err 就是捕獲到的錯誤對象
              handleError(err);
            });


          如果你用 async/await 的寫法,則用 try..catch.. 捕獲:

          async () => {  try {    let res = await axios.post('/test');    console.log(res);
            } catch (err) {    // err 就是捕獲到的錯誤對象
              handleError(err);
            }
          };

          當(dāng)捕獲到異常之后,統(tǒng)一交給 handleError 函數(shù)處理,這個函數(shù)會將接收到的異
          常進(jìn)行處理,并調(diào)用 上報接口 將異常數(shù)據(jù)傳到服務(wù)器,從而完成采集。

          上面我們寫的異常捕獲,邏輯上是沒問題的,實(shí)操起來就會發(fā)現(xiàn)第一道坎:頁面這么多,難道每個請求都要包一層 catch 嗎?

          是啊,如果我們是新開發(fā)一個項目,在開始的時候就規(guī)定每個請求要包一層 catch 也無可厚非,但是如果是在一個已有的規(guī)模還不小的項目中接入前端監(jiān)控,這時候在每個頁面或每個請求 catch 顯然是不現(xiàn)實(shí)的。

          所以,為了最大程度的降低接入成本,減少侵入性,我們是用第二種方案:在 axios 攔截器中捕獲異常。

          前端項目,為了統(tǒng)一處理請求,比如 401 的跳轉(zhuǎn),或者全局錯誤提示,都會在全局寫一個 axios 實(shí)例,為這個實(shí)例添加攔截器,然后在其他頁面中直接倒入這個實(shí)例使用,比如:

          // 全局請求:src/request/axios.jsconst instance = axios.create({  baseURL: 'https://api.test.com'
            timeout: 15000,  headers: {    'Content-Type''application/json',
            },
          })export default instance


          然后在具體的頁面中這樣發(fā)起請求:

          // a 頁面:src/page/a.jsximport http from '@/src/request/axios.js';async () => {  let res = await http.post('/test');  console.log(res);
          };


          這樣的話,我們發(fā)現(xiàn)每個頁面的請求都會走全局 axios 實(shí)例,所以我們只需要在全局請求的位置捕獲異常即可,就不需要在每個頁面捕獲了,這樣接入成本會大大降低。

          按照這個方案,結(jié)下來我們在 src/request/axios.js 這個文件中動手實(shí)施。

          攔截器中捕獲異常


          首先我們?yōu)?axios 添加響應(yīng)攔截器:

          // 響應(yīng)攔截器instance.interceptors.response.use(  (response) => {    return response.data;
            },  (error) => {    // 發(fā)生異常會走到這里
              if (error.response) {      let response = error.response;      if (response.status >= 400) {        handleError(response);
                }
              } else {      handleError(null);
              }    return Promise.reject(error);
            },
          );


          響應(yīng)攔截器的第二個參數(shù)是在發(fā)生錯誤時執(zhí)行的函數(shù),參數(shù)就是異常。我們首先要判斷是否存在 error.response,存在就說明接口有響應(yīng),也就是接口通了,但是返回錯誤;不存在則說明接口沒通,請求一直掛起,多數(shù)是接口崩潰了。

          如果有響應(yīng),首先獲取狀態(tài)碼,根據(jù)狀態(tài)碼來判斷什么時候需要收集異常。上面的判斷方式簡單粗暴,只要狀態(tài)碼大于 400 就視為一個異常,拿到響應(yīng)數(shù)據(jù),并執(zhí)行上報邏輯。

          如果沒有響應(yīng),可以看作是接口超時異常,調(diào)用異常處理函數(shù)時傳一個 null 即可。

          前端異常



          上面我們介紹了在 axios 攔截器中如何捕獲接口異常,這部分我們再介紹如何捕獲前端異常。

          前端代碼捕獲異常,最常用的方式就是用 try..catch.. 了,任意同步代碼塊都可以放到 try 塊中,只要發(fā)生異常就會執(zhí)行 catch:

          try {  // 任意同步代碼} catch (err) {  console.log(err);
          }


          上面說“任意同步代碼”而不是“任意代碼”,主要是普通的 Promise 寫法 try..catch.. 是捕獲不到的,只能用 .catch() 捕獲,如:

          try {  Promise.reject(new Error('出錯了')).catch((err) => console.log('1:', err));
          } catch (err) {  console.log('2:', err);
          }


          把這段代碼丟進(jìn)瀏覽器,打印結(jié)果是:

          1:Error: 出錯了


          很明顯只是 .catch 捕獲到了異常。不過與上面接口異常的邏輯一樣,這種方式處理當(dāng)前頁面異常沒什么問題,但從整個應(yīng)用來看,這樣捕獲異常侵入性強(qiáng),接入成本高,所以我們的思路依然是全局捕獲。

          全局捕獲 js 的異常也比較簡單,用 window.addEventLinstener('error') 即可:

          // js 錯誤捕獲window.addEventListener('error', (error) => {  // error 就是js的異常});



          為啥不用 window.onerror ?

          這里很多小伙伴有疑問,為什么不用 window.onerror 全局監(jiān)聽呢?window.addEventLinstener('error') 和 window.onerror 有什么區(qū)別呢?

          首先這兩個函數(shù)功能基本一致,都可以全局捕獲 js 異常。但是有一類異常叫做 資源加載異常,就是在代碼中引用了不存在的圖片,js,css 等靜態(tài)資源導(dǎo)致的異常,比如:

          const loadCss = ()=> {  let link = document.createElement('link')
            link.type = 'text/css'
            link.rel = 'stylesheet'
            link.href = 'https://baidu.com/15.css'
            document.getElementsByTagName('head')[10].append(link)
          }render() {  return <div>
              <img src='./bbb.png'/>
              <button onClick={loadCss}>加載樣式<button/>
            </div>}


          上述代碼中的 baidu.com/15.css bbb.png 是不存在的,JS 執(zhí)行到這里肯定會報一個資源找不到的錯誤。但是默認(rèn)情況下,上面兩種 window 對象上的全局監(jiān)聽函數(shù)都監(jiān)聽不到這類異常。

          因?yàn)橘Y源加載的異常只會在當(dāng)前元素觸發(fā),異常不會冒泡到 window,因此監(jiān)聽 window 上的異常是捕捉不到的。那怎么辦呢?

          如果你熟悉 DOM 事件你就會明白,既然冒泡階段監(jiān)聽不到,那么在捕獲階段一定能監(jiān)聽到。

          方法就是給 window.addEventListene 函數(shù)指定第三個參數(shù),很簡單就是 true,表示該監(jiān)聽函數(shù)會在捕獲階段執(zhí)行,這樣就能監(jiān)聽到資源加載異常了。

          // 捕獲階段全局監(jiān)聽window.addEventListene(  'error',  (error) => {    if (error.target != window) {      console.log(error.target.tagName, error.target.src);
              }    handleError(error);
            },  true,
          );


          上述方式可以很輕松的監(jiān)聽到圖片加載異常,這就是為什么更推薦 window.addEventListene 的原因。不過要記得,第三個參數(shù)設(shè)為 true,監(jiān)聽事件捕獲,就可以全局捕獲到 JS 異常和資源加載異常。

          需要特別注意,window.addEventListene 同樣不能捕獲 Promise 異常。不管是 Promise.then() 寫法還是 async/await 寫法,發(fā)生異常時都不能捕獲。

          因此,我們還需要全局監(jiān)聽一個 unhandledrejection 函數(shù)來捕獲未處理的 Promise 異常。

          // promise 錯誤捕獲window.addEventListener('unhandledrejection', (error) => {  // 打印異常原因
            console.log(error.reason);  handleError(error);  // 阻止控制臺打印
            error.preventDefault();
          });


          unhandledrejection 事件會在 Promise 發(fā)生異常并且沒有指定 catch 的時候觸發(fā),相當(dāng)于一個全局的 Promise 異常兜底方案。這個函數(shù)會捕捉到運(yùn)行時意外發(fā)生的 Promise 異常,這對我們排錯非常有用。

          默認(rèn)情況下,Promise 發(fā)生異常且未被 catch 時,會在控制臺打印異常。如果我們想阻止異常打印,可以用上面的 error.preventDefault() 方法。

          異常處理函數(shù)



          前面我們在捕獲到異常時調(diào)用了一個異常處理函數(shù) handleError,所有的異常和上報邏輯統(tǒng)一在這個函數(shù)內(nèi)處理,接下來我們實(shí)現(xiàn)這個函數(shù)。

          const handleError = (error: any, type: 1 | 2) {  if(type == 1) {    // 處理接口異常
            }  if(type == 2) {    // 處理前端異常
            }
          }


          為了區(qū)分異常類型,函數(shù)新加了第二個參數(shù) type 表示當(dāng)前異常屬于前端還是接口。在不同的場景中使用如下:

          • 處理前端異常:handleError(error, 1)

          • 處理接口異常:handleError(error, 2)


          處理接口異常


          處理接口異常,我們需要將拿到的 error 參數(shù)解析,然后取到需要的數(shù)據(jù)。接口異常一般需要的數(shù)據(jù)字段如下:

          • code:http 狀態(tài)碼

          • url:接口請求地址

          • method:接口請求方法

          • params:接口請求參數(shù)

          • error:接口報錯信息


          這些字段都可以在 error 參數(shù)中獲取,方法如下:

          const handleError = (error: any, type: 1 | 2) {  if(type == 1) {    // 此時的 error 響應(yīng),它的 config 字段中包含請求信息
              let { url, method, params, data } = error.config
              let err_data = {
                 url, method,       params: { query: params, body: data },       error: error.data?.message || JSON.stringify(error.data),
              })
            }
          }


          config 對象中的 params 表示 GET 請求的 query 參數(shù),data 表示 POST 請求的 body 參數(shù),所以我在處理參數(shù)的時候,將這兩個參數(shù)合并為一個,用一個屬性 params 來表示。

          params: { query: params, body: data }


          還有一個 error 屬性表示錯誤信息,這個獲取方式要根據(jù)你的接口返回格式來拿。要避免獲取到接口可能返回的超長錯誤信息,多半是接口沒處理,這樣可能會導(dǎo)致寫入數(shù)據(jù)失敗,要提前與后臺規(guī)定好。

          處理前端異常


          前端異常異常大多數(shù)就是 js 異常,異常對應(yīng)到 js 的 Error 對象,在處理之前,我們先看 Error 有哪幾種類型:

          • ReferenceError:引用錯誤

          • RangeError:超出有效范圍

          • TypeError:類型錯誤

          • URIError:URI 解析錯誤


          這幾類異常的引用對象都是 Error,因此可以這樣獲?。?/span>

          const handleError = (error: any, type: 1 | 2) {  if(type == 2) {    let err_data = null
              // 監(jiān)測 error 是否是標(biāo)準(zhǔn)類型
              if(error instanceof Error) {      let { name, message } = error
                err_data = {        type: name,        error: message
                }
              } else {
                err_data = {        type'other',        error: JSON.strigify(error)
                }
              }
            }
          }

          上述判斷中,首先判斷異常是否是 Error 的實(shí)例。事實(shí)上絕大部分的代碼異常都是標(biāo)準(zhǔn)的 JS Error,但我們這里還是判斷一下,如果是的話直接獲取異常類型和異常信息,不是的話將異常類型設(shè)置為 other 即可。

          我們隨便寫一個異常代碼,看一下捕獲的結(jié)果:

          function test() {  console.aaa('ccc');
          }test();

          然后捕獲到的異常是這樣的:

          const handleError = (error: any) => {  if (error instanceof Error) {    let { name, message } = error;    console.log(name, message);    // 打印結(jié)果:TypeError console.aaa is not a function
            }
          };


          獲取環(huán)境數(shù)據(jù)


          獲取環(huán)境數(shù)據(jù)的意思是,不管是接口異常還是前端異常,除了異常本身的數(shù)據(jù)之外,我們還需要一些其他信息來幫助我們更快更準(zhǔn)的定位到哪里出錯了。

          這類數(shù)據(jù)我們稱之為 “環(huán)境數(shù)據(jù)”,就是觸發(fā)異常時所在的環(huán)境。比如是誰在哪個頁面的哪個地方觸發(fā)的錯誤,有了這些,我們就能馬上找到錯誤來源,再根據(jù)異常信息解決錯誤。

          環(huán)境數(shù)據(jù)至少包括下面這些:

          • app:應(yīng)用的名稱/標(biāo)識

          • env:應(yīng)用環(huán)境,一般是開發(fā),測試,生產(chǎn)

          • version:應(yīng)用的版本號

          • user_id:觸發(fā)異常的用戶 ID

          • user_name:觸發(fā)異常的用戶名

          • page_route:異常的頁面路由

          • page_title:異常的頁面名稱


          appversion 都是應(yīng)用配置,可以判斷異常出現(xiàn)在哪個應(yīng)用的哪個版本。這兩個字段我建議直接獲取 package.json 下的 name 和 version 屬性,在應(yīng)用升級的時候,及時修改 version 版本號即可。

          其余的字段,需要根據(jù)框架的配置獲取,下面我分別介紹在 Vue 和 React 中如何獲取。

          在 Vue 中


          在 Vue 中獲取用戶信息一般都是直接從 Vuex 里面拿,如果你的用戶信息沒有存到 Vuex 里,從 localStorage 里獲取也是一樣的。

          如果在 Vuex 里,可以這樣實(shí)現(xiàn):

          import store from '@/store'; // vuex 導(dǎo)出目錄let user_info = store.state;let user_id = user_info.id;let user_name = user_info.name;

          用戶信息存在狀態(tài)管理中,頁面路由信息一般是在 vue-router 中定義。前端的路由地址可以直接從 vue-router 中獲取,頁面名稱可以配置在 meta 中,如:

          {  path: '/test',  name: 'test',  meta: {    title: '測試頁面'
            },  component: () => import('@/views/test/Index.vue')
          },


          這樣配置之后,獲取當(dāng)前頁面路由和頁面名稱就簡單了:

          window.vm = new Vue({...})let route = vm.$routelet page_route = route.pathlet page_title = route.meta.title


          最后一步,我們再獲取當(dāng)前環(huán)境。當(dāng)前環(huán)境用一個環(huán)境變量 VUE_APP_ENV 表示,有三個值:


          • dev:開發(fā)環(huán)境

          • test:測試環(huán)境

          • pro:生產(chǎn)環(huán)境


          然后在根目錄下新建三個環(huán)境文件,寫入環(huán)境變量:

          • .env.development:VUE_APP_ENV=dev

          • .env.staging:VUE_APP_ENV=test

          • .env.production:VUE_APP_ENV=pro


          現(xiàn)在獲取 env 環(huán)境時就可以這么獲?。?/span>

          {  env: process.env.VUE_APP_ENV;
          }

          最后一步,執(zhí)行打包時,傳入模式以匹配對應(yīng)的環(huán)境文件:

          # 測試環(huán)境打包$ num run build --mode staging# 生產(chǎn)環(huán)境打包$ num run build --mode production

          獲取到環(huán)境數(shù)據(jù),再拼上異常數(shù)據(jù),我們就準(zhǔn)備好了數(shù)據(jù)等待上報了。

          在 React 中

          和 Vue 一樣,用戶信息可以直接從狀態(tài)管理里拿。因?yàn)?React 中沒有全局獲取當(dāng)前旅游的快捷方式,所以頁面信息我也會放在狀態(tài)管理里面。我用的狀態(tài)管理是 Mobx,獲取方式如下:

          import { TestStore } from '@/stores'; // mobx 導(dǎo)出目錄let { user_info, cur_path, cur_page_title } = TestStore;// 用戶信息:user_info// 頁面信息:cur_path,cur_page_title

          這樣的話,就需要在每次切換頁面時,更新 mobx 里的路由信息,怎么做呢?

          其實(shí)在根路由頁(一般是首頁)的 useEffect 中監(jiān)聽即可:

          import { useLocation } from 'react-router';import { observer, useLocalObservable } from 'mobx-react';import { TestStore } from '@/stores';export default observer(() => {  const { pathname, search } = useLocation();  const test_inst = useLocalObservable(() => TestStore);  useEffect(() => {
              test_inst.setCurPath(pathname, search);
            }, [pathname]);
          });

          獲取到用戶信息和頁面信息,接下來就是當(dāng)前環(huán)境了。和 Vue 一樣通過 --mode 來指定模式,并加載相應(yīng)的環(huán)境變量,只不過設(shè)置方法略有不同。大多數(shù)的 React 項目可能都是用 create-react-app 創(chuàng)建的,我們以此為例介紹怎么修改。

          首先,打開 scripts/start.js 文件,這是執(zhí)行 npm run start 時執(zhí)行的文件,我們在開頭部分第 6 行加代碼:

          process.env.REACT_APP_ENV = 'dev';

          沒錯,我們指定的環(huán)境變量就是 REACT_APP_ENV,因?yàn)橹挥?REACT_ 開頭的環(huán)境變量可被讀取。

          然后再修改 scripts/build.js 文件的第 48 行,修改后如下:

          if (argv.length >= 2 && argv[0] == '--mode') {  switch (argv[1]) {    case 'staging':
                process.env.REACT_APP_ENV = 'test';      break;    case 'production':
                process.env.REACT_APP_ENV = 'pro';      break;    default:
            }
          }

          此時獲取 env 環(huán)境時就可以這么獲取:

          {  env: process.env.REACT_APP_ENV;
          }


          總結(jié)



          經(jīng)過前面一系列操作,我們已經(jīng)比較全面的獲取到了異常數(shù)據(jù),以及發(fā)生異常時到環(huán)境數(shù)據(jù),接下來就是調(diào)用上報接口,將這些數(shù)據(jù)傳給后臺存起來,我們以后查找和追蹤就很方便了。

          如果你也需要前端監(jiān)控,不妨花上半個小時,按照文中介紹的方法收集一下異常數(shù)據(jù),相信對你很有幫助。



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


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

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  亚洲成人资源网 | 亚洲黄色成人在线 | 亚洲无码黄片 | 影音先锋 一区二区 | www.操B在线播放 |