大家好,我是念念!这是上周问朋友的综合设计题。如果你从来没有使用过嵌入式监控系统,或者没有深入了解,那基本就爽了。原创文章首发于我的公众号:前端私教念念这篇文章会讲清楚:嵌入式监控系统负责哪些问题,api应该如何设计?为什么要用img的src发送请求,什么是sendBeacon?react和vue的错误边界如何处理?什么是监控SDK?比如公司开发上线了一个网站,但是开发者不可能预知用户实际使用的时候会发生什么:用户浏览了哪些页面?有多少用户会点击弹窗的确认按钮,有多少用户会点击取消?是否有页面崩溃?因此,我们需要一个埋点监控SDK来采集数据,后期进行统计分析。有了分析数据,我们才能有针对性地优化网站:不要在PV很少的页面上浪费大量人力;有bug的页面赶紧修,不然要325。比较知名的嵌入式点监控有GoogleAnalytics。除了网页端,还有适用于iOS和Android的SDK。公众号后台回复“ReactSDK”获取react版本github监控功能范围因为业务需求不同,大部分公司都会自己开发一套监控系统,但基本上都会覆盖这三类功能:用户行为监控负责统计PV(页面访问次数)、UV(页面访问次数)、用户点击操作。这种统计是用得最多的,只有有了这些数据才能量化我们工作的结果。虽然页面性能监控开发人员和测试人员在上线前都会对数据进行评估,但是用户的环境和我们是不一样的。它可能是3G网络或非常旧的型号。我们需要了解实际使用场景中的性能数据,比如页面加载时间、白屏时间等,错误告警监控获取错误数据并及时处理,可以避免大量用户受到影响。除了全局捕获的错误信息外,还有代码内部捕获的错误告警,都需要收集。下面将从API的设计入手,对以上三种进一步展开。SDK设计在开始设计之前,先来看看如何使用SDKimportStatisticSDKfrom'StatisticSDK';//全局初始化一次window.insSDK=newStatisticSDK('uuid-12345');{window.insSDK.event('点击','确认');...//其他业务代码}}>确认先将SDK实例挂载到全局,然后在业务代码中调用,这里实例的时候需要传入new一个id,因为这个嵌入式监控系统经常被多个业务使用,通过id来区分不同的数据源。首先实现实例化部分:classStatisticSDK{constructor(productID){this.productID=productID;}}数据发送数据发送是最基本的api,后续的功能都要以此为基础。通常这种前后端分离的场景都会使用AJAX发送数据,而这里我们使用的是图片的src属性。原因有二:没有跨域限制,srcipt标签和img标签可以直接发送跨域GET请求,不需要特殊处理;兼容性好,部分静态页面可能禁用脚本,脚本标签无法使用;但注意这张图片不是用来展示的,我们的目的是“传递数据”,只是利用img标签的src属性,在它的url后面拼接参数,服务端收到后解析。classStatisticSDK{constructor(productID){this.productID=productID;}send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}}imgtag优点是不需要附加到文档中,只需要设置src属性即可成功发起请求。通常请求的url会是一张1X1px的GIF图片。网上的文章对于这里返回的图片为什么是GIF都含糊不清。这里我查阅了一些资料并进行了测试:相同尺寸但不同格式的图片GIF的尺寸最小,所以选择返回一张GIF,这样性能损失更小;如果返回204,就会去img的onerror事件,抛出全局错误;如果它返回200和一个空对象,将会有一个CORB警告;当然,如果你不关心这个错误,你可以返回一个空对象。事实上,有一些工具可以做到这一点。页面需要添加一些埋点。例如,垃圾邮件发送者会添加这样一个隐藏标志来验证电子邮件是否被打开,如果它返回204或200空对象将导致明显的图像占位符更优雅的网络信标这种标记方式称为网络信标(webbeacon)。除了gif图片,从2014年开始,浏览器逐渐实现了专门的API来更优雅的完成这个:Navigator.sendBeacon使用起来非常简单Navigator.sendBeacon(url,data)相比图片的src,这种方式更有优势:不会和主要业务代码抢资源,而是在浏览器空闲的时候发送;并且还可以保证页面卸载时请求发送成功,不会阻塞页面刷新和跳转;目前的埋点监控工具通常优先使用sendBeacon,但由于浏览器兼容性问题,还是需要使用图片的src做掩护。用户行为监控实现了发送数据的API,现在可以用来实现用户行为监控的API。classStatisticSDK{constructor(productID){this.productID=productID;}//发送数据send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//自定义事件event(key,val={}){leteventURL='http://demo/'this.send(eventURL,{event:key,...val})}//pv曝光pv(){this.event('pv')}}用户行为包括自定义事件和pv曝光,pv曝光也可以看作是一种特殊的自定义行为事件。页面性能监控页面的性能数据可以通过performance.timingAPI获取,获取的数据是一个毫秒级的时间戳。以上内容你不需要都懂,但关键数据如下。根据它们可以计算出FP/DCL/Load等关键事件的时间点:页面第一次渲染时间:FP(firstPaint)=domLoading-navigationStartDOM加载完成:DCL(DOMContentEventLoad)=domContentLoadedEventEnd-navigationStartImage,style等外部资源加载:L(Load)=loadEventEnd-navigationStart上面的值可以对应性能面板中的结果。回到SDK,我们只需要实现一个API来上传所有的性能数据:classStatisticSDK{constructor(productID){this.productID=productID;//初始化并自动调用性能报告this.initPerformance()}//数据发送send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//性能报告initPerformance(){letperformanceURL='http://performance/'this.send(performanceURL,performance.timing)}}然后,它会在构造函数中自动调用,因为性能数据要上传,不需要用户每次都手动调用。错误告警监控错误告警监控分为JS原生错误和React/Vue组件错误处理。JS原生错误除了trycatch捕获到的错误,我们还需要上报没有捕获到的错误——通过error事件和unhandledrejection事件来监听。errolerror事件用于监听DOM操作错误DOMException和JS错误告警。具体来说,JS错误分为以下8类:InternalError:内部错误,如递归栈爆炸;RangeError:范围错误,如newArray(-1);EvalError:使用eval()时出错;ReferenceError:引用错误,比如使用了未定义的变量;SyntaxError:语法错误,如vara=;TypeError:类型错误,如[1,2].split('.');URIError:传递给encodeURI或decodeURl()的参数无效,如decodeURI('%2')Error:以上7类错误的基类,一般由开发者抛出。即代码运行时出现的上述8种错误都可以被检测到。unhandledrejectionPromise内部抛出的错误无法被error捕获,所以需要使用unhandledrejection事件。回到SDK的实现,处理错误告警的代码如下:classStatisticSDK{constructor(productID){this.productID=productID;//初始化错误监听this.initError()}//发送数据send(baseURL,query={}){query.productID=this.productID;让queryStr=Object.entries(query).map(([key,value])=>`${key}=${value}`).join('&')让img=newImage();img.src=`${baseURL}?${queryStr}`}//自定义错误报告error(err,etraInfo={}){consterrorURL='http://error/'const{message,stack}=err;this.send(errorURL,{message,stack,...etraInfo})}//初始化错误监听initError(){window.addEventListener('error',event=>{this.error(error);})window.addEventListener('unhandledrejection',event=>{this.error(newError(event.reason),{type:'unhandledrejection'})})}}和初始性能监控一样,初始化错误监控也是必须做的,所以需要在构造函数中调用。后续开发者只需要在业务代码的trycatch中调用error方法即可。React/Vue组件错误成熟的框架库都会有错误处理机制,React和Vue也不例外。React的errorboundary错误边界是希望当应用程序内部出现渲染错误时,整个页面不会崩溃。我们预先给它设置了一个pocketcomponent,可以细化粒度。只有出现错误的部分被这个“底层组件”替换,这样整个页面就无法正常工作了。它的使用非常简单,是一个具有特殊生命周期的类组件,用来包装业务组件。这两个生命周期分别是getDerivedStateFromError和componentDidCatch,代码如下://定义错误边界类ErrorBoundaryextendsReact.Component{state={error:null}staticgetDerivedStateFromError(error){return{error}}componentDidCatch(error,errorInfo){//调用我们实现的SDK实例insSDK.error(error,errorInfo)}render(){if(this.state.error){returnSomethingwentwrong.
}returnthis.props.children}}...搭建在线沙箱体验,公众号后台回复“ErrorBoundarydemo”获取地址返回SDK集成,在生产环境中,errorboundary被包裹的组件,如果内部抛出错误,则无法监听到全局错误事件,因为errorboundary本身就相当于一个trycatch。因此,需要在错误边界的组件内进行上报处理。也就是上面代码中的componentDidCatch生命周期。vue的errorboundaryvue也有类似的生命周期来做这个,就不细说了:errorCapturedVue.component('ErrorBoundary',{data:()=>({error:null}),errorCaptured(err,vm,info){this.error=`${err.stack}\n\nfoundin${info}ofcomponent`//调用我们的SDK并上报错误信息insSDK.error(err,info)returnfalse},render(h){if(this.error){returnh('pre',{style:{color:'red'}},this.error)}returnthis.$slots.default[0]}})...现在我们已经实现了一个完整的SDK框架,在实际开发中处理react/vue项目应该如何连接。实际生产中使用的SDK会更加健壮,但是思路是一样的。有兴趣的可以阅读源码。结论文章比较长,但是想要很好的回答这个问题,这些知识储备还是很有必要的。如果我们要设计SDK,首先要了解它的基本使用方法,然后才能知道如何构建下面的代码框架;那么我们需要明确SDK的功能范围:它需要能够处理三种类型的监控:用户行为、页面性能、错误告警;最后,react,vue项目一般都会做错误边界处理,如何接入我们自己的sdk。如果您觉得本文对您有用,点赞关注是对我最大的鼓励!您的支持是我创作的动力!