web页⾯录屏实现
在前⾯的话
在看到评论后,突然意识到⾃⼰没有提前说明,本⽂可以说是⼀篇调研学习⽂,是我⾃⼰感觉可⾏的⼀套⽅案,后续会去读读已经开源的⼀些类似的代码库,补⾜⾃⼰遗漏的⼀些细节,所以⼤家可以当作学习⽂,⽣产环境慎⽤。
录屏重现错误场景
如果你的应⽤有接⼊到web apm系统中,那么你可能就知道apm系统能帮你捕获到页⾯发⽣的未捕获错误,给出错误栈,帮助你定位到BUG。但是,有些时候,当你不知道⽤户的具体操作时,是没有办法重现这个错误的,这时候,如果有操作录屏,你就可以清楚地了解到⽤户的操作路径,从⽽复现这个BUG并且修复。
实现思路
思路⼀:利⽤Canvas截图
这个思路⽐较简单,就是利⽤canvas去画⽹页内容,⽐较有名的库有:,这个库的简单原理是:
收集所有的DOM,存⼊⼀个queue中;
根据zIndex按照顺序将DOM⼀个个通过⼀定规则,把DOM和其CSS样式⼀起画到Canvas上。
这个实现是⽐较复杂的,但是我们可以直接使⽤,所以我们可以获取到我们想要的⽹页截图。
为了使得⽣成的视频较为流畅,我们⼀秒中需要⽣成⼤约25帧,也就是需要25张截图,思路流程图如下:
但是,这个思路有个最致命的不⾜:为了视频流畅,⼀秒中我们需要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_NODE和Node.ELEMENT_NODE。同时,要注意,SVG和SVG⼦元素的创建需要使⽤API:createElementNS,所以,我们在记录Virtual DOM的时候,需要注意namespace的记录,上代码:
const SVG_NAMESPACE = '/2000/svg';
const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];
function createVirtualDom(element, isSVG = false)  {
switch (deType) {
case Node.TEXT_NODE:
return createVirtualText(element);
case Node.ELEMENT_NODE:
return createVirtualElement(element, isSVG || LowerCase() === 'svg');
default:
return null;
}
}
function createVirtualText(element) {
const vText = {
text: deValue,
type: 'VirtualText',
};
if (typeof element.__flow !== 'undefined') {
vText.__flow = element.__flow;
}
return vText;
}
function createVirtualElement(element, isSVG = false) {
const tagName = LowerCase();
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 (pe === 'VirtualText') {
node = );
} else {
node = typeof vdom.namespace === 'undefined'
: ateElementNS(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 = deValue;
break;
svg canvas
case 'attributes':
record.attributeName = attributeName;
record.attributeValue = Attribute(attributeName);
break;
case 'childList':
record.addedNodes = [...mutation.addedNodes].map((n) => {
const snapshot = this.takeSnapshot(n);
return {
...snapshot,
nextSibling: Sibling),
previousSibling: getFlowId(n.previousSibling)
};
});
break;
}
});
}
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事件监听
事件对象:select、input,textarea
window.addEventListener('input', FormInput, true);
onFormInput = (event) => {
const target = event.target;
if (
target &&
target.__flow &&
['select', 'textarea', 'input'].includes(LowerCase())
) {
type: 'input',
target: target.__flow.id,
value: target.value,
});
}
}
在window上使⽤捕获来捕获事件,后⾯也是这样处理的,这样做的原因是我们是可能并经常在冒泡阶段阻⽌冒泡来实现⼀些功能,所以使⽤捕获可以减少事件丢失,另外,像scroll事件是不会冒泡的,必须使⽤捕获。
onchange事件监听
input事件没法满⾜type为checkbox和radio的监控,所以需要借助onchange事件来监控
window.addEventListener('change', FormChange, true);
onFormChange = (event) => {
const target = event.target;
if (target && target.__flow) {
if (
LowerCase() === 'input' &&
['checkbox', 'radio'].Attribute('type'))
) {
type: 'checked',
target: target.__flow.id,
checked: target.checked,
});
}
}
}
onfocus事件监听
window.addEventListener('focus', FormFocus, true);
onFormFocus = (event) => {
const target = event.target;
if (target && target.__flow) {
type: 'focus',
target: target.__flow.id,
});
}
}
onblur事件监听
window.addEventListener('blur', FormBlur, true);
onFormBlur = (event) => {
const target = event.target;
if (target && target.__flow) {
type: 'blur',
target: target.__flow.id,
});
}
}
媒体元素变化监听
这⾥指audio和video,类似上⾯的表单元素,可以监听onplay、onpause事件、timeupdate、volumechange等等事件,然后存⼊records
Canvas画布变化监听
canvas内容变化没有抛出事件,所以我们可以:
收集canvas元素,定时去更新实时内容 hack⼀些画画的API,来抛出事件
canvas监听研究没有很深⼊,需要进⼀步深⼊研究
播放

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。