大家好,我是杨胜利。
前两篇,咱们介绍了为什么前端应该有监控零碎,以及搭建前端监控的总体步骤,前端监控的 Why 和 What 想必你曾经明确了。接下来咱们解决 How 如何实现的问题。
如果不理解前端监控,倡议先看前两篇:
- 为什么前端不能没有监控零碎?
- 前端监控的总体搭建步骤
本篇咱们介绍,前端如何采集数据,先从收集异样数据开始。
什么是异样数据?
异样数据,是指前端在操作页面的过程中,触发的执行异样或加载异样,此时浏览器会抛出来报错信息。
比如说你的前端代码用了个未声明的变量,此时控制台会打印出红色谬误,通知你报错起因。或者是接口申请出错了,在网络面板内也能查到异常情况,是申请发送的异样,还是接口响应的异样。
在咱们理论的开发场景中,前端捕捉的异样次要是分两个大类,接口异样
和 前端异样
,咱们别离看下这两大类异样怎么捕捉。
接口异样
接口异样肯定是在申请的时候触发。前端目前大部分的申请是用 axios
发动的,所以只有获取 axios 可能产生的异样即可。
如果你用 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);
}
};
当捕捉到异样之后,对立交给 handleError
函数解决,这个函数会将接管到的异样进行解决,并调用 上报接口
将异样数据传到服务器,从而实现采集。
下面咱们写的异样捕捉,逻辑上是没问题的,实操起来就会发现第一道坎:页面这么多,难道每个申请都要包一层 catch 吗?
是啊,如果咱们是新开发一个我的项目,在开始的时候就规定每个申请要包一层 catch 也无可非议,然而如果是在一个已有的规模还不小的我的项目中接入前端监控,这时候在每个页面或每个申请 catch 显然是不事实的。
所以,为了最大水平的升高接入老本,缩小侵入性,咱们是用第二种计划:在 axios 拦截器中捕捉异样。
前端我的项目,为了对立解决申请,比方 401 的跳转,或者全局谬误提醒,都会在全局写一个 axios 实例,为这个实例增加拦截器,而后在其余页面中间接倒入这个实例应用,比方:
// 全局申请:src/request/axios.js
const instance = axios.create({
baseURL: 'https://api.test.com'
timeout: 15000,
headers: {'Content-Type': 'application/json',},
})
export default instance
而后在具体的页面中这样发动申请:
// a 页面:src/page/a.jsx
import http from '@/src/request/axios.js';
async () => {let res = await http.post('/test');
console.log(res);
};
这样的话,咱们发现每个页面的申请都会走全局 axios 实例,所以咱们只须要在全局申请的地位捕捉异样即可,就不须要在每个页面捕捉了,这样接入老本会大大降低。
依照这个计划,结下来咱们在 src/request/axios.js
这个文件中入手施行。
拦截器中捕捉异样
首先咱们为 axios 增加响应拦截器:
// 响应拦截器
instance.interceptors.response.use((response) => {return response.data;},
(error) => {
// 产生异样会走到这里
if (error.response) {
let response = error.response;
if (response.status >= 400) {handleError(response);
}
} else {handleError(null);
}
return Promise.reject(error);
},
);
响应拦截器的第二个参数是在产生谬误时执行的函数,参数就是异样。咱们首先要判断是否存在 error.response
,存在就阐明接口有响应,也就是接口通了,然而返回谬误;不存在则阐明接口没通,申请始终挂起,少数是接口解体了。
如果有响应,首先获取状态码,依据状态码来判断什么时候须要收集异样。下面的判断形式简略粗犷,只有状态码大于 400 就视为一个异样,拿到响应数据,并执行上报逻辑。
如果没有响应,能够看作是接口超时异样,调用异样处理函数时传一个 null
即可。
前端异样
下面咱们介绍了在 axios 拦截器中如何捕捉接口异样,这部分咱们再介绍如何捕捉前端异样。
前端代码捕捉异样,最罕用的形式就是用 try..catch.. 了,任意同步代码块都能够放到 try
块中,只有产生异样就会执行 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);
}
把这段代码丢进浏览器,打印后果是:
1:Error: 出错了
很显著只是 .catch 捕捉到了异样。不过与下面接口异样的逻辑一样,这种形式解决以后页面异样没什么问题,但从整个利用来看,这样捕捉异样侵入性强,接入老本高,所以咱们的思路仍然是全局捕捉。
全局捕捉 js 的异样也比较简单,用 window.addEventLinstener('error')
即可:
// js 谬误捕捉
window.addEventListener('error', (error) => {// error 就是 js 的异样});
为啥不必 window.onerror?
这里很多小伙伴有疑难,为什么不必 window.onerror
全局监听呢?window.addEventLinstener('error')
和 window.onerror
有什么区别呢?
首先这两个函数性能基本一致,都能够全局捕捉 js 异样。然而有一类异样叫做 资源加载异样
,就是在代码中援用了不存在的图片,js,css 等动态资源导致的异样,比方:
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 执行到这里必定会报一个资源找不到的谬误。然而默认状况下,下面两种 window 对象上的全局监听函数都监听不到这类异样。
因为资源加载的异样只会在以后元素触发,异样不会冒泡到 window,因而监听 window 上的异样是捕获不到的。那怎么办呢?
如果你相熟 DOM 事件你就会明确,既然冒泡阶段监听不到,那么在捕捉阶段肯定能监听到。
办法就是给 window.addEventListene
函数指定第三个参数,很简略就是 true
,示意该监听函数会在捕捉阶段执行,这样就能监听到资源加载异样了。
// 捕捉阶段全局监听
window.addEventListene(
'error',
(error) => {if (error.target != window) {console.log(error.target.tagName, error.target.src);
}
handleError(error);
},
true,
);
上述形式能够很轻松的监听到图片加载异样,这就是为什么更举荐 window.addEventListene
的起因。不过要记得,第三个参数设为 true
,监听事件捕捉,就能够全局捕捉到 JS 异样和资源加载异样。
须要特地留神,window.addEventListene
同样不能捕捉 Promise 异样。不论是 Promise.then()
写法还是 async/await
写法,产生异样时都不能捕捉。
因而,咱们还须要全局监听一个 unhandledrejection
函数来捕捉未解决的 Promise 异样。
// promise 谬误捕捉
window.addEventListener('unhandledrejection', (error) => {
// 打印异样起因
console.log(error.reason);
handleError(error);
// 阻止控制台打印
error.preventDefault();});
unhandledrejection
事件会在 Promise 产生异样并且没有指定 catch
的时候触发,相当于一个全局的 Promise 异样兜底计划。这个函数会捕捉到运行时意外产生的 Promise 异样,这对咱们排错十分有用。
默认状况下,Promise 产生异样且未被 catch 时,会在控制台打印异样。如果咱们想阻止异样打印,能够用下面的 error.preventDefault()
办法。
异样处理函数
后面咱们在捕捉到异样时调用了一个异样处理函数 handleError
,所有的异样和上报逻辑对立在这个函数内解决,接下来咱们实现这个函数。
const handleError = (error: any, type: 1 | 2) {if(type == 1) {// 解决接口异样}
if(type == 2) {// 解决前端异样}
}
为了辨别异样类型,函数新加了第二个参数 type 示意以后异样属于前端还是接口。在不同的场景中应用如下:
- 解决前端异样:
handleError(error, 1)
- 解决接口异样:
handleError(error, 2)
解决接口异样
解决接口异样,咱们须要将拿到的 error 参数解析,而后取到须要的数据。接口异样个别须要的数据字段如下:
code
:http 状态码url
:接口申请地址method
:接口申请办法params
:接口申请参数error
:接口报错信息
这些字段都能够在 error 参数中获取,办法如下:
const handleError = (error: any, type: 1 | 2) {if(type == 1) {
// 此时的 error 响应,它的 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 参数,data
示意 POST 申请的 body 参数,所以我在解决参数的时候,将这两个参数合并为一个,用一个属性 params 来示意。
params: {query: params, body: data}
还有一个 error
属性示意错误信息,这个获取形式要依据你的接口返回格局来拿。要防止获取到接口可能返回的超长错误信息,多半是接口没解决,这样可能会导致写入数据失败,要提前与后盾规定好。
解决前端异样
前端异样异样大多数就是 js 异样,异样对应到 js 的 Error
对象,在解决之前,咱们先看 Error 有哪几种类型:
ReferenceError
:援用谬误RangeError
:超出无效范畴TypeError
:类型谬误URIError
:URI 解析谬误
这几类异样的援用对象都是 Error
,因而能够这样获取:
const handleError = (error: any, type: 1 | 2) {if(type == 2) {
let err_data = null
// 监测 error 是否是规范类型
if(error instanceof Error) {let { name, message} = error
err_data = {
type: name,
error: message
}
} else {
err_data = {
type: 'other',
error: JSON.strigify(error)
}
}
}
}
上述判断中,首先判断异样是否是 Error
的实例。事实上绝大部分的代码异样都是规范的 JS Error,但咱们这里还是判断一下,如果是的话间接获取异样类型和异样信息,不是的话将异样类型设置为 other
即可。
咱们轻易写一个异样代码,看一下捕捉的后果:
function test() {console.aaa('ccc');
}
test();
而后捕捉到的异样是这样的:
const handleError = (error: any) => {if (error instanceof Error) {let { name, message} = error;
console.log(name, message);
// 打印后果:TypeError console.aaa is not a function
}
};
获取环境数据
获取环境数据的意思是,不论是接口异样还是前端异样,除了异样自身的数据之外,咱们还须要一些其余信息来帮忙咱们更快更准的定位到哪里出错了。
这类数据咱们称之为“环境数据”,就是触发异样时所在的环境。比方是谁在哪个页面的哪个中央触发的谬误,有了这些,咱们就能马上找到谬误起源,再依据异样信息解决谬误。
环境数据至多包含上面这些:
app
:利用的名称 / 标识env
:应用环境,个别是开发,测试,生产version
:利用的版本号user_id
:触发异样的用户 IDuser_name
:触发异样的用户名page_route
:异样的页面路由page_title
:异样的页面名称
app
和 version
都是利用配置,能够判断异样呈现在哪个利用的哪个版本。这两个字段我倡议间接获取 package.json
下的 name
和 version
属性,在利用降级的时候,及时批改 version 版本号即可。
其余的字段,须要依据框架的配置获取,上面我别离介绍在 Vue 和 React 中如何获取。
在 Vue 中
在 Vue 中获取用户信息个别都是间接从 Vuex 外面拿,如果你的用户信息没有存到 Vuex 里,从 localStorage 里获取也是一样的。
如果在 Vuex 里,能够这样实现:
import store from '@/store'; // vuex 导出目录
let user_info = store.state;
let user_id = user_info.id;
let user_name = user_info.name;
用户信息存在状态治理中,页面路由信息个别是在 vue-router
中定义。前端的路由地址能够间接从 vue-router 中获取,页面名称能够配置在 meta
中,如:
{
path: '/test',
name: 'test',
meta: {title: '测试页面'},
component: () => import('@/views/test/Index.vue')
},
这样配置之后,获取以后页面路由和页面名称就简略了:
window.vm = new Vue({...})
let route = vm.$route
let page_route = route.path
let page_title = route.meta.title
最初一步,咱们再获取以后环境。以后环境用一个环境变量 VUE_APP_ENV
示意,有三个值:
dev
:开发环境test
:测试环境pro
:生产环境
而后在根目录下新建三个环境文件,写入环境变量:
.env.development
:VUE_APP_ENV=dev.env.staging
:VUE_APP_ENV=test.env.production
:VUE_APP_ENV=pro
当初获取 env
环境时就能够这么获取:
{env: process.env.VUE_APP_ENV;}
最初一步,执行打包时,传入模式以匹配对应的环境文件:
# 测试环境打包
$ num run build --mode staging
# 生产环境打包
$ num run build --mode production
获取到环境数据,再拼上异样数据,咱们就筹备好了数据期待上报了。
在 React 中
和 Vue 一样,用户信息能够间接从状态治理里拿。因为 React 中没有全局获取以后游览的快捷方式,所以页面信息我也会放在状态治理外面。我用的状态治理是 Mobx,获取形式如下:
import {TestStore} from '@/stores'; // mobx 导出目录
let {user_info, cur_path, cur_page_title} = TestStore;
// 用户信息:user_info
// 页面信息:cur_path,cur_page_title
这样的话,就须要在每次切换页面时,更新 mobx 里的路由信息,怎么做呢?
其实在根路由页(个别是首页)的 useEffect
中监听即可:
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]);
});
获取到用户信息和页面信息,接下来就是以后环境了。和 Vue 一样通过 --mode
来指定模式,并加载相应的环境变量,只不过设置办法略有不同。大多数的 React 我的项目可能都是用 create-react-app
创立的,咱们以此为例介绍怎么批改。
首先,关上 scripts/start.js
文件,这是执行 npm run start 时执行的文件,咱们在结尾局部第 6 行加代码:
process.env.REACT_APP_ENV = 'dev';
没错,咱们指定的环境变量就是 REACT_APP_ENV
,因为只有 REACT_
结尾的环境变量可被读取。
而后再批改 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
环境时就能够这么获取:
{env: process.env.REACT_APP_ENV;}
总结
通过后面一系列操作,咱们曾经比拟全面的获取到了异样数据,以及产生异样时到环境数据,接下来就是调用上报接口,将这些数据传给后盾存起来,咱们当前查找和追踪就很不便了。
如果你也须要前端监控,无妨花上半个小时,依照文中介绍的办法收集一下异样数据,置信对你很有帮忙。