怎样定位前端线上问题,一直以来,都是很头疼的问题,由于它发生于用户的一系列操做以后。错误的缘由可能源于机型,网络环境,接口请求,复杂的操做行为等等,在咱们想要去解决的时候很难复现出来,天然也就没法解决。 固然,这些问题并不是不能克服,让咱们来一块儿看看如何去监控并定位线上的问题吧。 css
这是搭建前端监控系统的第六章,主要是介绍如何使用js进行页面截屏和录屏,跟着我一步步作,你也能搭建出一个属于本身的前端监控系统。html
具体效果请移步:前端监控系统 前端
用户对前端程序员来讲,就是一个黑匣子。 若是用户上报了一个错误,前端程序员就是两眼一抹黑,由于不少错误是无法复现的。那么有没有一个办法能够解决用户和前端程序员之间的障碍呢, 让用户对咱们来讲,再也不是黑匣子,而是透明化。用户的页面长什么样,他们都作了什么操做,发生了什么错误,咱们都可以清晰的知道,那么,再有问题上报的时候,咱们就能够修复它。其实说白了,就是错误场景重现了。git
好了,话很少少,咱们进入主题。程序员
1、JS怎么实现截图?github
若是用户在页面上产生一些特殊行为,好比报错,白屏的时候,咱们想看看页面长什么样子的,就能够利用js截屏技术达到咱们的目的。这样咱们对用户当时的页面有个直观的感觉,解决问题的时候也会的驾轻就熟一些。web
须要用到一个开源库叫 html2Canvas ,这个库的名字,我想你们也是耳熟能详了。 关于他原理的介绍,有篇文章已经说得很详细了,如何实现web录屏。好,既然咱们能够对页面进行截图了,那么接下来的问题就是上传了。html2canvas的截图是图片数据,多则大几百Kb, 少则也有个上百Kb, 这么大的流量,对用户端,损耗确实过大。也许将来5G的大时代到了,这点流量根本不算什么,可是如今主要仍是4g时代,这样的流量损耗不容忽视。json
首先,对一个小页面用js截图进行了几种测试,以下: 参数 截图方式一 截图方式二 截图方式三 压缩前/后 字符串长度 28764/10787 93076/34903 168312/63118 图片压缩率 72% 40% 0% 截图大小 21Kb 68.2Kb 123Kb
使用html2canvas截图代码以下:canvas
/** * js处理截图 */ this.screenShot = function (cntElem, callback) { var shareContent = cntElem;//须要截图的包裹的(原生的)DOM 对象 var width = shareContent.offsetWidth; //获取dom 宽度 var height = shareContent.offsetHeight; //获取dom 高度 var canvas = document.createElement("canvas"); //建立一个canvas节点 var scale = 0.6; //定义任意放大倍数 支持小数 canvas.style.display = "none"; canvas.width = width * scale; //定义canvas 宽度 * 缩放 canvas.height = height * scale; //定义canvas高度 *缩放 canvas.getContext("2d").scale(scale, scale); //获取context,设置scale var opts = { scale: scale, // 添加的scale 参数 canvas: canvas, //自定义 canvas logging: false, //日志开关,便于查看html2canvas的内部执行流程 width: width, //dom 原始宽度 height: height, useCORS: true // 【重要】开启跨域配置 }; html2canvas(cntElem, opts).then(function(canvas) { var dataURL = canvas.toDataURL(); var tempCompress = dataURL.replace("data:image/png;base64,", ""); var compressedDataURL = Base64String.compress(tempCompress); callback(compressedDataURL); }); }
综上分析,截图方式一, 压缩率高,虽然截图不是很清晰,可是,也可以看得出,线上用户页面是什么样子的。并且,也解决了,在低端机上截图消耗性能过大的弊端,二十几Kb的流量,也是咱们彻底可以接受的大小了。因而可知,该方式可以知足咱们追踪用户行为的需求了,固然,也能够适当的提升清晰度,以实际状况而定。api
2、JS怎么实现录屏?
Fundebug好久以前出了一个录屏功能,进入Fundebug首页,第一条即是 黑科技!支持录屏。 这下就惊呆我了,js作前端监控,竟然还能录屏? 你丫这是要逆天啊? 因此,赶忙注册了帐号,进行试用。 由于当时fundebug的录屏功能还在适用阶段,因此我还误觉得是靠一连串的截图组成的视频,反倒惹人笑话😂。
实现JS录屏须要用到一个开源库rrweb,当看到这个库的时候很有一番感慨:
一叹前辈高人技艺之精湛,二叹开源世界心胸之广阔,正是有这些无私奉献的大牛们,才让这技术的世界多姿多彩。
这个录屏库的原理跟html2canvas不一样,并不是是靠截图,而是记录下整个dom树的结构,而后再分别记录不一样dom节点的变化,最后在iframe里边将他们从新渲染出来,就是咱们能看到的录屏结果了。至于具体的录屏效果,你们能够进入个人首页,点击「录屏测试」菜单查看效果,确实很惊艳。
录屏的api,rrweb已经写得很清楚了,我就再也不浪费笔墨了。Js的录屏效果很惊艳,可是同时也带来了一个比较棘手的问题,以个人网站为例,一段将近20秒的视频,视频数据居然达到了将近20M之多。像我网站首页,最大的dom树结构长度高达几十万之多,因此解决录屏信息的长度即是首要任务。你们能够看看个人测试数据,我录制了一段近2分钟的视频,数据总量压缩在1.5M左右,那么除以2分钟的之间,10秒钟差很少控制在120kb大小左右。跟fundebug提出的,10s内录屏数据压缩在百K之内,感受也相差很少了,总之我一人之力有限,时间亦有限,因此暂时先作到这一步,并投入使用看看效果再说。下边说说,如何进行字符串压缩
3、JS如何压缩JSON字符串?
对字符串进行压缩,可能工做中不少状况下是用不到的,可是若是作上传日志等相关的工做,如遇到截图,录屏等,那么字符串压缩就是不得不作了。
由于rrweb的录屏数据全都是JSON格式,天然而然要转化成JSON字符串进行上传了。我这里使用两步对JSON字符串进行压缩:
1. 简化JSON中的key值。
因为rrweb是将整个dom树转化成json格式的数据,因此里边出现了大量的重复的key,因此我这里使用更短小的key值,将他们替换,以达到缩短JSON字符串的目的。另外,里边包含了不少样式的内容,你们都知道,样式的key值很长,并且大多数都重复,因此样式里的值,咱们也要对其进行缩短替换。代码以下:
var JSON_KEY = {"type":"≠","childNodes":"ā","name":"á","id":"ǎ","tagName":"à","attributes":"ē","style":"é","textContent":"ě","isStyle":"è","isSVG":"ī","content":"í","href":"ǐ","src":"ì","class":"ō","tabindex":"ó","aria-label":"ǒ","viewBox":"ò","focusable":"ū","data-icon":"ú","width":"ǔ","height":"ù","fill":"ǖ","aria-hidden":"ǘ","stroke":"ǚ","stroke-width":"ǜ","paint-order":"ü","stroke-opacity":"ê","stroke-dasharray":"ɑ","stroke-linecap":"?","stroke-linejoin":"ń","stroke-miterlimit":"ň","clip-path":"Γ","alignment-baseline":"Δ","fill-opacity":"Θ","transform":"Ξ","text-anchor":"Π","offset":"Σ","stop-color":"Υ","stop-opacity":"Φ"}; var JSON_CSS_KEY = {"background":"≠","background-attachment":"ā","background-color":"á","background-image":"ǎ","background-position":"à","background-repeat":"ē","background-clip":"é","background-origin":"ě","background-size":"è","border":"Г","border-bottom":"η","color":"┯","style":"Υ","width":"б","border-color":"ū","border-left":"ǚ","border-right":"ň","border-style":"Δ","border-top":"З","border-width":"Ω","outline":"α","outline-color":"β","outline-style":"γ","outline-width":"δ","left-radius":"Ж","right-radius":"И","border-image":"ω","outset":"μ","repeat":"ξ","repeated":"π","rounded":"ρ","stretched":"σ","slice":"υ","source":"ψ","border-radius":"Б","radius":"Д","box-decoration":"Й","break":"К","box-shadow":"Л","overflow-x":"Ф","overflow-y":"У","overflow-style":"Ц","rotation":"Ч","rotation-point":"Щ","opacity":"Ъ","height":"Ы","max-height":"Э","max-width":"Ю","min-height":"Я","min-width":"а","font":"в","font-family":"г","font-size":"ж","adjust":"з","aspect":"и","font-stretch":"й","font-style":"к","font-variant":"л","font-weight":"ф","content":"ц","before":"ч","after":"ш","counter-increment":"щ","counter-reset":"ъ","quotes":"ы","list-style":"+","image":"-","position":"|","type":"┌","margin":"┍","margin-bottom":"┎","margin-left":"┏","margin-right":"┐","margin-top":"┑","padding":"┒","padding-bottom":"┓","padding-left":"—","padding-right":"┄","padding-top":"┈","bottom":"├","clear":"┝","clip":"┞","cursor":"┟","display":"┠","float":"┡","left":"┢","overflow":"┣","right":"┆","top":"┊","vertical-align":"┬","visibility":"┭","z-index":"┮","direction":"┰","letter-spacing":"┱","line-height":"┲","text-align":"6","text-decoration":"┼","text-indent":"┽","text-shadow":"10","text-transform":"┿","unicode-bidi":"╀","white-space":"╂","word-spacing":"╁","hanging-punctuation":"╃","punctuation-trim":"1","last":"3","text-emphasis":"4","text-justify":"5","justify":"7","text-outline":"8","text-overflow":"9","text-wrap":"11","word-break":"12","word-wrap":"13"} this.compressJson = function(o) { if (o instanceof Array) { var n = [] for (var i = 0; i < o.length; ++i) { n[i] = this.compressJson(o[i]) } return n } else if (o instanceof Object) { var n = {} for (var i in o) { if (i == "_cssText") { o[i]= o[i].replace(/ {/g, "{").replace(/; /g, ";").replace(/: /g, ":").replace(/, /g, ",").replace(/{ /g, "{") for (var key in JSON_CSS_KEY) { var cssAttr = JSON_CSS_KEY[key] var cssReg = new RegExp(key, "g"); o[i]= o[i].replace(cssReg, cssAttr) } } if (JSON_KEY[i]) { n[JSON_KEY[i]] = this.compressJson(o[i]) delete n[i] } else { n[i] = this.compressJson(o[i]) } } return n } return o }
2. 使用lz-string 库对字符串进行压缩
lz-string有两个api:
至此,前端监控系统即可以完成截屏和录屏的功能了。