JavaScript架構(gòu)搭建前端監(jiān)控如何采集異常數(shù)據(jù)
前言
前兩篇,我們介紹了為什么前端應(yīng)該有監(jiān)控系統(tǒng),以及搭建前端監(jiān)控的總體步驟,前端監(jiān)控的 Why 和 What 想必你已經(jīng)明白了。接下來(lái)我們解決 How 如何實(shí)現(xiàn)的問(wèn)題。
如果不了解前端監(jiān)控,建議先看前兩篇:
本篇我們介紹,前端如何采集數(shù)據(jù),先從收集異常數(shù)據(jù)開(kāi)始。
什么是異常數(shù)據(jù)?
異常數(shù)據(jù),是指前端在操作頁(yè)面的過(guò)程中,觸發(fā)的執(zhí)行異常或加載異常,此時(shí)瀏覽器會(huì)拋出來(lái)報(bào)錯(cuò)信息。
比如說(shuō)你的前端代碼用了個(gè)未聲明的變量,此時(shí)控制臺(tái)會(huì)打印出紅色錯(cuò)誤,告訴你報(bào)錯(cuò)原因?;蛘呤墙涌谡?qǐng)求出錯(cuò)了,在網(wǎng)絡(luò)面板內(nèi)也能查到異常情況,是請(qǐng)求發(fā)送的異常,還是接口響應(yīng)的異常。
在我們實(shí)際的開(kāi)發(fā)場(chǎng)景中,前端捕獲的異常主要是分兩個(gè)大類(lèi),接口異常 和 前端異常,我們分別看下這兩大類(lèi)異常怎么捕獲。
接口異常
接口異常一定是在請(qǐng)求的時(shí)候觸發(fā)。前端目前大部分的請(qǐng)求是用 axios
發(fā)起的,所以只要獲取 axios 可能發(fā)生的異常即可。
如果你用 Promise 的寫(xiě)法,則用 .catch
捕獲:
axios .post('/test') .then((res) => { console.log(res); }) .catch((err) => { // err 就是捕獲到的錯(cuò)誤對(duì)象 handleError(err); });
如果你用 async/await 的寫(xiě)法,則用 try..catch..
捕獲:
async () => { try { let res = await axios.post('/test'); console.log(res); } catch (err) { // err 就是捕獲到的錯(cuò)誤對(duì)象 handleError(err); } };
當(dāng)捕獲到異常之后,統(tǒng)一交給 handleError
函數(shù)處理,這個(gè)函數(shù)會(huì)將接收到的異常進(jìn)行處理,并調(diào)用 上報(bào)接口
將異常數(shù)據(jù)傳到服務(wù)器,從而完成采集。
上面我們寫(xiě)的異常捕獲,邏輯上是沒(méi)問(wèn)題的,實(shí)操起來(lái)就會(huì)發(fā)現(xiàn)第一道坎:頁(yè)面這么多,難道每個(gè)請(qǐng)求都要包一層 catch 嗎?
是啊,如果我們是新開(kāi)發(fā)一個(gè)項(xiàng)目,在開(kāi)始的時(shí)候就規(guī)定每個(gè)請(qǐng)求要包一層 catch 也無(wú)可厚非,但是如果是在一個(gè)已有的規(guī)模還不小的項(xiàng)目中接入前端監(jiān)控,這時(shí)候在每個(gè)頁(yè)面或每個(gè)請(qǐng)求 catch 顯然是不現(xiàn)實(shí)的。
所以,為了最大程度的降低接入成本,減少侵入性,我們是用第二種方案:在 axios 攔截器中捕獲異常。
前端項(xiàng)目,為了統(tǒng)一處理請(qǐng)求,比如 401 的跳轉(zhuǎn),或者全局錯(cuò)誤提示,都會(huì)在全局寫(xiě)一個(gè) axios 實(shí)例,為這個(gè)實(shí)例添加攔截器,然后在其他頁(yè)面中直接倒入這個(gè)實(shí)例使用,比如:
// 全局請(qǐng)求:src/request/axios.js const instance = axios.create({ baseURL: 'https://api.test.com' timeout: 15000, headers: { 'Content-Type': 'application/json', }, }) export default instance
然后在具體的頁(yè)面中這樣發(fā)起請(qǐng)求:
// a 頁(yè)面:src/page/a.jsx import http from '@/src/request/axios.js'; async () => { let res = await http.post('/test'); console.log(res); };
這樣的話,我們發(fā)現(xiàn)每個(gè)頁(yè)面的請(qǐng)求都會(huì)走全局 axios 實(shí)例,所以我們只需要在全局請(qǐng)求的位置捕獲異常即可,就不需要在每個(gè)頁(yè)面捕獲了,這樣接入成本會(huì)大大降低。
按照這個(gè)方案,結(jié)下來(lái)我們?cè)?src/request/axios.js
這個(gè)文件中動(dòng)手實(shí)施。
攔截器中捕獲異常
首先我們?yōu)?axios 添加響應(yīng)攔截器:
// 響應(yīng)攔截器 instance.interceptors.response.use( (response) => { return response.data; }, (error) => { // 發(fā)生異常會(huì)走到這里 if (error.response) { let response = error.response; if (response.status >= 400) { handleError(response); } } else { handleError(null); } return Promise.reject(error); }, );
響應(yīng)攔截器的第二個(gè)參數(shù)是在發(fā)生錯(cuò)誤時(shí)執(zhí)行的函數(shù),參數(shù)就是異常。我們首先要判斷是否存在 error.response
,存在就說(shuō)明接口有響應(yīng),也就是接口通了,但是返回錯(cuò)誤;不存在則說(shuō)明接口沒(méi)通,請(qǐng)求一直掛起,多數(shù)是接口崩潰了。
如果有響應(yīng),首先獲取狀態(tài)碼,根據(jù)狀態(tài)碼來(lái)判斷什么時(shí)候需要收集異常。上面的判斷方式簡(jiǎn)單粗暴,只要狀態(tài)碼大于 400 就視為一個(gè)異常,拿到響應(yīng)數(shù)據(jù),并執(zhí)行上報(bào)邏輯。
如果沒(méi)有響應(yīng),可以看作是接口超時(shí)異常,調(diào)用異常處理函數(shù)時(shí)傳一個(gè) null
即可。
前端異常
上面我們介紹了在 axios 攔截器中如何捕獲接口異常,這部分我們?cè)俳榻B如何捕獲前端異常。
前端代碼捕獲異常,最常用的方式就是用 try..catch.. 了,任意同步代碼塊都可以放到 try
塊中,只要發(fā)生異常就會(huì)執(zhí)行 catch:
try { // 任意同步代碼 } catch (err) { console.log(err); }
上面說(shuō)“任意同步代碼”而不是“任意代碼”,主要是普通的 Promise 寫(xiě)法 try..catch.. 是捕獲不到的,只能用 .catch()
捕獲,如:
try { Promise.reject(new Error('出錯(cuò)了')).catch((err) => console.log('1:', err)); } catch (err) { console.log('2:', err); }
把這段代碼丟進(jìn)瀏覽器,打印結(jié)果是:
1: Error: 出錯(cuò)了
很明顯只是 .catch 捕獲到了異常。不過(guò)與上面接口異常的邏輯一樣,這種方式處理當(dāng)前頁(yè)面異常沒(méi)什么問(wèn)題,但從整個(gè)應(yīng)用來(lái)看,這樣捕獲異常侵入性強(qiáng),接入成本高,所以我們的思路依然是全局捕獲。
全局捕獲 js 的異常也比較簡(jiǎn)單,用 window.addEventLinstener('error') 即可:
// js 錯(cuò)誤捕獲 window.addEventListener('error', (error) => { // error 就是js的異常 });
為啥不用 window.onerror ?
這里很多小伙伴有疑問(wèn),為什么不用 window.onerror
全局監(jiān)聽(tīng)呢?window.addEventLinstener('error') 和 window.onerror
有什么區(qū)別呢?
首先這兩個(gè)函數(shù)功能基本一致,都可以全局捕獲 js 異常。但是有一類(lèi)異常叫做 資源加載異常,就是在代碼中引用了不存在的圖片,js,css 等靜態(tài)資源導(dǎo)致的異常,比如:
const loadCss = ()=> { let link = document.createElement('link') link.type = 'text/css' link.rel = 'stylesheet' link. 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í)行到這里肯定會(huì)報(bào)一個(gè)資源找不到的錯(cuò)誤。但是默認(rèn)情況下,上面兩種 window 對(duì)象上的全局監(jiān)聽(tīng)函數(shù)都監(jiān)聽(tīng)不到這類(lèi)異常。
因?yàn)橘Y源加載的異常只會(huì)在當(dāng)前元素觸發(fā),異常不會(huì)冒泡到 window,因此監(jiān)聽(tīng) window 上的異常是捕捉不到的。那怎么辦呢?
如果你熟悉 DOM 事件你就會(huì)明白,既然冒泡階段監(jiān)聽(tīng)不到,那么在捕獲階段一定能監(jiān)聽(tīng)到。
方法就是給 window.addEventListene
函數(shù)指定第三個(gè)參數(shù),很簡(jiǎn)單就是 true
,表示該監(jiān)聽(tīng)函數(shù)會(huì)在捕獲階段執(zhí)行,這樣就能監(jiān)聽(tīng)到資源加載異常了。
// 捕獲階段全局監(jiān)聽(tīng) window.addEventListene( 'error', (error) => { if (error.target != window) { console.log(error.target.tagName, error.target.src); } handleError(error); }, true, );
上述方式可以很輕松的監(jiān)聽(tīng)到圖片加載異常,這就是為什么更推薦 window.addEventListene
的原因。不過(guò)要記得,第三個(gè)參數(shù)設(shè)為 true
,監(jiān)聽(tīng)事件捕獲,就可以全局捕獲到 JS 異常和資源加載異常。
需要特別注意,window.addEventListene
同樣不能捕獲 Promise 異常。不管是 Promise.then()
寫(xiě)法還是 async/await
寫(xiě)法,發(fā)生異常時(shí)都不能捕獲。
因此,我們還需要全局監(jiān)聽(tīng)一個(gè) unhandledrejection
函數(shù)來(lái)捕獲未處理的 Promise 異常。
// promise 錯(cuò)誤捕獲 window.addEventListener('unhandledrejection', (error) => { // 打印異常原因 console.log(error.reason); handleError(error); // 阻止控制臺(tái)打印 error.preventDefault(); });
unhandledrejection
事件會(huì)在 Promise 發(fā)生異常并且沒(méi)有指定 catch
的時(shí)候觸發(fā),相當(dāng)于一個(gè)全局的 Promise 異常兜底方案。這個(gè)函數(shù)會(huì)捕捉到運(yùn)行時(shí)意外發(fā)生的 Promise 異常,這對(duì)我們排錯(cuò)非常有用。
默認(rèn)情況下,Promise 發(fā)生異常且未被 catch 時(shí),會(huì)在控制臺(tái)打印異常。如果我們想阻止異常打印,可以用上面的 error.preventDefault()
方法。
異常處理函數(shù)
前面我們?cè)诓东@到異常時(shí)調(diào)用了一個(gè)異常處理函數(shù) handleError
,所有的異常和上報(bào)邏輯統(tǒng)一在這個(gè)函數(shù)內(nèi)處理,接下來(lái)我們實(shí)現(xiàn)這個(gè)函數(shù)。
const handleError = (error: any, type: 1 | 2) { if(type == 1) { // 處理接口異常 } if(type == 2) { // 處理前端異常 } }
為了區(qū)分異常類(lèi)型,函數(shù)新加了第二個(gè)參數(shù) type 表示當(dāng)前異常屬于前端還是接口。在不同的場(chǎng)景中使用如下:
- 處理前端異常:
handleError(error, 1)
- 處理接口異常:
handleError(error, 2)
處理接口異常
處理接口異常,我們需要將拿到的 error 參數(shù)解析,然后取到需要的數(shù)據(jù)。接口異常一般需要的數(shù)據(jù)字段如下:
code
:http 狀態(tài)碼url
:接口請(qǐng)求地址method
:接口請(qǐng)求方法params
:接口請(qǐng)求參數(shù)error
:接口報(bào)錯(cuò)信息
這些字段都可以在 error 參數(shù)中獲取,方法如下:
const handleError = (error: any, type: 1 | 2) { if(type == 1) { // 此時(shí)的 error 響應(yīng),它的 config 字段中包含請(qǐng)求信息 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 對(duì)象中的 params
表示 GET 請(qǐng)求的 query 參數(shù),data
表示 POST 請(qǐng)求的 body 參數(shù),所以我在處理參數(shù)的時(shí)候,將這兩個(gè)參數(shù)合并為一個(gè),用一個(gè)屬性 params 來(lái)表示。
params: { query: params, body: data }
還有一個(gè) error
屬性表示錯(cuò)誤信息,這個(gè)獲取方式要根據(jù)你的接口返回格式來(lái)拿。要避免獲取到接口可能返回的超長(zhǎng)錯(cuò)誤信息,多半是接口沒(méi)處理,這樣可能會(huì)導(dǎo)致寫(xiě)入數(shù)據(jù)失敗,要提前與后臺(tái)規(guī)定好。
處理前端異常
前端異常異常大多數(shù)就是 js 異常,異常對(duì)應(yīng)到 js 的 Error
對(duì)象,在處理之前,我們先看 Error 有哪幾種類(lèi)型:
ReferenceError
:引用錯(cuò)誤RangeError
:超出有效范圍TypeError
:類(lèi)型錯(cuò)誤URIError
:URI 解析錯(cuò)誤
這幾類(lèi)異常的引用對(duì)象都是 Error
,因此可以這樣獲取:
const handleError = (error: any, type: 1 | 2) { if(type == 2) { let err_data = null // 監(jiān)測(cè) error 是否是標(biāo)準(zhǔn)類(lèi)型 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,但我們這里還是判斷一下,如果是的話直接獲取異常類(lèi)型和異常信息,不是的話將異常類(lèi)型設(shè)置為 other
即可。
我們隨便寫(xiě)一個(gè)異常代碼,看一下捕獲的結(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ù)之外,我們還需要一些其他信息來(lái)幫助我們更快更準(zhǔn)的定位到哪里出錯(cuò)了。
這類(lèi)數(shù)據(jù)我們稱之為 “環(huán)境數(shù)據(jù)”,就是觸發(fā)異常時(shí)所在的環(huán)境。比如是誰(shuí)在哪個(gè)頁(yè)面的哪個(gè)地方觸發(fā)的錯(cuò)誤,有了這些,我們就能馬上找到錯(cuò)誤來(lái)源,再根據(jù)異常信息解決錯(cuò)誤。
環(huán)境數(shù)據(jù)至少包括下面這些:
app
:應(yīng)用的名稱/標(biāo)識(shí)env
:應(yīng)用環(huán)境,一般是開(kāi)發(fā),測(cè)試,生產(chǎn)version
:應(yīng)用的版本號(hào)user_id
:觸發(fā)異常的用戶 IDuser_name
:觸發(fā)異常的用戶名page_route
:異常的頁(yè)面路由page_title
:異常的頁(yè)面名稱
app
和 version
都是應(yīng)用配置,可以判斷異常出現(xiàn)在哪個(gè)應(yīng)用的哪個(gè)版本。這兩個(gè)字段我建議直接獲取 package.json
下的 name
和 version
屬性,在應(yīng)用升級(jí)的時(shí)候,及時(shí)修改 version 版本號(hào)即可。
其余的字段,需要根據(jù)框架的配置獲取,下面我分別介紹在 Vue 和 React 中如何獲取。
在 Vue 中
在 Vue 中獲取用戶信息一般都是直接從 Vuex 里面拿,如果你的用戶信息沒(méi)有存到 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)管理中,頁(yè)面路由信息一般是在 vue-router
中定義。前端的路由地址可以直接從 vue-router 中獲取,頁(yè)面名稱可以配置在 meta
中,如:
{ path: '/test', name: 'test', meta: { title: '測(cè)試頁(yè)面' }, component: () => import('@/views/test/Index.vue') },
這樣配置之后,獲取當(dāng)前頁(yè)面路由和頁(yè)面名稱就簡(jiǎn)單了:
window.vm = new Vue({...}) let route = vm.$route let page_route = route.path let page_title = route.meta.title
最后一步,我們?cè)佾@取當(dāng)前環(huán)境。當(dāng)前環(huán)境用一個(gè)環(huán)境變量 VUE_APP_ENV
表示,有三個(gè)值:
dev
:開(kāi)發(fā)環(huán)境test
:測(cè)試環(huán)境pro
:生產(chǎn)環(huán)境
然后在根目錄下新建三個(gè)環(huán)境文件,寫(xiě)入環(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)境時(shí)就可以這么獲?。?/p>
{ env: process.env.VUE_APP_ENV; }
最后一步,執(zhí)行打包時(shí),傳入模式以匹配對(duì)應(yīng)的環(huán)境文件:
# 測(cè)試環(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ù)等待上報(bào)了。
在 React 中
和 Vue 一樣,用戶信息可以直接從狀態(tài)管理里拿。因?yàn)?React 中沒(méi)有全局獲取當(dāng)前旅游的快捷方式,所以頁(yè)面信息我也會(huì)放在狀態(tài)管理里面。我用的狀態(tài)管理是 Mobx,獲取方式如下:
import { TestStore } from '@/stores'; // mobx 導(dǎo)出目錄 let { user_info, cur_path, cur_page_title } = TestStore; // 用戶信息:user_info // 頁(yè)面信息:cur_path,cur_page_title
這樣的話,就需要在每次切換頁(yè)面時(shí),更新 mobx 里的路由信息,怎么做呢?
其實(shí)在根路由頁(yè)(一般是首頁(yè))的 useEffect
中監(jiān)聽(tīng)即可:
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]); });
獲取到用戶信息和頁(yè)面信息,接下來(lái)就是當(dāng)前環(huán)境了。和 Vue 一樣通過(guò) --mode
來(lái)指定模式,并加載相應(yīng)的環(huán)境變量,只不過(guò)設(shè)置方法略有不同。大多數(shù)的 React 項(xiàng)目可能都是用 create-react-app
創(chuàng)建的,我們以此為例介紹怎么修改。
首先,打開(kāi) scripts/start.js
文件,這是執(zhí)行 npm run start 時(shí)執(zhí)行的文件,我們?cè)陂_(kāi)頭部分第 6 行加代碼:
process.env.REACT_APP_ENV = 'dev';
沒(méi)錯(cuò),我們指定的環(huán)境變量就是 REACT_APP_ENV
,因?yàn)橹挥?REACT_
開(kāi)頭的環(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: } }
此時(shí)獲取 env
環(huán)境時(shí)就可以這么獲取:
{ env: process.env.REACT_APP_ENV; }
總結(jié)
經(jīng)過(guò)前面一系列操作,我們已經(jīng)比較全面的獲取到了異常數(shù)據(jù),以及發(fā)生異常時(shí)到環(huán)境數(shù)據(jù),接下來(lái)就是調(diào)用上報(bào)接口,將這些數(shù)據(jù)傳給后臺(tái)存起來(lái),我們以后查找和追蹤就很方便了。
如果你也需要前端監(jiān)控,不妨花上半個(gè)小時(shí),按照文中介紹的方法收集一下異常數(shù)據(jù),相信對(duì)你很有幫助。
以上就是JavaScript架構(gòu)搭建前端監(jiān)控如何采集異常數(shù)據(jù)的詳細(xì)內(nèi)容,更多關(guān)于JavaScript前端監(jiān)控采集異常數(shù)據(jù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 獲取javascript 里的數(shù)據(jù)
這篇文章主要介紹了微信小程序 獲取javascript 里的數(shù)據(jù)的相關(guān)資料,這里通過(guò)實(shí)例來(lái)說(shuō)明如何獲取javascript里的數(shù)據(jù),希望能幫助到大家,需要的朋友可以參考下2017-08-08微信小程序 簡(jiǎn)單實(shí)例(閱讀器)的實(shí)例開(kāi)發(fā)
這篇文章主要介紹了微信小程序 簡(jiǎn)單實(shí)例(閱讀器)的實(shí)例開(kāi)發(fā)的相關(guān)資料,需要的朋友可以參考下2016-09-09JS處理數(shù)據(jù)實(shí)現(xiàn)分頁(yè)功能
這篇文章介紹了JS處理數(shù)據(jù)實(shí)現(xiàn)分頁(yè)功能的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-01-01JavaScript的模塊化開(kāi)發(fā)框架Sea.js上手指南
Sea.js的目的是追求簡(jiǎn)單的代碼書(shū)寫(xiě)和組織方式,Sea.js并沒(méi)有過(guò)多功能而是主要對(duì)前端程序的部署結(jié)構(gòu)作出約束,下面我們就來(lái)看一下JavaScript的模塊化開(kāi)發(fā)框架Sea.js上手指南:2016-05-05微信小程序 less文件編譯成wxss文件實(shí)現(xiàn)辦法
這篇文章主要介紹了微信小程序 less文件編譯成微信小程序wxss文件實(shí)現(xiàn)辦法的相關(guān)資料,這里給出具體實(shí)現(xiàn)方法,需要的朋友可以參考下2016-12-12前端項(xiàng)目中監(jiān)聽(tīng)localStorage的變化
這篇文章主要為大家介紹了前端項(xiàng)目中監(jiān)聽(tīng)localStorage的變化的解決思路詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06微信小程序之前臺(tái)循環(huán)數(shù)據(jù)綁定
這篇文章主要介紹了微信小程序之前臺(tái)循環(huán)數(shù)據(jù)綁定的相關(guān)資料,這里提供實(shí)例幫助大家學(xué)習(xí)理解這部分內(nèi)容,需要的朋友可以參考下2017-08-08