前端异常

一般来说,根据笔者的目前研究,前端异常大体上可以分为两类:由于对语法的不了解、机制的不清楚或是没有做好降级处理而“主动”造成的错误(多为js异常)和由于资源加载、第三方库、浏览器本身机制造成的“被动异常”。

第二种异常的解决方式很多,通过各种手段也大多可以避免(比如更换源这种常用手段)。我们主要来说一下第一种!

函数的常见处理

我们知道,js中充斥着大量的函数,它们承担封装某个具体功能的作用。但是在看许多代码时,笔者发现了一个问题:我们总喜欢用 ​​return false/true;​​​ 来表示该函数内部功能代码的执行错误/正确。
《代码大全2》中说:“使用​​​throw​​​,而不是​​return false​​”。笔者深以为然。在我司的前端规范中也有类似的话!这很重要。

通过报错,我们可以传递许多字段信息出来,这些信息被捕获到以后可以成为分析用户行为、监控、定位前端异常的利器。我司自研的前端监控工具spider就是这么做的。

常见JS执行错误

SyntaxError

解析时发生语法错误

// 控制台运行
const xx,

window.onerror捕获不到SyntxError,一般SyntaxError在构建阶段,甚至本地开发阶段就会被发现。

TypeError

值不是所期待的类型

// 控制台运行
const person = void 0
person.name

ReferenceError

引用未声明的变量

// 控制台运行

RangeError

当一个值不在其所允许的范围或者集合中

(function fn ( ) { fn() })()

网络错误

ResourceError

资源加载错误

new Image().src = '/remote/image/notdeinfed.png'

HttpError

Http请求错误

// 控制台运行
fetch('/remote/notdefined', {})

搜集错误

所有起因来源于错误,那我们如何进行错误捕获。

try/catch

能捕获常规运行时错误,但是对于语法错误和异步错误不行

// 常规运行时错误,可以捕获
try {
console.log(notdefined);
} catch(e) {
console.log('捕获到异常:', e);
}

// 语法错误,不能捕获
try {
const notdefined,
} catch(e) {
console.log('捕获到异常:', e);
}

// 异步错误,不能捕获
try {
setTimeout(() => {
console.log(notdefined);
}, 0)
} catch(e) {
console.log('捕获到异常:',e);
}

​try/catch​​有它细致处理的优势,但缺点也比较明显。

window.onerror

​window.onerror​​ —— 当 JS 运行时错误发生时,window 会触发一个 ErrorEvent

/**
* @param {String} message 错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象
*/

window.onerror = function(message, source, lineno, colno,) {
console.log('捕获到异常:', {message, source, lineno, colno, error});
}

验证下几个错误是否可以捕获:

// 常规运行时错误,可以捕获
window.onerror = function(message, source, lineno, colno,) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined);

// 语法错误,不能捕获
window.onerror = function(message, source, lineno, colno,) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,

// 异步错误,可以捕获
window.onerror = function(message, source, lineno, colno,) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
console.log(notdefined);
}, 0)

// 资源错误,不能捕获
<script>
window.onerror = function(message, source, lineno, colno,) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="https://yun.tuia.cn/image/kkk.png">

​window.onerror​​ 不能捕获资源错误怎么办?

window.addEventListener

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而​​window.onerror​不能监测捕获。

// 图片、script、css加载错误,都能被捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>

// new Image错误,不能捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
new Image().src = 'https://yun.tuia.cn/image/lll.png'
</script>

// fetch错误,不能捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
fetch('https://tuia.cn/test')
</script>

其中​​new Image​​比较重要(和特别),可以单独自己处理自己的错误。

但通用的fetch怎么办呢,fetch返回Promise,但Promise的错误不能被捕获,怎么办呢?

​window.addEventListener​​​和​​window.onerror​​都不能捕获promise错误

Promise错误的解决

普通Promise错误

​try/catch​​不能捕获Promise中的错误

// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中
try {
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
})
} catch(err) {
console.error('in try catch', err)
}

// 需要使用catch方法
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
}).catch(err => {
console.log('in catch fn', err)
})

async错误

上面说的普通的 promise 错误可以通过​​async/await​​​来解决。但是有一点比较特殊的是:
​​​try/catch​​不能捕获 async 包裹的错误!

const getJSON = async () => {
throw new Error('inner error')
}

// 通过try/catch处理
const makeRequest = async () => {
try {
// 捕获不到
JSON.parse(getJSON());
} catch (err) {
console.log('outer', err);
}
};

try {
// try/catch不到
makeRequest()
} catch(err) {
console.error('in try catch', err)
}

try {
// 需要await,才能捕获到
await makeRequest()
} catch(err) {
console.error('in try catch', err)
}

import chunk错误

import 其实返回的也是一个 promise。它是一个特殊的语法 —— 在w3c的不懈努力下,import 支持“不必非要写在最顶部”的写法。对此,我们有如下两种方式捕获错误:

// Promise catch方法
import(/* webpackChunkName: "incentive" */'./index').then(module => {
module.default()
}).catch((err) => {
console.error('in catch fn', err)
})

// await 方法,try catch
try {
const module = await import(/* webpackChunkName: "incentive" */'./index');
module.default()
} catch(err) {
console.error('in try catch', err)
}

小结:全局捕获Promise中的错误

以上三种其实归结为Promise类型错误,可以通过 unhandledrejection API 捕获:

// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
console.log('捕获到异常:', e);
});

为了防止有漏掉的 Promise 异常,可通过 unhandledrejection 用来全局监听Uncaught Promise Error。

Vue中的错误

由于Vue会捕获所有Vue单文件组件或者​​Vue.extend​​​继承的代码,所以在Vue里面出现的错误,并不会直接被​​window.onerror​​​捕获,而是会抛给​​Vue.config.errorHandler​​。

/**
* 全局捕获Vue错误,直接扔出给onerror处理
*/
Vue.config.errorHandler = function (err) {
setTimeout(() => {
throw err
})
}

跨域问题

一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。

如果当前投放页面和云端JS所在不同域名,如果云端JS出现错误,​​window.onerror​​会出现Script Error。通过以下两种方法能给予解决:

后端配置​​Access-Control-Allow-Origin​​​、前端script加​​crossorigin​​。

<script src="http://yun.tuia.cn/test.js" crossorigin></script>
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = 'http://yun.tuia.cn/test.js';
document.body.appendChild(script);

如果不能修改服务端的请求头,可以考虑通过使用 try/catch 绕过,将错误抛出:

<!doctype html>
<html>
<head>
<title>Test page in http://test.com</title>
</head>
<body>
<script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
<script>.onerror = function (message, url, line, column,) {
console.log(message, url, line, column, error);
}

try {
foo(); // 调用testerror.js中定义的foo方法
} catch (e) {
throw e;
}</script>
</body>
</html>

我们捋一下场景,一般调用远端js,有下列三种常见情况。

  1. 调用远端JS的方法出错
  2. 远端JS内部的事件出问题
  3. 要么在setTimeout等回调内出错

上报接口

捕获到了错误,就要开始往服务端发送(可以生成日志或者异常文档,一般监控工具会自动完成)。这其实就是一次请求的过程。

里面有几个需要注意的地方:

为什么不能直接用ajax - GET/POST/HEAD请求接口进行上报?

一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。

为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

一般来说创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。而且载入js/css资源还会阻塞页面渲染,影响用户体验。

构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。
使用new Image进行接口上报。最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。

为什么推荐采用1x1的gif图片进行操作?

首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG。

同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。GIF才是最佳选择。

使用​​1*1​​的gif