前端监控
1. 监控类型
- 数据监控
数据监控即监控用户行为,大概可以分为以下几个方面:
- PV/UV:
- PV:page view页面浏览量或点击量;
- UV:指访问某个站点或点击某条新闻的不同 IP 地址的人数
- 用户在每一个页面的停留时间
- 用户通过什么入口来访问该网页
- 用户在相应的页面中触发的行为
- 性能监控
性能监控即通过监控分析产品性能,分为首屏性能和使用性能。大概可以分为以下几个方面:
- 首屏性能:
- 不同用户,不同机型和不同系统下的首屏加载时间
- 白屏时间
- 静态资源整体下载时间
- 使用性能:
- http请求的响应时间
- 页面渲染时间
- 页面交互动画完成时间
- 异常监控
异常监控即检测到产品使用异常,并及时上报异常情况。
- Javascript的异常监控
- 样式丢失的异常监控
2. 数据上报
向服务器端上报数据的方式有很多种,大致可以分为:ajax、img、navigator.sendBeacon
- img上报
- 防止跨域:图片的src属性不会跨域,但是可以发起请求
- 防止阻塞页面加载,影响用户体验
- 相比JPG/PNG,GIF体积更小
- navigator.sendBeacon上报
请求发出后会与当前页面脱离关联,作为浏览器的任务单独执行,可以保证数据一定会发出去,而且不会拖延卸载流程
- 异步上报
- 可跨域
- 可靠性
监控页面显示隐藏事件:
document.addEventListener("visibilitychange", function logData() {
if (document.visibilityState === "hidden") {
navigator.sendBeacon("/log", analyticsData);
}
});
监控页面关闭事件:
let _beforeUnload_time = 0;
window.onunload = function () {
let _gap_time = new Date().getTime() - _beforeUnload_time;
if (_gap_time <= 5) {
//浏览器关闭
console.log('浏览器关闭');
} else {
//浏览器刷新
console.log('浏览器刷新',document.domain);
}
};
window.onbeforeunload = function () {
console.log('关闭前');
_beforeUnload_time = new Date().getTime();
};
文档卸载期间发送数据
对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload (en-US) 事件处理器中产生的异步 XMLHttpRequest。
为了解决这个问题, 统计和诊断代码通常要在 unload 或者 beforeunload (en-US) 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。
通过在卸载事件处理器中创建一个图片元素并设置它的 src 属性的方法来延迟卸载以保证数据的发送。因为绝大多数用户代理会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。
使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。
异常监控
前端代码中一般涉及到的业务场景分为JS逻辑处理,外部资源加载,CSS样式处理,其中JS逻辑处理又分为同步和异步代码。
1. JS异常
try...catch即时运行异常局部捕获
try {
console.log(notdefined);
} catch (error) {
console.log(error,'error');
}
⚠️:try...catch不能捕获异步错误,不能捕获语法错误(VSCode可检测语法错误)
try {
setTimeout(()=>{
console.log(a);
},0)
} catch (error) {
console.log(error,'error'); // 无错误提示
}
window.onerror即时运行错误全局捕获
window.onerror = function(message, source, lineno, colno, error){
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined); // 可以捕获运行错误
setTimeout(()=>{ // 可以捕获异步错误
console.log(a);
},0)
⚠️:window.onerror不能捕获资源加载错误,不能捕获语法错误(VSCode可检测语法错误)
2. 资源加载异常
window.addEventListener('error', callback, true)全局捕获资源加载异常
<script>
window.addEventListener("error",(error) => {
console.log("捕获到异常:", error);
},true);
</script>
<script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
</script>
⚠️:window.addEventListener不能捕获new Image()错误,不能捕获fetch请求错误
<script>
new Image().src = 'https://yun.tuia.cn/image/lll.png'
fetch('https://tuia.cn/test')
</script>
3. 异步异常
Promise.catch()局部捕获Promise异常
new Promise((resolve, reject) => {
JSON.parse("");
resolve();
}).catch((err) => {
console.log("捕获到异常:", err);
});
await+try...catch局部捕获async异常
const getJSON = async () => {
throw new Error("inner error");
};
try {
await getJSON();
} catch (error) {
console.log("捕获到异常:", error);
}
window.addEventListener('unhandledrejection', callback)全局捕获Promise异常
window.addEventListener("unhandledrejection", function (e) {
console.log("捕获到异常:", e);
});
fetch("https://tuia.cn/test");
new Promise((resolve, reject) => {
JSON.parse("");
resolve();
});
4. CSS样式异常
window.getComputedStyle(element)捕获特定元素样式
<div id="myElement" class="my-element-style">测试元素</div>
var element = document.getElementById("myElement");
var style = window.getComputedStyle(element);
if (style.display === "block") {
console.log("error异常:样式丢失,display属性为block");
}
window.getComputedStyle()函数获取元素的计算样式,然后检查特定样式属性是否存在
window.addEventListener('error', callback, true)全局捕获CSS文件加载异常 略!
框架异常捕获
1. Vue异常捕获
Vue.config.errorHandler全局捕获Vue异常
Vue.config.errorHandler = function (err) {
setTimeout(() => {
throw err
})
}
2. React异常捕获ErrorBoundary组件
componentDidCatch、getDerivedStateFromError生命周期钩子局部捕获React异常
ErrorBoundary 只能捕获子组件的 render 错误,以下错误无法处理:
- 事件处理函数:如 onClick,onMouseEnter
- 异步代码:如 requestAnimationFrame,setTimeout,promise
- 服务端渲染错误
- ErrorBoundary 组件本身的错误
function ErrorCatch(Component){
return class WrapComponent extends React.Component{
constructor(props){
super(props);
this.state={ isError:false }
}
static getDerivedStateFromError(){
return{ isError:true } // 无法获取this,但是返回值会合并到state中,作为新的state值向下传递
}
componentDidCatch(error, errorInfo){
this.setState({ isError:true })
}
render(){
return this.state.isError ? <div>出现错误</div>
: <Component {...this.props}/>
}
}
}
生命周期钩子不能捕获函数事件处理器中的错误、异步错误、服务端渲染错误、错误边界组件自身抛出的错误
3. catch-react-error
github地址:github.com/x-orpheus/c…
网易云音乐技术团队推出的一款在React中捕获异常的插件,4年前发布,目前已经迭代到3.0.0版本。
catch-react-error底层实现是通过类装饰器和ErrorBoundary组合使用,同时支持React和React Native。由于ErrorBoundary不支持服务端渲染错误,所以通过try...catch处理了这部分。
实现原理:通过ErrorBoundary包裹入口组件,手动包裹成本太大,而且对于已经存在的代码需要重新修改,所以采用类装饰器处理包裹组件功能,ErrorBoundary无法处理服务端渲染错误,所以通过try/catch包裹SSR。
类装饰器定义:类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
catchreacterror:catchreacterror类装饰器组件返回一个HOC组件,用于包裹内层子组件。
import React, { Component, forwardRef } from "react";
const catchreacterror = (Boundary = DefaultErrorBoundary) => InnerComponent => {
class WrapperComponent extends Component {
render() {
const { forwardedRef } = this.props;
return (
<Boundary>
<InnerComponent {...this.props} ref={forwardedRef} />
</Boundary>
);
}
}
};
try/catch处理服务端渲染错误:
// 判断是否为SSR
function is_server() {
return !(typeof window !== "undefined" && window.document);
}
// try/catch包裹SSR
if (is_server()) {
const originalRender = InnerComponent.prototype.render;
InnerComponent.prototype.render = function() {
try {
return originalRender.apply(this, arguments);
} catch (error) {
console.error(error);
return <div>Something is Wrong</div>;
}
};
}
项目使用
// 安装插件
npm install catch-react-error
npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-class-properties
// 引入
import catchreacterror from "catch-react-error";
// 添加babelplugin配置
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
// 类组件使用
@catchreacterror()
class Count extends React.Component {
render() {
const { count } = this.props;
if (count === 3) {
throw new Error("count is three");
}
return <h1>{count}</h1>;
}
}
// 函数组件使用
function Count({ count }) {
if (count === 3) {
throw new Error("count is three");
}
return <h1>{count}</h1>;
}
const SaleCount = catchreacterror()(Count);
TS的装饰器官网