Vue项目异常上报实践

1,947 阅读4分钟

大多数web的异常报错,都可以在浏览器通过控制台查看。但是如果混合开发,h5页面在原生中,就不好确认是否报异常了。

这时,异常的收集和上报就至关重要了。

同步异常收集

image.png Vue的异常信息都是通过Vue.config.errorHandler方法处理。可以在里面写请求上报。

其原理就是vue用try/catch捕获异常,在catch里调用handleError方法,然后再从handleError里回调定义的Vue.config.errorHandler方法。

// src/core/util/error.js
export function handleError (err: Error, vm: any, info: string) {
  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
  // See: https://github.com/vuejs/vuex/issues/1505
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      while ((cur = cur.$parent)) {
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}

function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // if the user intentionally throws the original error in the handler,
      // do not log it twice
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  logError(err, vm, info)
}

function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

异步异常收集

download-1.png

Vue对异步异常的收集,只是在生命周期回调函数,监听事件,其他地方并未做处理。调用的处理方法见下方:

// src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

判断返回值是否是Promise,如果是就加一个catch,然后再调用handleError,这样就调起errorHandle函数了。

那么,其他场景的异步异常就获取不到了,比如methods对象中的方法们,所以我们可以自定义个方法:

初始化时,全局混入。

Vue.mixin({
  beforeCreate () {
    const methods = this.$options.methods || {}
    Object.keys(methods).forEach(key => {
      // 原有方法
      const fn = methods[key]
      // 重写原有方法,利用切面编程思想
      this.$options.methods[key] = function (...args) {
        const ret = fn.apply(this, args)
        if (ret && typeof ret.catch === 'function') {
          return ret.catch(Vue.config.asyncErrorHandler)
        } else {
          return ret
        }
      }
    })
  }
})

定义asyncErrorHandler,和errorHandler一样,放在Vue.config对象中就好。

Vue.config.asyncErrorHandler = err => {}

到此为止,异常的场景比较齐全了,我以为可以结束了。

但是,通过异常行列号经过sourcemap转化后,虽然文件是对的,但是错误位置完全不着边际。什么鬼。。。

一通查资料后,发现还需要一个跨浏览器堆栈跟踪日志标准化工具来对日志进行标准化处理,因为每个浏览器对于日志的格式是不一样的。这里我用到的TraceKitVue官方文档中提到的sentry错误追踪服务也用到了这个工具。

npm install tracekit --save
import Vue from 'vue'
import TraceKit from 'tracekit'

TraceKit.report.subscribe((errorReport) => {
  // 新建Image标签上报异常
  // 在这里还可以通过ajax进行数据上报
  new Image().src = `/log?logs=${errorReport}`
})

Vue.config.errorHandler = function (err, vm, info) {
  TraceKit.report(err)
  // 这里是为了让异常还能抛出到浏览器上
  console.log(err)
}

Vue.config.asyncErrorHandler = err => {
  TraceKit.report(err)
  console.log(err)
}

异常具体代码获取

异常上报了,但是异常对应的代码位置,是经过编译压缩的,根本定位不到源码位置,如何根据这些信息追溯到源码位置呢?

我们知道,source map是存储源码与最终生成代码的对应位置的文件,平时调试代码时可以用到过。但是,也是浏览器识别呀,没靠人眼没识别过呀。。。。

又是一通搜,发现有个mozilla提供的source-map包,可以根据异常行列号和sourcemap文件,找到源码的行列号,太棒了!

写段nodejs,把异常行,列号和对应sourcemap的路径传进去,执行一下,就得到结果拉。

const fs = require('fs');
const path = require('path')
const SourceMap = require('source-map');

const args = process.argv.splice(2)

const line = Number(args[0])
const column = Number(args[1])
const dir = args[2]

console.log('line=', args[0])
console.log('column=', args[1])

// let dir = path.resolve(__dirname, '../../app.08f6b37e.js.map')
 
const { readFileSync } = fs;
const { SourceMapConsumer } = SourceMap;

 
const rawSourceMap = JSON.parse(readFileSync(dir, 'utf8'));
 
SourceMapConsumer.with(rawSourceMap, null, consumer => {
  const pos = consumer.originalPositionFor({
    line,
    column
  });

  console.log(pos);
})

在我还在兴头儿上不断测试时,笑容渐渐凝固。。。

我发现,最终定位到源码的行列号,有时是不准的,难以接受。。。。

可还是要接受现实。。。

喝口水压压惊。

继续定位呗,还能咋滴。。。

我发现,到目前为止,位置虽然不准,但是差距已经不大了,也就是说整体方向是对的。那么思考,这时sourcemap所找的源码是不是也是经过处理之后的呢?

于是我把**.map.js也提交到了测试环境,跑了一下,在开发者工具中发现定位到的并不是源码,而是经过编译后的代码。

又是一通查,发现只设置productionSourceMaptrue是不够的,需要配置webpackdevtoolsource-map才可以找到源码。

module.exports = {
  productionSourceMap: true,
}

module.exports = {
    ...
    productionSourceMap: true,
    configureWebpack: config => {
        config.devtool = 'source-map'
    },
    ...
}

终于可以愉快的玩耍了。完美!

后记

异常收集后的处理

理想状态下,打包生产环境代码后,与此同时产生的sourceMap应该上传到一个日志系统(不要上传到生产环境哦,不然项目源码就泄露了),当收集到报错信息后,可以通过上面那段nodejs代码定位到源码异常位置。

这个异常系统对于我们业务项目来说,开发的话成本太高,后续公司也会有这种公共服务系统,所以也就只是把异常堆栈上报给后端,然后本地产生sourcemap,本地跑下nodejs获取到具体位置就满足需求拉。

如果你们还有其他方案,请不吝赐教,留言告诉我哦!