iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >web实现页面录屏的示例分析
  • 607
分享到

web实现页面录屏的示例分析

2023-06-09 20:06:13 607人浏览 薄情痞子
摘要

小编给大家分享一下WEB实现页面录屏的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!录屏重现错误场景如果你的应用有接入到web apm系统中,那么你可能就

小编给大家分享一下WEB实现页面录屏的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

录屏重现错误场景

如果你的应用有接入到web apm系统中,那么你可能就知道apm系统能帮你捕获到页面发生的未捕获错误,给出错误栈,帮助你定位到BUG。但是,有些时候,当你不知道用户的具体操作时,是没有办法重现这个错误的,这时候,如果有操作录屏,你就可以清楚地了解到用户的操作路径,从而复现这个BUG并且修复。

实现思路

思路一:利用Canvas截图

这个思路比较简单,就是利用canvas去画网页内容,比较有名的库有: html2canvas ,这个库的简单原理是:

  • 收集所有的DOM,存入一个queue中;

  • 根据zIndex按照顺序将DOM一个个通过一定规则,把DOM和其CSS样式一起画到Canvas上。

这个实现是比较复杂的,但是我们可以直接使用,所以我们可以获取到我们想要的网页截图。

为了使得生成的视频较为流畅,我们一秒中需要生成大约25帧,也就是需要25张截图,思路流程图如下:

web实现页面录屏的示例分析

但是,这个思路有个最致命的不足:为了视频流畅,一秒中我们需要25张图,一张图300KB,当我们需要30秒的视频时,图的大小总共为220M,这么大的网络开销明显不行。

思路二:记录所有操作重现

为了降低网络开销,我们换个思路,我们在最开始的页面基础上,记录下一步步操作,在我们需要"播放"的时候,按照顺序应用这些操作,这样我们就能看到页面的变化了。这个思路把鼠标操作和DOM变化分开:

鼠标变化:

  • 监听mouseover事件,记录鼠标的clientX和clientY。

  • 重放的时候使用js画出一个假的鼠标,根据坐标记录来更改"鼠标"的位置。

DOM变化:

  • 对页面DOM进行一次全量快照。包括样式的收集、JS脚本去除,并通过一定的规则给当前的每个DOM元素标记一个id。

  • 监听所有可能对界面产生影响的事件,例如各类鼠标事件、输入事件、滚动事件、缩放事件等等,每个事件都记录参数和目标元素,目标元素可以是刚才记录的id,这样的每一次变化事件可以记录为一次增量的快照。

  • 将一定量的快照发送给后端

  • 在后台根据快照和操作链进行播放。

当然这个说明是比较简略的,鼠标的记录比较简单,我们不展开讲,主要说明一下DOM监控的实现思路。

页面首次全量快照

首先你可能会想到,要实现页面全量快照,可以直接使用 outerHTML

const content = document.documentElement.outerHTML;

这样就简单记录了页面的所有DOM,你只需要首先给DOM增加标记id,然后得到outerHTML,然后去除JS脚本。

但是,这里有个问题,使用 outerHTML 记录的DOM会将把临近的两个Textnode合并为一个节点,而我们后续监控DOM变化时会使用 MutationObserver ,此时你需要大量的处理来兼容这种TextNode的合并,不然你在还原操作的时候无法定位到操作的目标节点。

那么,我们有办法保持页面DOM的原有结构吗?

答案是肯定的,在这里我们使用Virtual DOM来记录DOM结构,把documentElement变成Virtual DOM,记录下来,后面还原的时候重新生成DOM即可。

DOM转化为Virtual DOM

我们在这里只需要关心两种Node类型: Node.TEXT_NODENode.ELEMENT_NODE 。同时,要注意,SVG和SVG子元素的创建需要使用api:createElementNS,所以,我们在记录Virtual DOM的时候,需要注意namespace的记录,上代码:

