web网页页面录屏完成

日期:2021-02-26 类型:科技新闻 

关键词:小程序 活动,微信小程序demo,微信小程序游戏开发价格,手机小程序怎么做,视频播放微信小程序

在前面的话

在看到评价后,忽然观念到自身沒有提早表明,本文能够说是1篇调查学习培训文,是我自身觉得可行的1套计划方案,后续会去读读早已开源系统的1些相近的编码库,补足自身忽略的1些细节,因此大伙儿能够作为学习培训文,生产制造自然环境慎用。

录屏重现不正确情景

假如你的运用有接入到web apm系统软件中,那末你将会就了解apm系统软件能帮你捕捉到网页页面产生的未捕捉不正确,得出不正确栈,协助你精准定位到BUG。可是,一些情况下,当你不知道道客户的实际实际操作时,是沒有方法重现这个不正确的,这时候候,假如有实际操作录屏,你便可以清晰地掌握到客户的实际操作相对路径,从而复现这个BUG而且修补。

完成思路

思路1:运用Canvas截图

这个思路较为简易,便是运用canvas去画网页页面內容,较为着名的库有: html2canvas ,这个库的简易基本原理是:

  • 搜集全部的DOM,存入1个queue中;
  • 依据zIndex依照次序将DOM1个个根据1定标准,把DOM和其CSS款式1起画到Canvas上。

这个完成是较为繁杂的,可是大家能够立即应用,因此大家能够获得到大家要想的网页页面截图。

以便使得转化成的视頻较为顺畅,大家1秒中必须转化成大概25帧,也便是必须25张截图,思路步骤图以下:

可是,这个思路有个最致命的不够:以便视頻顺畅,1秒中大家必须25张图,1张图300KB,当大家必须30秒的视頻时,图的尺寸一共为220M,这么大的互联网花销显著不好。

思路2:纪录全部实际操作重现

以便减少互联网花销,大家换个思路,大家在最初的网页页面基本上,纪录下1步步实际操作,在大家必须"播发"的情况下,依照次序运用这些实际操作,这样大家就可以看到网页页面的转变了。这个思路把电脑鼠标实际操作和DOM转变分开:

电脑鼠标转变:

  • 监视mouseover恶性事件,纪录电脑鼠标的clientX和clientY。
  • 播放的情况下应用js画出1个假的电脑鼠标,依据座标纪录来变更"电脑鼠标"的部位。

DOM转变:

  • 对网页页面DOM开展1次全量快照。包含款式的搜集、JS脚本制作除去,并根据1定的标准给当今的每一个DOM元素标识1个id。
  • 监视全部将会对页面造成危害的恶性事件,比如各类电脑鼠标恶性事件、键入恶性事件、翻转恶性事件、放缩恶性事件这些,每一个恶性事件都纪录主要参数和总体目标元素,总体目标元素能够是刚刚纪录的id,这样的每次转变恶性事件能够纪录为1次增加量的快照。
  • 将1定量分析的快照推送给后端开发。
  • 在后台管理依据快照和实际操作链开展播发。

自然这个表明是较为简单的,电脑鼠标的纪录较为简易,大家不进行讲,关键表明1下DOM监管的完成思路。

网页页面初次全量快照

最先你将会会想起,要完成网页页面全量快照,能够立即应用 outerHTML

const content = document.documentElement.outerHTML;

这样就简易纪录了网页页面的全部DOM,你只必须最先给DOM提升标识id,随后获得outerHTML,随后除去JS脚本制作。

可是,这里有个难题,应用 outerHTML 纪录的DOM会将把邻近的两个TextNode合拼为1个连接点,而大家后续监管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用来纪录1些主要参数,包含标识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);

应用起来很简易,你只必须特定1个根连接点和必须监管的1些选项,那末当DOM转变时,在callback涵数中就会有1个mutationList,这是1个DOM的转变目录,在其中mutation的构造大约为:

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

大家应用1个数字能量数组来储放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的情况下,你必须1次增加量的快照,这里依然应用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上应用捕捉来捕捉恶性事件,后边也是这样解决的,这样做的缘故是大家是将会并常常在冒泡环节阻拦冒泡来完成1些作用,因此应用捕捉能够降低恶性事件遗失,此外,像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元素,定时执行去升级即时內容 hack1些画画的API,来抛出事了件

canvas监视科学研究沒有很深层次,必须进1步深层次科学研究

播发

思路较为简易,便是从后端开发拿到1些信息内容:

  • 全量快照Virtual DOM
  • 实际操作链records
  • 显示屏辨别率
  • doctype

运用这些信息内容,你便可以最先转化成网页页面DOM,在其中包含过虑script标识,随后建立iframe,append到1个器皿中,在其中应用1个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做为1帧還是本来展现。

以上便是本文的所有內容,期待对大伙儿的学习培训有一定的协助,也期待大伙儿多多适用脚本制作之家。