本文共 10345 字,大约阅读时间需要 34 分钟。
收集所有的DOM,存入一个queue中;
根据zIndex按照顺序将DOM一个个通过一定规则,把DOM和其CSS样式一起画到Canvas上。
监听mouseover事件,记录鼠标的clientX和clientY。
重放的时候使用js画出一个假的鼠标,根据坐标记录来更改"鼠标"的位置。
对页面DOM进行一次全量快照。包括样式的收集、JS脚本去除,并通过一定的规则给当前的每个DOM元素标记一个id。
监听所有可能对界面产生影响的事件,例如各类鼠标事件、输入事件、滚动事件、缩放事件等等,每个事件都记录参数和目标元素,目标元素可以是刚才记录的id,这样的每一次变化事件可以记录为一次增量的快照。
将一定量的快照发送给后端。
在后台根据快照和操作链进行播放。
outerHTML
const content = document.documentElement.outerHTML;
outerHTML
记录的DOM会将把临近的两个TextNode合并为一个节点,而我们后续监控DOM变化时会使用 MutationObserver
,此时你需要大量的处理来兼容这种TextNode的合并,不然你在还原操作的时候无法定位到操作的目标节点。 Node.TEXT_NODE
和 Node.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 }; }
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; }
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);
{ type: 'childList', // or characterData、attributes target:, // other params }
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; }
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.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, }); } } }
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, }); } }
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, }); } }
收集canvas元素,定时去更新实时内容
hack一些画画的API,来抛出事件
全量快照Virtual DOM
操作链records
屏幕分辨率
doctype
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(``); 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) } }
转载地址:http://zyfpi.baihongyu.com/