如何做好前端页面异常监控?

在开发工作中难免会出现 bug,一般项目都是测试检查通过后就可以发线上,可是在线上仍旧会出现各种意料之外或者未测试到的问题,这个时候有的用户会向客服反馈说哪里哪里有问题,这是一种被动的错误上报方式,毕竟不是所有的用户都会上报问题,更多的则是出现问题后直接离开我们的 APP。所以异常监控这块就显得越来越重要。

页面异常分类

在我们的项目中我将页面异常分为以下几种情况:

  1. javascript 异常(语法错误,运行时错误,跨域脚本)
  2. 资源加载异常(img js css)
  3. ajax 请求异常
  4. promise 异常
  5. vue 项目中全局异常捕获

接下来介绍如何捕获这些异常

前端页面异常捕获方式

window.onerror 捕获 javascript 异常

/**
 * 捕获javascript异常
 * @param {String}  message    错误信息
 * @param {String}  source     出错文件
 * @param {Number}  lineno     行号
 * @param {Number}  colno      列号
 * @param {Object}  error      Error对象(对象)
 */
window.onerror = function (message, source, lineno, colno, error) {
  console.log("捕获到异常:", { message, source, lineno, colno, error });
};

跨域脚本异常捕获

一般涉及跨域的 js 运行错误时会抛出错误提示 script error.,但没有具体信息(如出错文件,行列号提示等), 可利用资源共享策略来捕获跨域 js 错误
客户端:在 script 标签增加 crossorigin="anonymous"属性
服务端:静态资源响应头 Access-Control-Allow-Origin: *

window.addEventListener(‘error’,cb,true)捕获资源加载异常

img 加载异常时会触发 img.onerror 函数

// 捕获资源加载异常
window.addEventListener(
  "error",
  function (e) {
    const err = e.target.src || e.target.href;
    if (err) {
      console.log("捕获到资源加载异常", err);
    }
  },
  true
);

ajax 接口请求异常捕获

// 统一拦截ajax请求
function ajaxEventTrigger(event) {
  var ajaxEvent = new CustomEvent(event, { detail: this });
  window.dispatchEvent(ajaxEvent);
}

var oldXHR = window.XMLHttpRequest;
function newXHR() {
  var realXHR = new oldXHR();
  realXHR.addEventListener(
    "readystatechange",
    function () {
      ajaxEventTrigger.call(this, "ajaxReadyStateChange");
    },
    false
  );
  return realXHR;
}
window.XMLHttpRequest = newXHR;
var startTime = 0;
var gapTime = 0; // 计算请求延时
window.addEventListener("ajaxReadyStateChange", function (e) {
  var xhr = e.detail;
  var status = xhr.status;
  var readyState = xhr.readyState;
  /**
   * 计算请求延时
   */
  if (readyState === 1) {
    startTime = new Date().getTime();
  }
  if (readyState === 4) {
    gapTime = new Date().getTime() - startTime;
  }
  /**
   * 上报请求信息
   */
  if (readyState === 4) {
    if (status === 200) {
      // 接口正常响应时捕获接口响应耗时
      console.log("接口", xhr.responseURL, "耗时", gapTime);
    } else {
      // 接口异常时捕获异常接口及状态码
      console.log("异常接口", xhr.responseURL, "状态码", status);
    }
  }
});

promise 异常捕获

promise 中的报错顺序是:
如果有 catch 等捕获函数,则走 catch 捕获函数。catch 捕获函数如果没有抛出新的异常,则下一个 then 将会认为没有什么报错,会继续执行。
如果没有 catch 等捕获函数,我们需要注册 window.addEventListener(‘unhandledrejection’) 来处理

/**
 * Promise catch错误上报,需要在使用promise的地方显示调用.catch(),否则不会捕获错误
 */
if (typeof Promise !== "undefined") {
  var _promiseCatch = Promise.prototype.catch;
  Promise.prototype.catch = function (foo) {
    return _promiseCatch.call(this, catCatch(foo));
  };
}
function catCatch(foo) {
  return function (args) {
    let msg = args.stack ? args.stack : args;
    console.log("捕获到catch中的异常", msg);
    foo && foo.call(this, args);
  };
}

/**
 * 监听promise未处理的reject错误, 跨域情况下监控不到
 */
window.addEventListener("unhandledrejection", (event) => {
  console.log("捕获到未处理的promise异常", event.reson);
});

vue 项目全局异常捕获

Vue.config.errorHandler = function (err, vm, info) {
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
  let msg = `错误发生在:${info}中,具体信息:${err.stack}`;
  console.log(msg);
};

捕获到这些异常后我们需要将这些异常上报给服务器,我们直接以请求图片的形式发送上报内容

异常上报

function report(msg) {
  var reportUrl = "http://xxxx/report";
  new Image().src = reportUrl + encodeURIComponent(JSON.stringify(msg));
}