web屏幕录制,web前端录屏
在前面的话
看了评论,突然意识到没有提前说明。这篇文章可以说是一篇研究学习文章,是我觉得可行的一套解决方案。后续我会看一些已经开源的类似代码库,弥补我遗漏的一些细节,大家可以作为学习文章,在生产环境中慎用。
录屏重现错误场景
如果您的应用程序连接到web apm系统,那么您可能知道apm系统可以帮助您捕获页面上未捕获的错误,给出错误堆栈并帮助您定位错误。但是有时候,在不知道用户具体操作的情况下,是没有办法重现这个错误的。这时候如果有操作记录画面,就可以清楚的了解用户的操作路径,从而重现这个BUG并修复。
实现思路
思路一:利用Canvas截图
这个思路比较简单,就是用canvas来绘制web内容。比较知名的库有:html2canvas。这个库的简单原则是:
收集所有DOM并存储在一个队列中;按照zIndex的说法,DOM是通过一定的规则按顺序传递的,DOM及其CSS样式是画在画布上的。这个实现比较复杂,但是我们可以直接使用,这样就可以得到我们想要的网页截图。
为了让生成的视频更加流畅,我们需要每秒生成25帧左右,也就是25张截图。思路流程图如下:
但是这种想法有一个最致命的不足:为了让视频流畅,我们一秒钟需要25张图像,一张图像是300KB。当我们需要一个30秒的视频时,图像总大小是220M,这样网络开销显然不够。
思路二:记录所有操作重现
为了减少网络开销,还是换个思路吧。我们在初始页面的基础上记录下一步一步的操作,在需要‘玩’的时候依次应用这些操作,这样就能看到页面的变化。这个想法将鼠标操作与DOM改变分开:
鼠标变化:
监听mouseover事件并记录鼠标的clientX和clientY。重放时,用js画一个假鼠标,根据坐标记录改变‘鼠标’的位置。DOM变化:
拍摄页面DOM的完整快照。包括收集样式、删除JS脚本,以及通过某些规则为每个当前DOM元素标记一个id。监控所有可能影响界面的事件,如各种鼠标事件、输入事件、滚动事件、缩放事件等。每个事件记录参数和目标元素,目标元素可以是刚刚记录的id,这样每个变更事件都可以记录为一个增量快照。向后端发送一定数量的快照。根据快照和操作链在后台播放。当然这个解释比较简短,鼠标的记录也比较简单。我们就不展开了,主要说明DOM监控的实现思路。
页面首次全量快照
首先,你可能认为要实现整页快照,可以直接使用outerHTML。
const content=document . document element . outer html;这只是记录了页面的所有DOM。您只需要首先将标签id添加到DOM,然后获取outerHTML,然后删除JS脚本。
但是,这里有一个问题。outerHTML记录的DOM会把两个相邻的文本节点合并成一个节点,而我们以后监控DOM变化的时候会用到MutationObserver。这时,你需要大量的处理来兼容这种文本节点的组合,否则,在还原操作过程中,你将无法定位操作的目标节点。
那么,我们有什么办法可以保持页面DOM的原始结构呢?
答案是肯定的,这里我们用虚拟DOM来记录DOM结构,把documentElement改成虚拟DOM,记录下来,然后在恢复的时候重新生成DOM。
DOM转化为Virtual DOM
这里我们只需要关心两类节点:Node。文本_节点和节点。元素_节点。同时需要注意的是,SVG和SVG子元素的创建需要使用API:createElementNS。因此,当我们记录虚拟DOM时,需要注意名称空间的记录,用上面的代码:
const SVG _ NAMESPACE=http://www.w3.org/2000/svg;const XML_NAMESPACES=[xmlns , xmlns:svg , xmlns:xlink ];函数createVirtualDom(element,is SVG=false){ switch(element。节点类型){ case节点.文本_节点:返回createVirtualText(元素);案例节点元素_节点:返回createVirtualElement(元素,是SVG 元素。标记名。tolowercase()=== SVG );默认值:返回null } } function createVirtualText(element){ const vText={ text:element。nodevalue,type: VirtualText ,};如果(元素的类型。_ _流量!==undefined) { vText .__flow=element ._ _流量;}返回vText}函数createVirtualElement(element,is SVG=false){ const tagName=element。标记名。tolowercase();const children=get node children(元素,是SVG);const { attr,namespace }=getNodeAttributes(element,is SVG);const vElement={ tagName,type: VirtualElement ,children,attrs:attr,namespace,};如果(元素的类型。_ _流量!==undefined) { vElement .__flow=element ._ _流量;} return vElement}函数get node children(element,is SVG=false){ const child nodes=element。子节点?[.元素。子节点]:[];const children=[];子节点。foreach((cnode)={ children。push(createVirtualDom(cnode,is SVG));});return children.filter(c=!c);}函数getNodeAttributes(element,is SVG=false){ const attributes=element。属性?[.元素。属性]:[];const attr={ };让命名空间;attributes.forEach(({节点名,节点值})={ attr[节点名]=节点值;if(XML _ namespaces。包括(节点名)){命名空间=节点值;} else if(is SVG){ NAMESPACE=SVG _ NAMESPACE;} });返回{属性,命名空间};}通过以上代码,我们可以将整个文档元素转化为虚拟多姆,其中_ _流量用来记录一些参数,包括标记身份等,虚拟节点记录了:类型、属性、子级、命名空间。
Virtual DOM还原为DOM
将虚拟数字正射影像图还原为数字正射影像图的时候就比较简单了,只需要递归创建数字正射影像图即可,其中滤器是为了过滤脚本元素,因为我们不需要射流研究…脚本的执行。
函数createElement(vdom,节点过滤器=()=true){ let node;如果(vdom。type===虚拟文本){ node=document。创建文本节点(vdom。正文);} else { node=type的类型。命名空间===未定义?文档。createelement(vdom。标记名):文档。createelementns(vdom。命名空间vdom。标记名);for(让名字在vdom中。属性){ node。设置属性(名称,vdom。属性[名称]);} vdom。孩子。foreach((cnode)={ const子节点=createElement(cnode,节点过滤器);if(子节点节点筛选器(子节点)){ node。appendchild(子节点);} });} if (vdom .__flow) { node .__flow=vdom ._ _流量;}返回节点;}DOM结构变化监控
在这里,我们使用了API:变异观测器,更值得高兴的是,这个应用程序接口是所有浏览器都兼容的,所以我们可以大胆使用。
使用变异观察者:
const options={ childList: true,//是否观察子节点的变动子树:真,//是否观察所有后代节点的变动属性:true,//是否观察属性的变动attributeOldValue: true,//是否观察属性的变动的旧值characterData: true,//是否节点内容或节点文本的变动characterDataOldValue: true,//是否节点内容或节点文本的变动的旧值//attributeFilter: [class , src]不在此数组中的属性变化时将被忽略};const observer=new mutation observer((突变列表)={//突变列表:突变的数组});观察者。观察(文档。文档元素、选项);使用起来很简单,你只需要指定一个根节点和需要监控的一些选项,那么当数字正射影像图变化时,在回收函数中就会有一个变异列表,这是一个数字正射影像图的变化列表,其中变化的结构大概为:
{ type: childList ,//或characterData、attributes目标:DOM,//other params}我们使用一个数组来存放突变,具体的回收为:
const onMutationChange=(突变列表)={ const getFlowId=(节点)={ if(节点){//新插入的数字正射影像图没有标记,所以这里需要兼容如果(!节点.流量)节点. flow={ id:uuid()};返回节点. flow _ _ flow . id } };突变列表。foreach((mutation)={ const { target,type,attributeName }=mutation const record={ type,target: getFlowId(target),};switch(type){ case 字符数据:记录。价值=目标。nodevalue打破;案例“属性”:记录。attributeName=attributeName记录。属性值=目标。获取属性(attributeName);打破;案例“子列表”:记录。删除的节点=[.突变.删除节点].map(n=getFlowId(n));record.addedNodes=[.mutation.addedNodes].map((n)={ const快照=这个。拍摄快照(n);返回{.快照,下一个兄弟:getFlowId(n .下一个兄弟),上一个兄弟:getFlowId(n .上一个兄弟)};});打破;} this.records.push(记录);});}函数takeSnapshot(node,options={ }){ this。标记节点(node);const snapshot={ vdom:createVirtualDom(node),};如果(选项。doctype===true){ snapshot。doctype=文档。doctype。姓名;快照。客户端宽度=文档。身体。客户端宽度;快照。客户高度=文档。身体。客户身高;}返回快照;}这里面只需要注意,当你处理新增数字正射影像图的时候,你需要一次增量的快照,这里仍然使用虚拟数字正射影像图来记录,在后面播放的时候,仍然生成多姆,插入到父元素即可,所以这里需要参照多姆,也就是兄弟节点。
表单元素监控
上面的突变观察者并不能监控到投入等元素的值变化,所以我们需要对表单元素的值进行特殊处理。
oninput事件监听
移动用户号码簿号码文档:https://开发者。Mozilla。org/en-US/docs/Web/API/全局事件处理程序/on input
事件对象:选择、输入、文本区
window.addEventListener(input ,this.onFormInput,true);onFormInput=(event)={ const target=event。目标;如果(目标目标__flow [select , textarea , input].包括(目标。标记名。tolowercase()){ this。记录。推({ type: input ,target: target .__flow.id,value: target.value,});}}在窗户上使用捕获来捕获事件,后面也是这样处理的,这样做的原因是我们是可能并经常在冒泡阶段阻止冒泡来实现一些功能,所以使用捕获可以减少事件丢失,另外,像卷起事件是不会冒泡的,必须使用捕获。
onchange事件监听
移动用户号码簿号码文档:https://开发者。Mozilla。org/en-US/docs/Web/API/全局事件处理程序/on input
投入事件没法满足类型为检验盒和收音机的监控,所以需要借助待清扫房事件来监控
窗户。addevent侦听器( change ,this.onFormChange,true);onFormChange=(event)={ const target=event。目标;如果(目标目标. flow){ if(目标。标记名。to lower case()=== input [ checkbox , radio].包括(目标。get属性( type ){ this。记录。push({ type: checked ,target: target .__flow.id,checked: target.checked,});} }}onfocus事件监听
移动用户号码簿号码文档:https://开发者。Mozilla。org/en-US/docs/Web/API/全局事件处理程序/onfocus
window.addEventListener(focus ,this.onFormFocus,true);onFormFocus=(event)={ const target=event。目标;如果(目标目标.流){这个。记录。推({ type: focus ,target: target .__flow.id,});}}onblur事件监听
移动用户号码簿号码文档:https://开发者。Mozilla。org/en-US/docs/Web/API/global event handlers/onblur
window.addEventListener(blur ,this.onFormBlur,true);onFormBlur=(event)={ const target=event。目标;如果(目标目标.流){这个。记录。推({ type:模糊,目标:目标.__flow.id,});}}媒体元素变化监听
这里指声音的和视频,类似上面的表单元素,可以监听onplay、onpause事件、时间更新、体积变化等等事件,然后存入记录
Canvas画布变化监听
帆布内容变化没有抛出事件,所以我们可以:
收集帆布元素,定时去更新实时内容砍一些画画的API,来抛出事件
帆布监听研究没有很深入,需要进一步深入研究
播放
思路比较简单,就是从后端拿到一些信息:
全量快照虚拟数字正射影像图操作链记录屏幕分辨率文档类型利用这些信息,你就可以首先生成页面多姆,其中包括过滤脚本标签,然后创建iframe,追加到一个容器中,其中使用一个地图来存储数字正射影像图
函数play(options={ }){ const { container,records=[],snapshot={ } }=optionsconst { vdom,doctype,clientHeight,clientWidth }=snapshotthis。节点缓存={ };this.records=记录;this.container=容器;this.snapshot=快照;这个。iframe=文档。createelement(“iframe”);const documentElement=createElement(vdom,(node)={ //缓存DOM const flowId=节点。_ _流节点_ _ flow.idif(flowId){ this。节点缓存[flowId]=节点;} //过滤剧本回归!(node.nodeType===Node .元素_节点节点。标记名。tolowercase()==== script );});这个。iframe。风格。width=` { client width } px `;这个。iframe。风格。height=` $ { client height } px集装箱。appendchild(iframe);const doc=iframe。内容文档;this.iframeDocument=docdoc。open();doc.write(`!doctype $ { doctype } html head/head body/body/html `);医生。close();医生。替换子级(documentElement,doc。document element);这个。exec记录();}函数exec记录(pre duration=0){ const记录=this。记录。shift();让节点;if(record){ setTimeout(()={ switch(record。type){//子列表、字符数据、/属性、输入、选中、//聚焦、模糊、播放、暂停等事件的处理}这个。执行记录(记录。持续时间);},record.duration - preDuration) }}上面的期间在上文中省略了,这个你可以根据自己的优化来做播放的流畅度,看是多个记录作为一帧还是原本呈现。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。