当前位置: 首页 > Web前端 > HTML

rrweb录屏原理分析

时间:2023-03-28 17:45:17 HTML

rrweb是'recordandreplaytheweb'的缩写,用于录制和回放用户在web界面的操作。1.包结构分析rrweb-snapshot:包含快照和重建功能。snapshot用于将DOM及其状态转换为可序列化的数据结构,并添加唯一标识符;rebuild就是将快照记录的数据结构重建到对应的DOM中,插入到文档中rrweb:包含记录和重放两个功能。record用于记录DOM中的所有变化;replay就是将记录的变化按照对应的时间一点一点重放。rrweb-player:为rrweb提供一套UI控件,提供基于GUI的暂停、快进、拖拽任意时间点播放等功能。rrdom:为node平台模拟浏览器的dom、event等接口2.调试网页录屏和回放的功能主要在rrweb包中,rrweb-snapshot是rrweb的依赖包。所以我们只需要调试这两个包即可。rrweb项目由lerna+yarnworkspace的monorepo架构维护;查看官网的教程,进入项目根目录运行两条命令:yarninstallyarndev。项目打包完成。最后执行命令cdpackages/rrwebnpmlink创建一个rrweb-demo,在项目根目录下Executenpmlinkrrweb,然后就可以应用本地打包好的rrweb源码了。ps:可以先npminstallrrweb,这样可以把rrweb添加到依赖中,避免找不到rrweb的情况。sourceMap但是还有一个问题,rrweb没有打包sourcemap,所以我们调试的时候看到的是rrweb的打包文件,影响可读性,所以我们需要构建rrweb的sourcemap文件,并使用在rrweb-demo项目中。rrweb项目的所有子包都使用了typescript,所以我们需要修改packages/rrwebpackages/rrweb-snapshot下的tsconfig.json"compilerOptions""compilerOptions":{"sourceMap":true}packages/rrweb/rollup.config.jsfrom在配置中,默认会输出commonJs、esm、iife格式的包。在dev模式下,为了加快打包速度,我们只能输出esm格式的包。packages/rrweb/package.jsondev默认只会输出iife格式的包,详见process.env.BROWSER_ONLY;为了调试方便,我们在项目中使用esm格式包rollup.output,添加souremap配置注释,iife格式commonjs和browerminify模式依次注释即可。然后每次打包只会输出esm格式的包,sourcemap文件也会生成rollup-plugin-rename-node-modules。rrweb包打包为esm时,使用rollup-plugin-rename-node-modules插件将node_moudules重命名为ext,发布到npm时会忽略nodule_modules,但可以发布ext文件夹,保证rrweb无论是在本地还是在成功发送包后都可以正常运行;但是这样会影响sourcemap的生成,sourcemap文件的源文件路径为空这样调试时就找不到原文件了。具体请参考usagerrweb-snapshot。rrweb_demo项目的配置rrweb_demo配置别名指向本地构建的esm格式的rrweb:启动rrweb_demo项目后,打开devtool,发现rrweb的sourcemap没有加载。这时候就需要配置source-map-loader。这就是结局?不,不。因为在rrweb_demo项目中,通过软链接找到/Users/用户名/Documents/group_share/rrweb/packages/rrweb路径下的打包品,导致rrweb的sourcemap文件虽然已经成功加载,它仍然无法通过sourcemap。在sources字段找到原来的ts文件,可以看到web-map-loader也有类似的问题。好吧,喝口水,震惊!google了半天,找到了配置tsconfig的[soureRoot](https://www.typescriptlang.org/tsconfig#sourceRoot),配置为绝对路径。{"compilerOptions":{"sourceRoot":"/Users/username/Documents/group_share/rrweb/packages/rrweb/src"}}""重新打包rrweb,重新启动rrweb_demo就可以调试了。至此,大功告成,终于可以进入正题了。整体工作流程rrweb为了实现web界面录制和回放功能,着重于dom元素的序列化、增量快照、回放和沙箱。从上图可以看出rrweb记录的入口是record方法,然后序列化doucment,完成全量快照;然后通过浏览器提供的MutationObserver监听dom元素的创建、删除和属性变化,同时监听鼠标移动。点击和页面交互等事件。无论是全量快照还是增量快照,事件数据都会以json格式发出并保存在服务器上;当需要恢复用户界面时,从服务器拉取json,调用replay方法启动程序的播放过程。全量快照事件数据//全量快照的数据类型{type:EventType.FullSnapshot,data:{node:{type:NodeType.Document;子节点:serializedNodeWithId[];兼容模式?:字符串;id:number},initialOffset:{left:window.pageXOffset,top:window.pageYOffset,},}}突变事件数据{type:EventType.IncrementalSnapshot,data:{source:IncrementalSource.Mutation,//0texts:textMutation[];属性:attributeMutation[];移除:removedNodeMutation[];添加:addedNodeMutation[];isAttachIframe?:true,},}Scroll事件数据{type:EventType.IncrementalSnapshot,data:{source:IncrementalSource.Scroll,//3id:number;x:数字;y:number;,}}Record(记录)record方法内部会根据事件类型初始化事件监听,比如DOM元素变化、鼠标移动、鼠标交互、滚动等都有自己专属的事件监听方法;MutationObserverApi监听dom元素的变化。Dom序列化从官网的序列化文档可以知道,如果我们只需要在本地记录和播放浏览器中的变化,那么我们可以简单的通过深拷贝DOM来保存当前视图,然后用requestAnimationFrame播放好的演示;但是实际场景需要传输数据,所以web状态必须序列化为文本格式(比如josn)。截取官网文档的一段:对于序列化的特殊处理,说说我个人对descripting的理解。一个是为了安全的沙箱环境;另一种是不需要执行脚本,因为无论用户如何点击,事件的逻辑处理都体现在dom的更新上,而这个可以通过MutationObserverAPI监控,类似于domdiff.处理表单表单;input的值可以根据type类型设置在value/checked/selected等属性上,需要单独记录资源路径并转为绝对路径;这个也很好理解,我们无法确定客户端的文件目录结构,所以只能转换成绝对路径;我们之前生成sourcemap文件的时候,也设置了一个绝对路径。样式是内联的;避免额外的资源请求,保证播放体验。dom的序列化是在Snapshot方法中完成的:snapshot方法的逻辑比较清晰,序列化的主要逻辑在serializeNode方法中,根据nodeType对不同的节点进行序列化;值得注意的是,这里还维护了一个序列MappingofId->dom。可以看到,在增量事件数据中,有parentId、id、nextId。根据这三个id就可以确定dom元素的位置,这样后面重绘界面的时候就可以确定dom的位置了。serializeNodeserializeNode方法根据nodeType判断节点类型,依次序列化节点。document节点:compatMode判断文档的渲染模式是否为标准模式;{type:NodeType.Document,childNodes:[]}文档类型DocumentType节点:直接返回节点的基本信息,如名称、类型、publicId、systemIdd。{type:NodeType.DocumentType,name:(nasDocumentType).name,publicId:(nasDocumentType).publicId,systemId:(nasDocumentType).systemId,rootId}注释节点{type:NodeType.Comment,textContent:n.textContent}文本节点学习了CSSStyleSheet,一个操作css的api。元素节点调用transformAttribute方法将标签上的属性转换为绝对路径。将链接引入的远程样式转换为inlineStylesheet。表单元素处理。input、textarea、select等元素记录自己的值或checked;option元素记录选择的属性。占位符元素。只记录dom元素的宽高,重绘时用一个空的div代替。最后经过处理得到下图的结构。例如document._sn属性监听Dom变化,通过MutationObserver监听dom元素的变化,以批量异步的方式触发回调,通过MutationRecord数组将DOM变化传递给回调方法。type==='attributes':表示DOM属性的变化。所有属性发生变化的节点都会记录在this.attributes数组中。结构是{节点:节点,属性:{}}。只有本次变更涉及的属性才会记录在attributes属性中;type==='characterData':表示characterData节点的变化,会记录在this.texts数组中,结构为{node:Node,value:string},value为characterData节点的最新值;type==='childList':表示子节点树的childList的变化。与前两种相比,加工会更加复杂。rrweb为了实现增量快照,采用set结构;addedSet、movedSet、droppedSet分别对应三个节点操作:add、move、delete,类似于React的diff机制。再次截取官方文档的描述:总结一下:为了避免漏节点,需要遍历子节点,然后使用set结构去重。通过parentId和nextId建立DOM关系,方便绘制界面;但是现在节点统一序列化了,那么就会出现dom节点的父节点,或者下一个兄弟节点还没有序列化,获取不到id的问题;所以需要维护一个双向链表,遍历addedset中的节点依次添加到链表中。最后逆序遍历链表,将节点依次序列化。新节点突变事件双向链表的维护逻辑遍历this.addset中的节点,依次添加到链表中。如果DOM节点的previousSibling已经存在于链表中,则将其插入到node.previousSibling节点之后。如果DOM节点的nextSibling已经存在于链表中,则插入到node.nextSibling节点之前;如果不存在,则将其插入链表的头部。通过这种添加方式,可以保证兄弟节点的顺序。DOM节点的nextSibling必须在节点后面,previousSibling必须在节点前面;addedSet中的所有节点都添加到链表后,会倒序遍历addList链表,这样可以保证DOM节点的nextSibling必须在DOM节点之前序列化,nextId时可以得到下一次序列化DOM节点。节点处理流程回放(replay)通过Replayer提供的play方法,可以在iframe中回放上面记录的事件。第一步,在初始化rrweb.Replayer实例时,分别调用创建两个服务:createPlayerService用于处理事件播放的逻辑,createSpeedService用于控制播放速度。第二步会调用replayer.play()方法触发PLAY事件类型,开始事件播放的处理流程。constreplayer=newrrweb.Replayer(events);replayer.play();//this.service是createPlayerService创建的播放控制服务实例//timeOffset值是鼠标拖动后的时间偏移this.service.send({type:'PLAY',payload:{timeOffset}});播放过程为了支持进度条的随意拖动和播放速度的设置(如上图所示),在播放过程中实现了自定义定时器。高精度定时器Timer,关键属性和方法是classTimer{//播放的起始位置,对应进度条拖动时的任意时间点publictimeOffset:number=0;//播放速度publicspeed:number;//播放队列{doAction:()=>void;延迟:数字}私人行动:actionWithDelay[];私人英国皇家空军:编号|空=空;私人liveMode:布尔值;//...publicstart(){this.timeOffset=0;//performance.timing.navigationStart+performance.now()约等于Date.now()letlastTimestamp=performance.now();const{动作}=这个;常量自我=这个;functioncheck(){consttime=performance.now();//当前播放时间self.timeOffset+=(time-lastTimestamp)*self.speed;lastTimestamp=时间;while(actions.length){缺点t动作=动作[0];//当前播放时间大于动作需要执行的时间段//action.delay=event.timestamp-baselineTimeif(self.timeOffset>=action.delay){actions.shift();动作.doAction();}else{休息;}}if(actions.length>0||self.liveMode){self.raf=requestAnimationFrame(check);}}this.raf=requestAnimationFrame(check);}}rebuildrebuild的过程其实和snapshot的过程差不多,直接上图:总结本文主要介绍了rrweb的调试方法和rrweb的工作流程,希望对各位顽固的朋友有所帮助!参考rrweb带你还原问题站点