# 概述
# 为什么要处理异常?
异常是不可控的,会影响最终的呈现结果,但是我们有充分的理由去做这样的事情。
- 增强用户体验;
- 远程定位问题;
- 未雨绸缪,及早发现问题;
- 无法复线问题,尤其是移动端,机型,系统都是问题;
- 完善的前端方案,前端监控系统;
对于 JS 而言,我们面对的仅仅只是异常,异常的出现不会直接导致 JS 引擎崩溃,最多只会使当前执行的任务终止。
# 需要处理哪些异常?
对于前端来说,我们可做的异常捕获还真不少。总结一下,大概如下:
- JS 语法错误、代码异常
- AJAX 请求异常
- 静态资源加载异常
- Promise 异常
- 崩溃和卡顿
- 白屏
- Iframe 异常
- 跨域 Script error
# JS及资源加载异常
# 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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# window.onerror
pure js错误收集,window.onerror,当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。
- window.onerror 可以捕获常见语法、同步、异步错误等错误;
- window.onerror 无法捕获 Promise 错误、网络错误、资源加载错误;
- window.onerror 应该在所有JS脚本之前被执行,以免遗漏;
- window.onerror 容易被覆盖,在处理回调时应该考虑,被人也在使用该事件监听
/**
* @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});
}
console.log(notdefined);
// 语法错误,不能捕获 ❌
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,
// 异步错误,可以捕获 ✅
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
console.log(notdefined);
}, 0)
// 资源错误,不能捕获 ❌
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# unhandledrejection
- 捕获异步错误(Promise/async/await)
// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
console.log('捕获到异常:', e);
});
2
3
4
5
# 网络请求异常
可以通过覆写 window.fetch和 XMLHttpRequest 对象原生方法实现
- xmlHttpRequest
if(!window.XMLHttpRequest) return;
var xmlhttp = window.XMLHttpRequest;
var _oldSend = xmlhttp.prototype.send;
var _handleEvent = function (event) {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
// 自定义错误上报 }
}
xmlhttp.prototype.send = function () {
if (this['addEventListener']) {
this['addEventListener']('error', _handleEvent);
this['addEventListener']('load', _handleEvent);
this['addEventListener']('abort', _handleEvent);
} else {
var _oldStateChange = this['onreadystatechange'];
this['onreadystatechange'] = function (event) {
if (this.readyState === 4) {
_handleEvent(event);
}
_oldStateChange && _oldStateChange.apply(this, arguments);
};
}
return _oldSend.apply(this, arguments);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- fetch
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) { // True if status is HTTP 2xx
// 上报错误
}
return res;
})
.catch(error => {
// 上报错误
throw error;
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 框架异常
# React componentDidCatch
但error boundaries并不会捕捉以下错误:React事件处理,异步代码,error boundaries自己抛出的错误。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
class App extends React.Component {
render() {
return (
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Vue.config.errorHandler
- 由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler。
import { createApp } from "vue";
import App from "./App.vue";
let app = createApp(App);
app.config.errorHandler = function(e) {
console.log(e);
//错误上报...
};
app.mount("#app");
2
3
4
5
6
7
8
9
# 卡顿异常
# 方式一:利用FPS
卡顿指的是显示器刷新时下一帧的画面还没有准备好,导致连续多次展示同样的画面,从而让用户感觉到页面不流畅,也就是所谓的掉帧,衡量一个页面是否卡顿的指标就是我们熟知的 FPS。
- 如何获取 FPS Chrome DevTool 中有一栏 Rendering 中包含 FPS 指标,但目前浏览器标准中暂时没有提供相应 API ,只能手动实现。这里需要借助 requestAnimationFrame 方法模拟实现,浏览器会在下一次重绘之前执行 rAF 的回调,因此可以通过计算每秒内 rAF 的执行次数来计算当前页面的 FPS
- 如何上报“真实卡顿”
从技术角度看 FPS 低于 60 即视为卡顿,但在真实环境中用户很多行为都可能造成 FPS 的波动,并不能无脑地把 FPS 低于 60 以下的 case 全部上报,会造成非常多无效数据,因此需要结合实际的用户体验重新定义“真正的卡顿”,这里贴一下司内 APM 平台的上报策略:
页面 FPS 持续低于预期:当前页面连续 3s FPS 低于 20。
用户操作带来的卡顿:当用户进行交互行为后,渲染新的一帧的时间超过 16ms + 100ms。
# 方式二:PerformanceObserver&requestIdleCallback
- new PerformanceObserver
- entry.duration > 100 判断大于100ms,即可认定为长任务
- 使用 requestIdleCallback上报数据
import tracker from "../util/tracker";
import formatTime from "../util/formatTime";
import getLastEvent from "../util/getLastEvent";
import getSelector from "../util/getSelector";
export function longTask() {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 100) {
let lastEvent = getLastEvent();
requestIdleCallback(() => {
tracker.send({
kind: "experience",
type: "longTask",
eventType: lastEvent.type,
startTime: formatTime(entry.startTime), // 开始时间
duration: formatTime(entry.duration), // 持续时间
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
});
}
});
}).observe({ entryTypes: ["longtask"] });
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 崩溃异常
Web 页面崩溃指在网页运行过程页面完全无响应的现象,通常有两种情况会造成页面崩溃:
JS 主线程出现无限循环,触发浏览器的保护策略,结束当前页面的进程。
内存不足
- 全局变量
- 被遗忘的定时器
- 脱离Dom的引用
- 滥用闭包
监控内存泄漏
- window.performance.memory
- 开发阶段
- 浏览器的 Performance
- 移动端可使用 PerformanceDog
发生崩溃时主线程被阻塞,因此对崩溃的监控只能在独立于 JS 主线程的 Worker 线程中进行,我们可以采用 Web Worker 心跳检测的方式来对主线程进行不断的探测,如果主线程崩溃,就不会有任何响应,那就可以在 Worker 线程中进行崩溃异常的上报。这里继续贴一下 Slardar 的检测策略
# 处理方式
- 利用 window 对象的 load 和 beforeunload 事件实现了网页崩溃的监控。
不错的文章,推荐阅读:Logging Information on Browser Crashes。
- 使用 Service Worker 来实现网页崩溃的监控
Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。
- Webworker 心跳检测
JS 主线程:
- 固定时间间隔(2s)向 Web Worker 发送心跳
Web Worker:
- 定期(2s)检查是否收到心跳。
- 超过一定时间(6s)未收到心跳,则认为页面崩溃。
- 检测到崩溃后,通过 http 请求进行异常上报。
# 白屏异常
h5白屏异常监控技术调研 (opens new window)
- MutationObserver
利用 MutationObserver 接口提供了监视对 DOM 树所做更改的能力,是 DOM3 Events 规范的一部分。 方法:在首屏内容模块插入一个 div,利用 Mutation Observer API 监听该 div 的 dom 事件,判断该 div 的高度是否大于 0 或者大于指定值,如果大于了,就表示主要内容已经渲染出来,可计算首屏时间。
- elementsFromPoint
- elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
- 根据 elementsFromPoint api,获取屏幕水平中线和竖直中线所在的元素
import tracker from "../util/tracker";
import onload from "../util/onload";
function getSelector(element) {
var selector;
if (element.id) {
selector = `#${element.id}`;
} else if (element.className && typeof element.className === "string") {
selector =
"." +
element.className
.split(" ")
.filter(function (item) {
return !!item;
})
.join(".");
} else {
selector = element.nodeName.toLowerCase();
}
return selector;
}
export function blankScreen() {
const wrapperSelectors = ["body", "html", "#container", ".content"];
let emptyPoints = 0;
function isWrapper(element) {
let selector = getSelector(element);
if (wrapperSelectors.indexOf(selector) >= 0) {
emptyPoints++;
}
}
onload(function () {
let xElements, yElements;
debugger;
for (let i = 1; i <= 9; i++) {
xElements = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
yElements = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
}
if (emptyPoints >= 0) {
let centerElements = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
);
tracker.send({
kind: "stability",
type: "blank",
emptyPoints: "" + emptyPoints,
screen: window.screen.width + "x" + window.screen.height,
viewPoint: window.innerWidth + "x" + window.innerHeight,
selector: getSelector(centerElements[0]),
});
}
});
}
//screen.width 屏幕的宽度 screen.height 屏幕的高度
//window.innerWidth 去除工具条与滚动条的窗口宽度 window.innerHeight 去除工具条与滚动条的窗口高度
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- 截屏,通过像素对比(字节)
# 跨域脚本异常
由于浏览器安全策略限制,跨域脚本报错时,无法直接获取错误的详细信息,只能得到一个Script Error。例如,我们会引入第三方依赖,或者将自己的脚本放在CDN时。
- 方案一:
- 将js内联到HTML中
- 将js文件与HTML放在同域下
- 方案二:
- 为页面上script标签添加crossorigin属性
- 被引入脚本所在服务端响应头中,增加 Access-Control-Allow-Origin 来支持跨域资源共享
# iframe 异常
- window.frames[0].onerror
# Node 错误捕获方式
Node 端使用 process 对象监听 uncaughtException、 unhandledRejection 事件,
捕获未处理的 JS 异常和 Promise 异常。