const SVG_NAMESPACE = 'Http://www.w3.org/2000/svg';const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];function createVirtualDom(element, isSVG = false)  {  switch (element.nodeType) {    case Node.TEXT_NODE:      return createVirtualText(element);    case Node.ELEMENT_NODE:      return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg');    default:      return null;  }}function createVirtualText(element) {  const vText = {    text: element.nodeValue,    type: 'VirtualText',  };  if (typeof element.__flow !== 'undefined') {    vText.__flow = element.__flow;  }  return vText;}function createVirtualElement(element, isSVG = false) {  const tagName = element.tagName.toLowerCase();  const children = getNodeChildren(element, isSVG);  const { attr, namespace } = getNodeAttributes(element, isSVG);  const vElement = {    tagName, type: 'VirtualElement', children, attributes: attr, namespace,  };  if (typeof element.__flow !== 'undefined') {    vElement.__flow = element.__flow;  }  return vElement;}function getNodeChildren(element, isSVG = false) {  const childNodes = element.childNodes ? [...element.childNodes] : [];  const children = [];  childNodes.forEach((cnode) => {    children.push(createVirtualDom(cnode, isSVG));  });  return children.filter(c => !!c);}function getNodeAttributes(element, isSVG = false) {  const attributes = element.attributes ? [...element.attributes] : [];  const attr = {};  let namespace;  attributes.forEach(({ nodeName, nodeValue }) => {    attr[nodeName] = nodeValue;    if (XML_NAMESPACES.includes(nodeName)) {      namespace = nodeValue;    } else if (isSVG) {      namespace = SVG_NAMESPACE;    }  });  return { attr, namespace };}

通过以上代码,我们可以将整个documentElement转化为Virtual DOM,其中__flow用来记录一些参数,包括标记ID等,Virtual Node记录了:type、attributes、children、namespace。

Virtual DOM还原为DOM

将Virtual DOM还原为DOM的时候就比较简单了,只需要递归创建DOM即可,其中nodeFilter是为了过滤script元素,因为我们不需要JS脚本的执行。

function createElement(vdom, nodeFilter = () => true) {  let node;  if (vdom.type === 'VirtualText') {    node = document.createTextNode(vdom.text);  } else {    node = typeof vdom.namespace === 'undefined'      ? document.createElement(vdom.tagName)      : document.createElementNS(vdom.namespace, vdom.tagName);    for (let name in vdom.attributes) {      node.setAttribute(name, vdom.attributes[name]);    }    vdom.children.forEach((cnode) => {      const childNode = createElement(cnode, nodeFilter);      if (childNode && nodeFilter(childNode)) {        node.appendChild(childNode);      }    });  }  if (vdom.__flow) {    node.__flow = vdom.__flow;  }  return node;}

DOM结构变化监控

在这里,我们使用了API:MutationObserver,更值得高兴的是,这个API是所有浏览器都兼容的,所以我们可以大胆使用。

使用MutationObserver:

const options = {  childList: true, // 是否观察子节点的变动  subtree: true, // 是否观察所有后代节点的变动  attributes: true, // 是否观察属性的变动  attributeOldValue: true, // 是否观察属性的变动的旧值  characterData: true, // 是否节点内容或节点文本的变动  characterDataOldValue: true, // 是否节点内容或节点文本的变动的旧值  // attributeFilter: ['class', 'src'] 不在此数组中的属性变化时将被忽略};const observer = new MutationObserver((mutationList) => {    // mutationList: array of mutation});observer.observe(document.documentElement, options);

使用起来很简单,你只需要指定一个根节点和需要监控的一些选项,那么当DOM变化时,在callback函数中就会有一个mutationList,这是一个DOM的变化列表,其中mutation的结构大概为:

{    type: 'childList', // or characterData、attributes    target: <DOM>,    // other params}

我们使用一个数组来存放mutation,具体的callback为:

const onMutationChange = (mutationsList) => {  const getFlowId = (node) => {    if (node) {      // 新插入的DOM没有标记,所以这里需要兼容      if (!node.__flow) node.__flow = { id: uuid() };      return node.__flow.id;    }  };  mutationsList.forEach((mutation) => {    const { target, type, attributeName } = mutation;    const record = {       type,       target: getFlowId(target),     };    switch (type) {      case 'characterData':        record.value = target.nodeValue;        break;      case 'attributes':        record.attributeName = attributeName;        record.attributeValue = target.getAttribute(attributeName);        break;      case 'childList':        record.removedNodes = [...mutation.removedNodes].map(n => getFlowId(n));        record.addedNodes = [...mutation.addedNodes].map((n) => {          const snapshot = this.takeSnapshot(n);          return {            ...snapshot,            nextSibling: getFlowId(n.nextSibling),            previousSibling: getFlowId(n.previousSibling)          };        });        break;    }    this.records.push(record);  });}function takeSnapshot(node, options = {}) {  this.markNodes(node);  const snapshot = {    vdom: createVirtualDom(node),  };  if (options.doctype === true) {    snapshot.doctype = document.doctype.name;    snapshot.clientWidth = document.body.clientWidth;    snapshot.clientHeight = document.body.clientHeight;  }  return snapshot;}

这里面只需要注意,当你处理新增DOM的时候,你需要一次增量的快照,这里仍然使用Virtual DOM来记录,在后面播放的时候,仍然生成DOM,插入到父元素即可,所以这里需要参照DOM,也就是兄弟节点。

表单元素监控

上面的MutationObserver并不能监控到input等元素的值变化,所以我们需要对表单元素的值进行特殊处理。

oninput事件监听

MDN文档: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput

事件对象:select、input,textarea

window.addEventListener('input', this.onFORMInput, true);onFormInput = (event) => {  const target = event.target;  if (    target &&     target.__flow &&    ['select', 'textarea', 'input'].includes(target.tagName.toLowerCase())   ) {     this.records.push({       type: 'input',        target: target.__flow.id,        value: target.value,      });   }}

在window上使用捕获来捕获事件,后面也是这样处理的,这样做的原因是我们是可能并经常在冒泡阶段阻止冒泡来实现一些功能,所以使用捕获可以减少事件丢失,另外,像scroll事件是不会冒泡的,必须使用捕获。

onchange事件监听

MDN文档: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput

input事件没法满足type为checkbox和radio的监控,所以需要借助onchange事件来监控

window.addEventListener('change', this.onFormChange, true);onFormChange = (event) => {  const target = event.target;  if (target && target.__flow) {    if (      target.tagName.toLowerCase() === 'input' &&      ['checkbox', 'radio'].includes(target.getAttribute('type'))    ) {      this.records.push({        type: 'checked',         target: target.__flow.id,         checked: target.checked,      });    }  }}

onfocus事件监听

MDN文档: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onfocus

window.addEventListener('focus', this.onFormFocus, true);onFormFocus = (event) => {  const target = event.target;  if (target && target.__flow) {    this.records.push({      type: 'focus',       target: target.__flow.id,    });  }}

onblur事件监听

MDN文档: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onblur

window.addEventListener('blur', this.onFormBlur, true);onFormBlur = (event) => {  const target = event.target;  if (target && target.__flow) {    this.records.push({      type: 'blur',       target: target.__flow.id,    });  }}

媒体元素变化监听

这里指audio和video,类似上面的表单元素,可以监听onplay、onpause事件、timeupdate、volumechange等等事件,然后存入records

Canvas画布变化监听

canvas内容变化没有抛出事件,所以我们可以:

收集canvas元素,定时去更新实时内容 hack一些画画的API,来抛出事件

canvas监听研究没有很深入,需要进一步深入研究

播放

web实现页面录屏的示例分析

思路比较简单,就是从后端拿到一些信息:

  • 全量快照Virtual DOM

  • 操作链records

  • 屏幕分辨率

  • doctype

利用这些信息,你就可以首先生成页面DOM,其中包括过滤script标签,然后创建iframe,append到一个容器中,其中使用一个map来存储DOM

function play(options = {}) {  const { container, records = [], snapshot ={} } = options;  const { vdom, doctype, clientHeight, clientWidth } = snapshot;  this.nodeCache = {};  this.records = records;  this.container = container;  this.snapshot = snapshot;  this.iframe = document.createElement('iframe');  const documentElement = createElement(vdom, (node) => {    // 缓存DOM    const flowId = node.__flow && node.__flow.id;    if (flowId) {      this.nodeCache[flowId] = node;    }    // 过滤script    return !(node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'script');   });      this.iframe.style.width = `${clientWidth}px`;  this.iframe.style.height = `${clientHeight}px`;  container.appendChild(iframe);  const doc = iframe.contentDocument;  this.iframeDocument = doc;  doc.open();  doc.write(`<!doctype ${doctype}><html><head></head><body></body></html>`);  doc.close();  doc.replaceChild(documentElement, doc.documentElement);  this.execRecords();}
function execRecords(preDuration = 0) {  const record = this.records.shift();  let node;  if (record) {    setTimeout(() => {      switch (record.type) {        // 'childList'、'characterData'、        // 'attributes'、'input'、'checked'、        // 'focus'、'blur'、'play''pause'等事件的处理      }      this.execRecords(record.duration);    }, record.duration - preDuration)  }}

上面的duration在上文中省略了,这个你可以根据自己的优化来做播放的流畅度,看是多个record作为一帧还是原本呈现。

以上是“web实现页面录屏的示例分析”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注编程网精选频道!

--结束END--

本文标题: web实现页面录屏的示例分析

本文链接: https://www.lsjlt.com/news/257833.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • web实现页面录屏的示例分析
    小编给大家分享一下web实现页面录屏的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!录屏重现错误场景如果你的应用有接入到web apm系统中,那么你可能就...
    99+
    2023-06-09
  • Vue单页面骨架屏的示例分析
    这篇文章主要介绍了Vue单页面骨架屏的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。关于骨架屏介绍骨架屏的作用主要是在网络请求较慢时...
    99+
    2024-04-02
  • web页面更新脚本的示例分析
    小编给大家分享一下web页面更新脚本的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!#!/bin/bash#version 0.1  ...
    99+
    2023-06-03
  • iphone刘海屏页面适配的示例分析
    小编给大家分享一下iphone刘海屏页面适配的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1. 下面是实现iphone...
    99+
    2024-04-02
  • 开发响应式web页面的示例分析
    本篇文章给大家分享的是有关开发响应式web页面的示例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。   响应式web设计开发,使用媒体查询来适应不同的手...
    99+
    2023-06-05
  • web开发中页面静态化的示例分析
    这篇文章给大家分享的是有关web开发中页面静态化的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。   public void GenerathHtmlByString(...
    99+
    2024-04-02
  • html5页面的示例分析
    这篇文章将为大家详细讲解有关html5页面的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。内容 与之前的HTML4.01相比,HTML5增加了非常多的改变:①&n...
    99+
    2024-04-02
  • 如何实现Web前端页面跳转并取到值的示例分析
    这篇文章将为大家详细讲解有关如何实现Web前端页面跳转并取到值的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。这个是A页面这是B页面通过点击A页面上的添加日志 跳...
    99+
    2024-04-02
  • python页面解析的示例分析
    这篇文章主要介绍python页面解析的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!页面解析对于页面解析最强大的当然是正则表达式,这个对于不同网站不同的使用者都不一样,就不用过多的说明。其次就是解析库了,常用...
    99+
    2023-06-08
  • Web登录实例分析
    本文小编为大家详细介绍“Web登录实例分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“Web登录实例分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。 正文 1. 一个简...
    99+
    2024-04-02
  • Xamarin.FormsShell中页面的示例分析
    这篇文章主要为大家展示了“Xamarin.FormsShell中页面的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Xamarin.FormsShell中页面的示例分析”这篇文章吧。轻拍...
    99+
    2023-06-04
  • amazeui页面分析之怎么实现登录页面
    小编给大家分享一下amazeui页面分析之怎么实现登录页面,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一、总结tpl命名空间:tpl命名空间的样式都是从app....
    99+
    2023-06-09
  • web响应式页面举例分析
    本篇内容介绍了“web响应式页面举例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1、页面头部的met...
    99+
    2024-04-02
  • HTML5中单页面手势滑屏切换的示例分析
    这篇文章主要介绍HTML5中单页面手势滑屏切换的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!1、实现原理假设有5个页面,每个页面占屏幕100%宽,则创建一个p容器viewp...
    99+
    2024-04-02
  • react.js实现页面登录跳转示例
    1,页面目录信息: 2,从index.js导入路由信息BasicRoute.js,然后BasicRoute.js中存储App.js和StatisticsInformation.js...
    99+
    2023-01-31
    react.js登录跳转 react.js页面跳转
  • Javascript刷新页面的示例分析
    小编给大家分享一下Javascript刷新页面的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Javascript刷新页...
    99+
    2024-04-02
  • HTML页面结构的示例分析
    这篇文章主要介绍了HTML页面结构的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。 许多时刻学网页出产垦荒的时辰第一看到的印象深入...
    99+
    2024-04-02
  • 移动端h5页面根据屏幕适配的示例分析
    小编给大家分享一下移动端h5页面根据屏幕适配的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!方法一:引入淘宝开源的可伸缩布局方案引入淘宝开源的可伸缩布局方...
    99+
    2023-06-09
  • web开发中微信网页登录逻辑的示例分析
    这篇文章给大家分享的是有关web开发中微信网页登录逻辑的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。现在的网站开发,都绕不开微信登录(毕竟微信已经成为国民工具)。虽然文...
    99+
    2024-04-02
  • ajax实现无刷新分页的示例分析
    这篇文章给大家分享的是有关ajax实现无刷新分页的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Ajax 无刷新页面的好处:提供良好的客户体验,通过 Ajax 在后台从数据库中取得数据并展示,取缔了等待加...
    99+
    2023-06-08
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作