处理反常的意义

随着网页项目越来越杂乱,许多反常报错很难在开发和测验阶段被发现,虽然你或许避开了语法等惯例过错,但不可避免的是代码在运转时的过错你依旧无法精确预料,假设现在有如下一段 Vue 代码,它在生命周期的 created 阶段异步恳求并接收了过错的数据,或许就会导致页面烘托呈现过错:

<template>
  {{ test.obj.xxx }}
</template>
......
created() {
    this.getSomeData()
},
methods: {
    getSomeData() {
      this.fetch().then((res) => {
        this.test = res // 假设这是恳求的过错数据
      })
    },
}

而假如测验人员及时发现了这一过错的话,当他翻开控制台时往往就会立即下结论了:噢,是前端的锅

Vue项目处理过错上报原来如此简略

事实上真正的项目中或许会遇到更多”奇妙”的问题,而且假如过错仅产生在某些用户端,那将无从发觉,所以咱们会想到应该在程序中处理捕获运转时过错,将过错上报至服务器,然后剖析和改善代码来修复已经产生的过错。

所以该怎么应对并处理或许产生的某些过错,成为了前端开发的一门必修课,你当然能够在每个代码片段中重复编写 try...catch...、为每个 Promise 都处理 catch,但这难免显得有些难堪,所以我考虑能不能用更优雅的办法,一致处理一切反常,将过错在大局进行捕获然后上报剖析。其实在 Vue 中完成这样大局的反常处理并不难,下面看看我是怎么做的吧。

怎么大局捕获过错反常

查询 Vue 文档咱们能够发现大局装备中就有这么一个捕获过错的处理钩子 errorHandler,用法很简略:

Vue.config.errorHandler = function (err, vm, info) {
    // `info` 是 Vue 特定的过错信息,比如过错所在的生命周期钩子 
    // 只在 2.2.0+ 可用 
}

只需求用这个钩子就能够处理大部分 Vue 运用中的过错(如组件生命周期中的过错、自定义事情处理函数内部过错、v-onDOM 监听器内部抛出的过错),而且回调中自带的 info 参数也标记了这个过错大概是归于哪类,一起它还能处理回来 Promise 链的过错,能够说是十分强大了,但是它也并非能处理一切的反常,不然文章写到这就该完毕了 ~ 接下来咱们测验一下。

首先在大局过错捕获中输出一下 log,先运转一下最初的恳求数据过错例子:

Vue.config.errorHandler = function (err, vm, info) {
    console.log('vue反常过错捕获: ', '过错产生在 ' + info)
}

Vue项目处理过错上报原来如此简略

能够看到反常成功被捕获了,由于咱们模拟了一个数据过错导致烘托犯错,所以过错产生在 render 层,假如是在函数中的 Promise 产生的过错呢?咱们试一下:

