JavaScript架构搭建前端监控如何采集异常数据


Posted in Javascript onJune 25, 2022

前言

前两篇,我们介绍了为什么前端应该有监控系统,以及搭建前端监控的总体步骤,前端监控的 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.cssbbb.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:触发异常的用户 ID
  • user_name:触发异常的用户名
  • page_route:异常的页面路由
  • page_title:异常的页面名称

appversion 都是应用配置,可以判断异常出现在哪个应用的哪个版本。这两个字段我建议直接获取 package.json 下的 nameversion 属性,在应用升级的时候,及时修改 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;
}

总结

经过前面一系列操作,我们已经比较全面的获取到了异常数据,以及发生异常时到环境数据,接下来就是调用上报接口,将这些数据传给后台存起来,我们以后查找和追踪就很方便了。

如果你也需要前端监控,不妨花上半个小时,按照文中介绍的方法收集一下异常数据,相信对你很有帮助。

以上就是JavaScript架构搭建前端监控如何采集异常数据的详细内容,更多关于JavaScript前端监控采集异常数据的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
兼容多浏览器的字幕特效Marquee的通用js类
Jul 20 Javascript
下载网站打开页面后间隔多少时间才显示下载链接地址的代码
Apr 25 Javascript
JS之Date对象和获取系统当前时间详解
Jan 13 Javascript
基于jQuery的判断iPad、iPhone、Android是横屏还是竖屏的代码
May 11 Javascript
Jquery效果大全之制作电脑健康体检得分特效附源码下载
Nov 02 Javascript
使用纯JS代码判断字符串中有多少汉字的实现方法(超简单实用)
Nov 12 Javascript
移动端日期插件Mobiscroll.js使用详解
Dec 19 Javascript
JS 调用微信扫一扫功能
Dec 22 Javascript
vuejs使用$emit和$on进行组件之间的传值的示例
Oct 04 Javascript
Vue-cli3简单使用(图文步骤)
Apr 30 Javascript
vue中实现Monaco Editor自定义提示功能
Jul 05 Javascript
jQuery实现推拉门效果
Oct 19 jQuery
webpack介绍使用配置教程详解webpack介绍和使用
Jun 25 #Javascript
JavaScript设计模式之原型模式详情
Jun 21 #Javascript
js前端设计模式优化50%表单校验代码示例
微前端qiankun改造日渐庞大的项目教程
Jun 21 #Javascript
JavaScript架构localStorage特殊场景下二次封装操作
Jun 21 #Javascript
js前端图片加载异常兜底方案
Jun 21 #Javascript
JavaScript中10个Reduce常用场景技巧
Jun 21 #Javascript
You might like
web server使用php生成web页面的三种方法总结
2013/10/28 PHP
Laravel 4.2 中队列服务(queue)使用感受
2014/10/30 PHP
php微信公众号开发(2)百度BAE搭建和数据库使用
2016/12/15 PHP
关于php支持的协议与封装协议总结(推荐)
2017/11/17 PHP
laravel 时间格式转时间戳的例子
2019/10/11 PHP
javascript 硬盘序列号+其它硬件信息
2008/12/23 Javascript
JS判断移动端访问设备并加载对应CSS样式
2014/06/13 Javascript
详解JavaScript中shift()方法的使用
2015/06/09 Javascript
Eclipse引入jquery报错如何解决
2015/12/01 Javascript
使用BootStrap建立响应式网页——通栏轮播图(carousel)
2016/12/21 Javascript
bootstrap如何让dropdown menu按钮式下拉框长度一致
2017/04/10 Javascript
js事件委托和事件代理案例分享
2017/07/25 Javascript
利用JS hash制作单页Web应用的方法详解
2017/10/10 Javascript
基于Vue 2.0的模块化前端 UI 组件库小结
2017/12/21 Javascript
关于Mac下安装nodejs、npm和cnpm的教程
2018/04/11 NodeJs
vue-cli3.0如何使用CDN区分开发、生产、预发布环境
2018/11/22 Javascript
js中火星坐标、百度坐标、WGS84坐标转换实现方法示例
2020/03/02 Javascript
基于JavaScript实现大文件上传后端代码实例
2020/08/18 Javascript
vue中watch的用法汇总
2020/12/28 Vue.js
[01:32:50]DOTA2-DPC中国联赛 正赛 DLG vs XG BO3 第一场 1月25日
2021/03/11 DOTA
Python解析最简单的验证码
2016/01/07 Python
python使用 HTMLTestRunner.py生成测试报告
2017/10/20 Python
解析Python中的eval()、exec()及其相关函数
2017/12/20 Python
python cumsum函数的具体使用
2019/07/29 Python
django-初始配置(纯手写)详解
2019/07/30 Python
自定义django admin model表单提交的例子
2019/08/23 Python
python实现树的深度优先遍历与广度优先遍历详解
2019/10/26 Python
Python中openpyxl实现vlookup函数的实例
2020/10/28 Python
Kathmandu美国网站:新西兰户外运动品牌
2019/03/23 全球购物
香港莎莎官网Sasa.com:亚洲著名国际化妆品商城
2019/11/10 全球购物
卫生系统先进事迹
2014/05/13 职场文书
公务员政审单位鉴定材料
2014/05/16 职场文书
四风问题班子对照检查材料
2014/09/27 职场文书
单位接收函格式
2015/01/30 职场文书
先进个人自荐书
2015/03/06 职场文书
Python+Matplotlib+LaTeX玩转数学公式
2022/02/24 Python