댓글을 읽다가 갑자기 제가 미리 설명하지 않았다는 것을 깨달았습니다. 이 기사는 제가 실현 가능하다고 생각하는 일련의 솔루션입니다. 비슷한 코드 라이브러리를 읽어보겠습니다. 내 지식을 보충하기 위해 오픈 소스로 제공되었습니다. 누락된 세부 사항이 있으므로 학습용 텍스트로 취급하고 프로덕션 환경에서 주의해서 사용할 수 있습니다.
오류 시나리오 재현을 위한 화면 녹화애플리케이션이 웹 APM 시스템에 연결되어 있으면 apm 시스템이 페이지에서 발생하는 포착되지 않은 오류를 캡처하고, 오류 스택을 제공하고, BUG를 찾는 데 도움을 줄 수 있다는 것을 알 수 있습니다. 그러나 때로는 사용자의 구체적인 조작을 알 수 없는 경우 버그를 재현할 수 있는 방법이 없는 경우가 있습니다. 이때 조작 화면 기록이 있으면 사용자의 조작 경로를 명확하게 파악할 수 있으므로 버그를 재현할 수 있습니다. 버그. 그리고 수리.
구현 아이디어 아이디어 1: 캔버스를 사용하여 스크린샷 찍기이 아이디어는 비교적 간단합니다. 캔버스를 사용하여 웹 콘텐츠를 그리는 것입니다. 더 유명한 라이브러리는 다음과 같습니다. 이 라이브러리의 간단한 원리는 다음과 같습니다.
이 구현은 비교적 복잡하지만 직접 사용할 수 있으므로 원하는 웹페이지의 스크린샷을 얻을 수 있습니다.
생성된 비디오를 더 부드럽게 만들기 위해서는 초당 약 25프레임을 생성해야 합니다. 즉, 25개의 스크린샷이 필요하다는 것을 의미합니다. 아이디어의 흐름도는 다음과 같습니다.
하지만 이 아이디어에는 가장 치명적인 결함이 있습니다. 동영상을 원활하게 만들기 위해서는 1초에 25장의 사진이 필요하며, 30초 분량의 동영상이 필요할 때 사진의 총 크기는 220M입니다. 그렇게 큰 네트워크 오버헤드는 분명히 아닙니다.
아이디어 2: 모든 작업의 반복을 기록합니다.네트워크 오버헤드를 줄이기 위해 우리는 초기 페이지를 기반으로 다음 단계의 작업을 기록합니다. 페이지. 이 아이디어는 마우스 작업과 DOM 변경을 분리합니다.
마우스 변경 사항:
물론 이 설명은 상대적으로 간단합니다. 마우스 기록은 DOM 모니터링의 구현 아이디어를 주로 설명하지 않습니다.
페이지의 첫 번째 전체 스냅샷 우선, 페이지의 전체 스냅샷을 얻으려면 직접적으로 outerHTML
사용할 수 있다고 생각할 수도 있습니다.
const 콘텐츠 = document.documentElement.outerHTML;
이는 단순히 페이지의 모든 DOM을 기록하기만 하면 됩니다. 먼저 DOM에 태그 ID를 추가한 다음 외부 HTML을 가져온 다음 JS 스크립트를 제거하면 됩니다.
그러나 여기에는 문제가 있습니다 outerHTML
사용하여 기록된 DOM은 두 개의 인접한 TextNode를 하나의 노드로 병합합니다. 이후 DOM 변경 사항을 모니터링할 때 이 MutationObserver
과 호환되기 위해서는 많은 처리가 필요합니다. 그렇지 않으면 복원 작업 중에 작업의 대상 노드를 찾을 수 없습니다.
그렇다면 페이지 DOM의 원래 구조를 유지할 수 있는 방법이 있습니까?
대답은 '예'입니다. 여기서는 Virtual DOM을 사용하여 DOM 구조를 기록하고, documentElement를 Virtual DOM으로 변환하고, 기록하고, 나중에 복원할 때 DOM을 재생성합니다.
DOM을 가상 DOM으로 변환 여기서는 Node.TEXT_NODE
및 Node.ELEMENT_NODE
라는 두 가지 노드 유형만 신경 쓰면 됩니다. 동시에 SVG 및 SVG 하위 요소를 생성하려면 createElementNS API를 사용해야 합니다. 따라서 Virtual DOM을 기록할 때 위 코드에 주의해야 합니다.
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];function createVirtualDom(element, isSVG = false) { 스위치(element.nodeType) { 케이스 Node.TEXT_NODE: 반환 createVirtualText(element); 케이스 Node.ELEMENT_NODE: return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg'); 기본값: return null }}function createVirtualText(element) { const vText = { 텍스트: element.nodeValue, 유형: 'VirtualText', } if (typeof element.__flow !== '정의되지 않음') { vText.__flow = element.__flow; } return vText;}function createVirtualElement(element, isSVG = false) { const tagName = element.tagName.toLowerCase(); const children = getNodeChildren(element, isSVG ); const { attr, 네임스페이스 } = getNodeAttributes(element, isSVG); tagName, 유형: 'VirtualElement', 하위, 속성: attr, 네임스페이스, }; if (typeof element.__flow !== 'undefine') { vElement.__flow = element.__flow; } return vElement;}function getNodeChildren(element, isSVG = false) { const childNodes = element.childNodes ? [...element.childNodes] : []; childNodes.forEach((cnode) => { children.push(createVirtualDom(cnode, isSVG)); }); return children.filter(c => !!c);}function getNodeAttributes(element, isSVG = false) { const 속성 = 요소.속성 ? [...요소.속성] : []; attribute.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { 네임스페이스 = nodeValue; } else if (isSVG) { 네임스페이스 = SVG_NAMESPACE; } }); return { 속성, 네임스페이스 };}
위의 코드를 통해 전체 documentElement를 가상 DOM으로 변환할 수 있으며, 여기서 __flow는 태그 ID 등을 포함한 일부 매개변수를 기록하는 데 사용됩니다. 가상 노드는 유형, 속성, 하위, 네임스페이스를 기록합니다.
DOM으로 복원된 가상 DOM가상 DOM을 DOM으로 복원하는 것은 상대적으로 간단합니다. JS 스크립트를 실행할 필요가 없기 때문에 DOM을 재귀적으로 생성하기만 하면 됩니다.
function createElement(vdom, nodeFilter = () => true) { let node; if (vdom.type === 'VirtualText') { node = document.createTextNode(vdom.text) } else { node = typeof vdom.namespace; === '정의되지 않음' ? document.createElement(vdom.tagName) : document.createElementNS(vdom.namespace, vdom.tagName) for (이름 입력) 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; } 노드 반환;}DOM 구조 변경 모니터링
여기서는 MutationObserver라는 API를 사용합니다. 더욱 기쁜 점은 이 API가 모든 브라우저와 호환되므로 과감하게 사용할 수 있다는 것입니다.
MutationObserver 사용:
const options = { childList: true, // 하위 노드의 변경 사항을 관찰할지 여부 subtree: true, // 모든 하위 노드의 변경 사항을 관찰할지 여부 attribute: true, // 속성의 변경 사항을 관찰할지 여부 attributeOldValue: true, // 여부 속성의 변경 사항을 관찰하려면 변경된 이전 값 CharacterData: true, // 노드 내용 또는 노드 텍스트가 변경되는지 여부 CharacterDataOldValue: true, // 노드 내용 또는 노드 텍스트가 이전 값을 변경하는지 여부 // attributeFilter: ['class', ' 소스'] 변경 시 이 배열에 없는 속성은 무시됩니다.};const 관찰자 = new MutationObserver((mutationList) => { // mutationList: array of mutation});observer.observe(document.documentElement, options);
사용이 매우 간단합니다. 모니터링해야 하는 일부 옵션과 루트 노드만 지정하면 됩니다. 그런 다음 DOM이 변경되면 콜백 함수에 DOM 변경 목록인 mutationList가 있게 됩니다. 돌연변이의 대략적인 내용은 다음과 같습니다.
{ 유형: 'childList', // 또는 CharacterData, 속성 대상: <DOM>, // 기타 매개변수}
특정 콜백은 다음과 같습니다.
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 }; 변형; const 레코드 = { 유형, 대상: getFlowId(대상), }; 스위치(유형) { 케이스 'characterData': 레코드.값 = target.nodeValue break; attributeValue = target.getAttribute(attributeName); 케이스 'childList': Record.removedNodes = [...mutation.removedNodes].map(n => getFlowId(n)); Record.addNodes = [...mutation. addedNodes].map((n) => { const snapshot = this.takeSnapshot(n); return { ...snapshot, nextSibling: getFlowId(n. nextSibling), 이전Sibling: getFlowId(n.previousSibling) }) } 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;
여기서는 새 DOM을 처리할 때 증분 스냅샷이 필요하다는 점만 기억하면 됩니다. 여기서는 여전히 가상 DOM을 사용하여 나중에 재생할 때 DOM을 생성하고 상위 요소에 삽입합니다. 형제 노드인 DOM을 참조해야 합니다.
양식 요소 모니터링위의 MutationObserver는 입력 및 기타 요소의 값 변화를 모니터링할 수 없으므로 양식 요소의 값에 대해 특별한 처리를 수행해야 합니다.
oninput 이벤트 수신MDN 문서: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
이벤트 객체: 선택, 입력, 텍스트 영역
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({ 유형: '입력', 대상: target.__flow.id, 값: target.value, });
캡처를 사용하여 창에서 이벤트를 캡처합니다. 그 이유는 버블링 단계에서 일부 기능을 구현하는 동안 버블링을 방지할 수 있기 때문에 캡처를 사용하면 스크롤 이벤트와 같은 이벤트 손실도 줄일 수 있기 때문입니다. 거품이 생기지 않으므로 캡처해야 합니다.
onchange 이벤트 청취MDN 문서: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
입력 이벤트는 체크박스와 라디오 형태의 모니터링을 만족시킬 수 없으므로 모니터링을 위해서는 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({ 유형: '확인됨', 대상: target.__flow.id, 확인됨: 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.__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.__flow.id, });미디어 요소 변경 모니터링
위의 양식 요소와 유사하게 onplay, onpause 이벤트, timeupdate, Volumechange 및 기타 이벤트를 모니터링하고 기록에 저장할 수 있습니다.
캔버스 캔버스 변화 모니터링캔버스 내용이 변경되면 이벤트가 발생하지 않으므로 다음을 수행할 수 있습니다.
캔버스 요소를 수집하고 정기적으로 실시간 콘텐츠를 업데이트하여 일부 그리기 API를 해킹하여 이벤트를 발생시키세요.
캔버스 모니터링에 대한 연구는 그다지 심층적이지 않아 앞으로 더 심층적인 연구가 필요하다.
놀다아이디어는 비교적 간단합니다. 백엔드에서 몇 가지 정보를 얻으면 됩니다.
이 정보를 사용하면 먼저 필터링 스크립트 태그가 포함된 페이지 DOM을 생성한 다음 iframe을 생성하고 이를 컨테이너에 추가할 수 있습니다. 컨테이너는 지도를 사용하여 DOM을 저장합니다.
function play(options = {}) { const { 컨테이너, 레코드 = [], snapshot ={} } = 옵션; const { vdom, doctype, clientHeight, clientWidth } = snapshot; records; this.container = 컨테이너; this.snapshot = snapshot; this.iframe = document.createElement('iframe') const documentElement(vdom, (node) => { // 캐시 DOM const flowId = node.__flow && node.__flow.id; if (flowId) { this.nodeCache[flowId] = node } // 필터 스크립트 return !(node.nodeType == = Node.ELEMENT_NODE && node.tagName.toLowerCase() === '스크립트') }); `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; const doc = iframeDocument = 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' , // '속성', 'input', 'checked', // 'focus', 'blur', 'play''pause' 및 기타 이벤트 처리} this.execRecords(record.duration) }, Record.duration - preDuration) }}
위 글에서는 위의 지속시간을 생략하고 자신의 최적화에 따라 재생 부드러움을 최적화할 수 있으며, 여러 레코드가 하나의 프레임으로 표시되는지 아니면 그대로 표시되는지 확인할 수 있습니다.
위 내용은 이 기사의 전체 내용입니다. 모든 분들의 학습에 도움이 되기를 바랍니다. 또한 모든 분들이 VeVb Wulin Network를 지지해 주시길 바랍니다.