หลังจากอ่านความคิดเห็น ฉันก็รู้ทันทีว่าฉันไม่ได้อธิบายไว้ล่วงหน้า บทความนี้อาจกล่าวได้ว่าเป็นบทความวิจัยและการเรียนรู้ มันเป็นชุดวิธีแก้ปัญหาที่ฉันรู้สึกว่าเป็นไปได้ เป็นโอเพ่นซอร์สเพื่อเสริมความรู้ของฉันเอง มีรายละเอียดบางอย่างที่ขาดหายไป ดังนั้นคุณจึงถือเป็นข้อความการเรียนรู้และใช้อย่างระมัดระวังในสภาพแวดล้อมการใช้งานจริง
การบันทึกหน้าจอเพื่อสร้างสถานการณ์ข้อผิดพลาดขึ้นมาใหม่หากแอปพลิเคชันของคุณเชื่อมต่อกับระบบ web apm คุณอาจทราบว่าระบบ apm สามารถช่วยให้คุณจับข้อผิดพลาดที่ตรวจไม่พบซึ่งเกิดขึ้นบนเพจ จัดเตรียมสแต็กข้อผิดพลาด และช่วยคุณค้นหา BUG อย่างไรก็ตาม บางครั้งเมื่อคุณไม่ทราบการทำงานเฉพาะของผู้ใช้ ก็ไม่มีทางที่จะทำให้เกิดข้อผิดพลาดได้ ในขณะนี้ หากมีการบันทึกหน้าจอการทำงาน คุณสามารถเข้าใจเส้นทางการทำงานของผู้ใช้ได้อย่างชัดเจน จึงทำให้เกิดข้อผิดพลาดได้ BUG และการซ่อมแซม
แนวคิดในการนำไปปฏิบัติ แนวคิดที่ 1: ใช้ Canvas เพื่อจับภาพหน้าจอแนวคิดนี้ค่อนข้างง่าย ซึ่งก็คือการใช้ Canvas ในการวาดเนื้อหาเว็บ ไลบรารีที่มีชื่อเสียงกว่าคือ: html2canvas หลักการง่ายๆ ของไลบรารีนี้คือ:
การใช้งานนี้ค่อนข้างซับซ้อน แต่เราสามารถใช้งานได้โดยตรง เพื่อที่เราจะได้ภาพหน้าจอของหน้าเว็บที่เราต้องการ
เพื่อให้วิดีโอที่สร้างขึ้นมีความราบรื่นยิ่งขึ้น เราจำเป็นต้องสร้างประมาณ 25 เฟรมต่อวินาที ซึ่งหมายความว่าเราจำเป็นต้องมีภาพหน้าจอ 25 ภาพ ผังงานของแนวคิดมีดังนี้:
อย่างไรก็ตาม แนวคิดนี้มีข้อบกพร่องร้ายแรงที่สุด: เพื่อให้วิดีโอราบรื่น เราจำเป็นต้องมีรูปภาพ 25 ภาพในหนึ่งวินาที และหนึ่งภาพคือ 300KB เมื่อเราต้องการวิดีโอความยาว 30 วินาที ขนาดรูปภาพทั้งหมดจะเท่ากับ 220M ค่าใช้จ่ายเครือข่ายขนาดใหญ่ดังกล่าวไม่ชัดเจน
แนวคิดที่ 2: บันทึกการเกิดซ้ำของการดำเนินการทั้งหมดเพื่อลดค่าใช้จ่ายของเครือข่าย เราเปลี่ยนความคิดของเรา เราบันทึกการดำเนินการทีละขั้นตอนตามหน้าแรก เมื่อเราต้องการเล่น เราใช้การดำเนินการเหล่านี้ตามลำดับ เพื่อที่เราจะได้เห็นการเปลี่ยนแปลงใน หน้าหนังสือ. แนวคิดนี้แยกการทำงานของเมาส์และการเปลี่ยนแปลง DOM:
การเปลี่ยนแปลงของเมาส์:
แน่นอนว่าคำอธิบายนี้ค่อนข้างสั้น เราจะไม่ลงรายละเอียดเกี่ยวกับแนวคิดการใช้งาน DOM
ภาพเต็มแรกของเพจ ก่อนอื่น คุณอาจคิดว่าเพื่อให้ได้สแนปชอตแบบเต็มของหน้า คุณสามารถใช้ outerHTML
ได้โดยตรง
เนื้อหา const = document.documentElement.outerHTML;
นี่เป็นเพียงการบันทึก DOM ทั้งหมดของเพจ คุณจะต้องเพิ่มรหัสแท็กลงใน DOM ก่อน จากนั้นจึงรับ externalHTML จากนั้นจึงลบสคริปต์ JS
อย่างไรก็ตาม มีปัญหาเกิดขึ้นที่นี่ DOM ที่บันทึกโดยใช้ outerHTML
จะรวม TextNodes สองอันที่อยู่ติดกันเป็นโหนดเดียว เมื่อเราตรวจสอบการเปลี่ยนแปลง DOM ในภายหลัง เราจะใช้ MutationObserver
ในเวลานี้ คุณต้องมีการประมวลผลจำนวนมากเพื่อให้เข้ากันได้กับการรวมนี้ ของ TextNodes มิฉะนั้นคุณจะไม่สามารถค้นหาโหนดเป้าหมายของการดำเนินการระหว่างการดำเนินการกู้คืนได้
แล้วมีวิธีใดบ้างที่เราจะรักษาโครงสร้างเดิมของเพจ DOM ไว้ได้?
คำตอบคือใช่ ที่นี่เราใช้ Virtual DOM เพื่อบันทึกโครงสร้าง DOM เปลี่ยน documentElement เป็น Virtual DOM บันทึก และสร้าง DOM ใหม่เมื่อกู้คืนในภายหลัง
แปลง DOM เป็น DOM เสมือน เราจำเป็นต้องสนใจ Node สองประเภทเท่านั้นที่นี่: Node.TEXT_NODE
และ Node.ELEMENT_NODE
ในเวลาเดียวกัน ควรสังเกตว่าการสร้างองค์ประกอบย่อย SVG และ SVG ต้องใช้ API: createElementNS ดังนั้น เมื่อเราบันทึก Virtual DOM เราจำเป็นต้องใส่ใจกับโค้ดข้างต้น:
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];ฟังก์ชัน createVirtualDom(องค์ประกอบ, isSVG = false) { สวิตช์ (element.nodeType) { case Node.TEXT_NODE: return createVirtualText (องค์ประกอบ); case Node.ELEMENT_NODE: return createVirtualElement (element, isSVG || element.tagName.toLowerCase() === 'svg'); ค่าเริ่มต้น: return null; }} ฟังก์ชั่น createVirtualText (องค์ประกอบ) { 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 { attr, เนมสเปซ } = getNodeAttributes (องค์ประกอบ, isSVG); tagName, ประเภท: 'VirtualElement', เด็ก ๆ , คุณลักษณะ: attr, เนมสเปซ, }; if (typeof element.__flow !== 'unknown') { 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);} ฟังก์ชัน getNodeAttributes(องค์ประกอบ, isSVG = false) { const คุณลักษณะ = element.attributes ? [...element.attributes] : []; คุณลักษณะ.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { namespace = nodeValue; } else if (isSVG) { namespace = SVG_NAMESPACE; } }); กลับ { attr, เนมสเปซ };}
ด้วยโค้ดข้างต้น เราสามารถแปลง documentElement ทั้งหมดเป็น Virtual DOM ซึ่ง __flow ใช้เพื่อบันทึกพารามิเตอร์บางตัว รวมถึง ID แท็ก ฯลฯ บันทึกโหนดเสมือน: ประเภท คุณลักษณะ ลูก เนมสเปซ
DOM เสมือนถูกกู้คืนเป็น DOMการกู้คืน Virtual DOM เป็น DOM ทำได้ค่อนข้างง่าย คุณเพียงแค่ต้องสร้าง DOM แบบเรียกซ้ำเท่านั้น nodeFilter ใช้เพื่อกรององค์ประกอบสคริปต์เนื่องจากเราไม่ต้องการการดำเนินการของสคริปต์ JS
ฟังก์ชัน createElement(vdom, nodeFilter = () => true) { ให้ node; if (vdom.type === 'VirtualText') { node = document.createTextNode(vdom.text); } else { node = typeof vdom.namespace === 'unknown' ? document.createElement(vdom.tagName) : document.createElementNS(vdom.namespace, vdom.tagName); for (ให้ใส่ชื่อเข้าไป) vdom.attributes) { node.setAttribute (ชื่อ, vdom.attributes [ชื่อ]); } vdom.children.forEach ((cnode) => { const childNode = createElement (cnode, nodeFilter); if (childNode && nodeFilter (childNode) ) { node.appendChild(โหนดลูก) } }); } ถ้า (vdom.__flow) { node.__flow = vdom.__flow; } ส่งคืนโหนด;}การตรวจสอบการเปลี่ยนแปลงโครงสร้าง DOM
ที่นี่ เราใช้ API: MutationObserver สิ่งที่น่ายินดียิ่งกว่าคือ API นี้เข้ากันได้กับเบราว์เซอร์ทั้งหมด ดังนั้นเราจึงสามารถใช้มันได้อย่างกล้าหาญ
การใช้ MutationObserver:
const options = { childList: true, // ไม่ว่าจะสังเกตการเปลี่ยนแปลงในแผนผังย่อยของโหนดย่อย: true, // ไม่ว่าจะสังเกตการเปลี่ยนแปลงในแอตทริบิวต์ของโหนดที่สืบทอดทั้งหมดหรือไม่: จริง, // ไม่ว่าจะสังเกตการเปลี่ยนแปลงในแอตทริบิวต์ของแอตทริบิวต์ คุณลักษณะOldValue: จริง, // ไม่ว่าจะ เพื่อสังเกตการเปลี่ยนแปลงในคุณลักษณะ ค่าเก่าที่เปลี่ยนแปลง characterData: true, // ไม่ว่าเนื้อหาโหนดหรือข้อความของโหนดจะเปลี่ยน characterDataOldValue: true, // ไม่ว่าเนื้อหาโหนดหรือข้อความโหนดจะเปลี่ยนค่าเก่า //attributeFilter: ['class', ' เอสอาร์ซี'] คุณสมบัติที่ไม่ได้อยู่ในอาร์เรย์นี้จะถูกละเว้นเมื่อเปลี่ยน};const Observer = new MutationObserver((mutationList) => { // MutationList: array ofmutation});observer.observe(document.documentElement, options);
มันใช้งานง่ายมาก คุณเพียงแค่ต้องระบุโหนดรูทและตัวเลือกบางตัวที่ต้องได้รับการตรวจสอบ จากนั้นเมื่อ DOM เปลี่ยนแปลง ก็จะมีmutationList ในฟังก์ชัน callback ซึ่งเป็นรายการของการเปลี่ยนแปลงโครงสร้าง ของการกลายพันธุ์ก็ประมาณนี้:
{ ประเภท: 'childList', // หรือ characterData, แอตทริบิวต์เป้าหมาย: <DOM>, // พารามิเตอร์อื่น ๆ }
เราใช้อาร์เรย์เพื่อจัดเก็บการกลายพันธุ์ การโทรกลับเฉพาะคือ:
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // DOM ที่แทรกใหม่ไม่มีเครื่องหมาย ดังนั้นจึงจำเป็นต้องเข้ากันได้ที่นี่ถ้า (!node.__flow) node.__flow = { id: uuid() }; return node.__flow.id; } }; การกลายพันธุ์; const record = { type, target: getFlowId(target), }; switch (type) { case 'CharacterData': record.value = target.nodeValue; คุณลักษณะค่า = target.getAttribute(attributeName); ตัวพิมพ์เล็ก '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), พี่น้องก่อนหน้า: getFlowId(n.previousSibling) }); } this.records.push(บันทึก); });}ฟังก์ชัน takeSnapshot(node, options = {}) { this.markNodes(node); const snapshot = { vdom: createVirtualDom(node), }; document.doctype.name; snapshot.clientWidth = document.body.clientWidth; snapshot.clientHeight; } กลับสแน็ปช็อต;
โปรดทราบว่าเมื่อคุณประมวลผล DOM ใหม่ คุณต้องมีสแน็ปช็อตส่วนเพิ่ม ที่นี่ คุณยังคงใช้ Virtual DOM ในการบันทึก เมื่อคุณเล่นในภายหลัง คุณยังคงสร้าง DOM และแทรกลงในองค์ประกอบหลัก ดังนั้นที่นี่ คุณต้องอ้างอิงถึง DOM ซึ่งเป็นโหนดพี่น้อง
การตรวจสอบองค์ประกอบของแบบฟอร์มMutationObserver ข้างต้นไม่สามารถติดตามการเปลี่ยนแปลงค่าของอินพุตและองค์ประกอบอื่น ๆ ได้ ดังนั้นเราจึงจำเป็นต้องดำเนินการประมวลผลพิเศษกับค่าขององค์ประกอบของแบบฟอร์ม
การฟังเหตุการณ์ oninputเอกสาร MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
วัตถุเหตุการณ์: เลือก, อินพุต, พื้นที่ข้อความ
window.addEventListener('input', this.onFormInput, true);onFormInput = (เหตุการณ์) => { 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.__flow) { if ( target.tagName.toLowerCase() == = 'input' && ['ช่องทำเครื่องหมาย', '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.__flow) { this.records.push ({ ประเภท: '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({ ประเภท: 'blur ', เป้าหมาย: target.__flow.id, }); }}การตรวจสอบการเปลี่ยนแปลงองค์ประกอบสื่อ
นี่หมายถึงเสียงและวิดีโอ เช่นเดียวกับองค์ประกอบของแบบฟอร์มด้านบน คุณสามารถตรวจสอบเหตุการณ์ onplay, onpause, การอัปเดตเวลา, การเปลี่ยนแปลงระดับเสียง และเหตุการณ์อื่นๆ จากนั้นจัดเก็บไว้ในบันทึก
การตรวจสอบการเปลี่ยนแปลงผืนผ้าใบไม่มีเหตุการณ์ใดถูกโยนทิ้งเมื่อเนื้อหาแคนวาสเปลี่ยนแปลง ดังนั้นเราจึงสามารถ:
รวบรวมองค์ประกอบ Canvas และอัปเดตเนื้อหาแบบเรียลไทม์เป็นประจำ แฮ็ก API การวาดภาพบางส่วนเพื่อจัดกิจกรรม
การวิจัยเกี่ยวกับการติดตามผืนผ้าใบไม่ได้เจาะลึกมากนักและจำเป็นต้องมีการวิจัยเชิงลึกเพิ่มเติม
เล่นแนวคิดนี้ค่อนข้างง่าย เพียงรับข้อมูลจากแบ็กเอนด์:
เมื่อใช้ข้อมูลนี้ คุณสามารถสร้างหน้า DOM ซึ่งรวมถึงแท็กสคริปต์กรอง จากนั้นสร้าง iframe และต่อท้ายคอนเทนเนอร์ ซึ่งใช้แผนที่เพื่อจัดเก็บ DOM
ฟังก์ชั่นเล่น (ตัวเลือก = {}) { const { คอนเทนเนอร์ บันทึก = [], สแน็ปช็อต ={} } = ตัวเลือก; const { vdom, doctype, clientHeight, clientWidth } = สแน็ปช็อต; this.nodeCache = {}; บันทึก; this.container = คอนเทนเนอร์; this.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; } // สคริปต์ตัวกรองส่งคืน !(node.nodeType == = Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'สคริปต์'); }); this.iframe.style.width = `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; const doc = iframe.contentDocument; this.iframeDocument = doc.open(); doc.write(`<!doctype ${doctype}><html><head></head><body></body></html>`); doc.close(); doc.replaceChild(documentElement, doc.documentElement); this.execRecords();}
ฟังก์ชั่น execRecords (preDuration = 0) { const record = this.records.shift (); ให้โหนด; if (บันทึก) { setTimeout (() => { สวิตช์ (record.type) { // 'childList', 'CharacterData' , // การจัดการ 'แอตทริบิวต์', 'อินพุต', 'ตรวจสอบ', // 'โฟกัส', 'เบลอ', 'เล่น' หยุดชั่วคราว' และเหตุการณ์อื่น ๆ} this.execRecords(record.duration); }, บันทึก.ระยะเวลา - ระยะเวลาล่วงหน้า) }}
ระยะเวลาข้างต้นไม่ได้ระบุไว้ในบทความด้านบน คุณสามารถปรับความราบรื่นในการเล่นให้เหมาะสมตามการปรับให้เหมาะสมของคุณเอง และดูว่าหลายบันทึกถูกนำเสนอเป็นเฟรมเดียวหรือตามที่แสดง
ข้างต้นคือเนื้อหาทั้งหมดของบทความนี้ ฉันหวังว่ามันจะเป็นประโยชน์ต่อการศึกษาของทุกคน ฉันหวังว่าทุกคนจะสนับสนุน VeVb Wulin Network