async created() {
    await this.getSomeData()
},
method: {
    async getSomeData() {
      const res = await this.fetch()
      this.test = res
    },
    fetch() {
      asdasd = 1 // 这儿给一个未定义的变量赋值,肯定会报错
      return new Promise((resolve) => {
          // ......省掉
      })
    }

Vue项目处理过错上报原来如此简略

没有问题,接下来咱们再试试一个按钮用 v-on 绑定 click,但是故意在办法内制作过错,看看是什么作用:

<button @click="doSomeThing"> Test </button>
..........
doSomeThing() {
  aaaaaaaa = 111111 // 这儿给一个未定义的变量赋值,肯定会报错
},

Vue项目处理过错上报原来如此简略

看来事情也能正常捕获,咱们再试试写一个组件,在组件中自定义一个事情,看看成果怎么:

<my-custom-comp @node-click="doSomeThing" />
// 在组件中是 $emit 触发:
this.$emit('node-click', item)

Vue项目处理过错上报原来如此简略

这个反常依旧是被成功捕获了,当然生命周期钩子中的过错反常也都能成功捕获,就不多做演示了,到目前为止都没有什么问题,但是假如过错不产生在 Vue 内部呢?

<button onclick="foo()">bad button</button>

Vue项目处理过错上报原来如此简略

能够看到这个反常没有被顺利捕获,同样的,假如是外部 JS 代码报错,也都是无法捕获的,也便是说这个钩子只能捕获与 Vue 相关联的事情。

宏使命中的过错也是无法捕获的:

.......
fetch() {
  return new Promise((resolve) => {
    setTimeout(() => {
      asd = 1 // 在宏使命的异步中呈现的过错
      resolve({})
    }, 1000)
  })
},

Vue项目处理过错上报原来如此简略

假如 Promise 反常未被正常处理的话,也是捕获不到的,如下代码,注意这儿 create 没有用 await 办法调用异步办法:

created() {
    this.getSomeData();
},
methods: {
    async getSomeData() {
      await (asdasd = 1);
    },
},

Vue项目处理过错上报原来如此简略

下面咱们就逐个处理这两个场景下的过错捕获问题。

处理 JS 的额定过错

咱们能够用 BOM 提供的大局过错处理函数 window.onerror 来测验捕获,它接收多个参数:

window.onerror = function (message, source, line, column, error) {
  console.log('大局捕获过错', message, source, line, column, error)
}

犯错代码:

<button onclick="foo()">bad button</button>

Vue项目处理过错上报原来如此简略

现在 JS 反常过错都能够被捕获到了,包括 setTimeout 宏使命的异步过错也能够被捕获,但咱们注意到未被正常处理的 Promise 过错仍不能成功捕获。

处理 Promise 过错

参阅 Vueerror.js 的代码,同步使命反常捕获便是套上一层 try...catch...,这也解释了为什么 Vue 捕获的过错不会被大局 window.onerror 再次捕获,由于已经在这儿抛出了。而异步使命反常处理则是判断假如是 Promise 则把 catch 指向过错处理中:

Vue项目处理过错上报原来如此简略

咱们能够仿照写一个插件,来处理 Vue 实例中 methods 的反常。

function isPromise(ret) {
  return ret && typeof ret.then === 'function' && typeof ret.catch === 'function'
}
const handleMethods = (instance) => {
  if (instance.$options.methods) {
    let actions = instance.$options.methods || {}
    for (const key in actions) {
      if (Object.hasOwnProperty.call(actions, key)) {
        let fn = actions[key]
        actions[key] = function (...args) {
          let ret = args.length > 0 ? fn.apply(this, args) : fn.call(this)
          if (isPromise(ret) && !ret._handled) {
            ret._handled = true
            return ret.catch((e) => errorHandler(e, this, `捕获到了未处理的Promise反常: (Promise/async)`))
          }
        }
      }
    }
  }
}
export default {
  install: (Vue, options) => {
    Vue.mixin({
      beforeCreate() {
        this.$route.meta.capture && handleMethods(this)
      },
    })
  },
}

由于遍历一切办法或许会造成页面功用丢失,所以这儿我加了一个条件,需求在路由设置 meta 才能开启该组件下的method额定反常捕获。

Vue项目处理过错上报原来如此简略

再试试上面的过错代码,看看成果:

created() {
    this.getSomeData();
},
methods: {
    async getSomeData() {
      await (asdasd = 1);
    },
},

Vue项目处理过错上报原来如此简略

能够被正常捕获,这种办法的好处是咱们能够把产生过错的实例信息传进去,假如不想运用这种办法,或是在 Vue3 中运用 setup 办法而不是 options 写法,还能够运用大局的事情监听来捕获:

window.addEventListener('unhandledrejection', (event) => {
  console.log('大局捕获未处理的Promise反常', event)
})

Vue项目处理过错上报原来如此简略

完好代码

errorPlugin.js

function errorHandler(err, vm, info) {
  console.log('vue反常过错捕获: ', '过错信息 ' + info)
  // TODO: 处理过错上报
}
const handleMethods = (instance) => {
  if (instance.$options.methods) {
    let actions = instance.$options.methods || {}
    for (const key in actions) {
      if (Object.hasOwnProperty.call(actions, key)) {
        let fn = actions[key]
        actions[key] = function (...args) {
          let ret = args.length > 0 ? fn.apply(this, args) : fn.call(this)
          if (isPromise(ret) && !ret._handled) {
            ret._handled = true
            return ret.catch((e) => errorHandler(e, this, `捕获到了未处理的Promise反常: (Promise/async)`))
          }
        }
      }
    }
  }
}
function isPromise(ret) {
  return ret && typeof ret.then === 'function' && typeof ret.catch === 'function'
}
let GlobalError = {
  install: (Vue, options) => {
    Vue.config.errorHandler = errorHandler
    // eslint-disable-next-line max-params
    window.onerror = function (message, source, line, column, error) {
      errorHandler(message, null, '大局捕获过错')
      // console.log('大局捕获过错', message, source, line, column, error)
    }
    window.addEventListener('unhandledrejection', (event) => {
      errorHandler(event, null, '大局捕获未处理的Promise反常')
    })
    Vue.mixin({
      beforeCreate() {
        this.$route.meta.capture && handleMethods(this)
      },
    })
  },
}
export default GlobalError

main.js 中引进

import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
// 引进过错处理插件
import ErrorPlugin from './errorPlugin'
Vue.use(ErrorPlugin)
new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app')

在 Vue3 中运用

相同在 main.js 中引进插件即可:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ErrorPlugin from './errorPlugin'
createApp(App).use(router).use(ErrorPlugin).mount('#app')

完毕

假如你需求更加丰富的过错收集剖析功用,仍是得运用如Sentry、Bugsnag这类完善的过错追踪服务,不过相对来讲这些都需求不少装备部署操作。本文介绍了怎么简略地在 Vue 中大局捕获反常过错,提高代码健壮性,且能避免在代码中编写很多反常捕获块,一起也减少了犯错时控制台的大片飘红报警,收集过错能够协助咱们定位开发与测验阶段不易发现的疑难杂症,这部分能够运用 http 恳求将过错信息发送到服务器。

往期精彩

# 一行代码引发的 JS 探求 : call 和 apply 究竟哪个更快?

# 当UI走查说页面色值过错时,先别急着检查代码

# 运用语义化 HTML + CSS 编写一个原生 Web Components 组件

# CSS 容器查询来了,你不能错失的10个精彩事例共享!