Setelah membaca komentar, saya tiba-tiba menyadari bahwa saya tidak menjelaskannya sebelumnya. Artikel ini dapat dikatakan sebagai artikel penelitian dan pembelajaran. Ini adalah serangkaian solusi yang saya rasa layak telah menjadi sumber terbuka untuk menambah pengetahuan saya. Ada beberapa detail yang hilang, sehingga Anda dapat memperlakukannya sebagai teks pembelajaran dan menggunakannya dengan hati-hati di lingkungan produksi.
Rekaman layar untuk mereproduksi skenario kesalahanJika aplikasi Anda terhubung ke sistem web apm, maka Anda mungkin tahu bahwa sistem apm dapat membantu Anda menangkap kesalahan yang tidak tertangkap yang terjadi pada halaman, menyediakan tumpukan kesalahan, dan membantu Anda menemukan BUG. Namun, terkadang, ketika Anda tidak mengetahui operasi spesifik pengguna, tidak ada cara untuk mereproduksi bug tersebut. Saat ini, jika ada rekaman layar operasi, Anda dapat dengan jelas memahami jalur operasi pengguna, sehingga mereproduksi BUG. Dan perbaikan.
Ide implementasi Ide 1: Gunakan Canvas untuk mengambil tangkapan layarIdenya relatif sederhana, yaitu menggunakan kanvas untuk menggambar konten web. Perpustakaan yang lebih terkenal adalah: html2canvas. Prinsip sederhana dari perpustakaan ini adalah:
Implementasinya memang relatif rumit, namun kita bisa menggunakannya secara langsung, sehingga kita bisa mendapatkan screenshot halaman web yang kita inginkan.
Agar video yang dihasilkan lebih lancar, kita perlu menghasilkan sekitar 25 frame per detik, yang berarti kita memerlukan 25 screenshot. Diagram alur idenya adalah sebagai berikut:
Namun, ide ini memiliki kelemahan paling fatal: untuk membuat video menjadi lancar, kita memerlukan 25 gambar dalam satu detik, dan satu gambar berukuran 300KB. Saat kita membutuhkan video berdurasi 30 detik, ukuran total gambar adalah 220M. Jelas tidak ada overhead jaringan sebesar itu.
Ide 2: Catat pengulangan semua operasiUntuk mengurangi overhead jaringan, kami mengubah pemikiran kami. Kami mencatat operasi langkah demi langkah berikutnya berdasarkan halaman awal. Saat kami perlu bermain, kami menerapkan operasi ini secara berurutan, sehingga kami dapat melihat perubahannya halaman. Ide ini memisahkan operasi mouse dan perubahan DOM:
Perubahan tikus:
Tentu saja penjelasan ini relatif singkat. Perekaman mouse relatif sederhana. Kami tidak akan menjelaskan secara detail. Kami hanya akan menjelaskan ide implementasi pemantauan DOM.
Cuplikan penuh pertama dari halaman tersebut Pertama-tama, Anda mungkin berpikir bahwa untuk mendapatkan cuplikan halaman secara penuh, Anda dapat langsung menggunakan outerHTML
const konten = document.documentElement.outerHTML;
Ini hanya mencatat semua DOM halaman. Anda hanya perlu menambahkan id tag ke DOM terlebih dahulu, lalu mendapatkan HTML luar, lalu menghapus skrip JS.
Namun, ada masalah di sini. DOM yang direkam menggunakan outerHTML
akan menggabungkan dua TextNode yang berdekatan menjadi satu node. Saat kami memantau perubahan DOM selanjutnya, kami akan menggunakan MutationObserver
. Saat ini, Anda memerlukan banyak pemrosesan agar kompatibel dengan penggabungan ini dari TextNodes. , jika tidak, Anda tidak akan dapat menemukan node target operasi selama operasi pemulihan.
Jadi, adakah cara agar kita dapat mempertahankan struktur asli halaman DOM?
Jawabannya adalah ya. Di sini kita menggunakan Virtual DOM untuk mencatat struktur DOM, mengubah documentElement menjadi Virtual DOM, mencatatnya, dan membuat ulang DOM saat memulihkan nanti.
Ubah DOM menjadi DOM Virtual Kita hanya perlu memperhatikan dua tipe Node di sini: Node.TEXT_NODE
dan Node.ELEMENT_NODE
. Pada saat yang sama, perlu dicatat bahwa pembuatan sub-elemen SVG dan SVG memerlukan penggunaan API: createElementNS. Oleh karena itu, ketika kita merekam Virtual DOM, kita perlu memperhatikan catatan namespace.
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];function createVirtualDom(elemen, isSVG = false) { switch (element.nodeType) { case Node.TEXT_NODE: kembali createVirtualText(elemen); case Node.ELEMENT_NODE: kembalikan createVirtualElement(elemen, isSVG || elemen.tagName.toLowerCase() === 'svg'); teks: element.nodeValue, ketik: 'VirtualText', }; if (typeof element.__flow !== 'tidak terdefinisi') { vText.__flow = elemen.__flow; } kembalikan vText;}fungsi createVirtualElement(elemen, isSVG = false) { const tagName = elemen.tagName.toLowerCase(); ); const { attr, namespace } = getNodeAttributes(elemen, isSVG); tagName, ketik: 'VirtualElement', turunan, atribut: attr, namespace, }; if (typeof element.__flow !== 'undefinisi') { vElement.__flow = element.__flow; isSVG = false) { const childNodes = elemen.childNodes ? [...element.childNodes] : []; const anak = []; childNodes.forEach((cnode) => { kids.push(createVirtualDom(cnode, isSVG)); }); return kids.filter(c => !!c);}fungsi getNodeAttributes(elemen, isSVG = false) { const atribut = elemen.atribut ? [...elemen.atribut] : []; const attr = {}; atribut.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { namespace = nodeValue; } else if (isSVG) { namespace = SVG_NAMESPACE; } }); return { attr, namespace };}
Melalui kode di atas, kita dapat mengubah seluruh documentElement menjadi Virtual DOM, di mana __flow digunakan untuk mencatat beberapa parameter, termasuk ID tag, dll. Catatan Node Virtual: jenis, atribut, anak, namespace.
DOM virtual dikembalikan ke DOMMemulihkan DOM Virtual ke DOM relatif mudah. Anda hanya perlu membuat DOM secara rekursif. NodeFilter digunakan untuk memfilter elemen skrip karena kita tidak memerlukan eksekusi skrip JS.
fungsi createElement(vdom, nodeFilter = () => true) { biarkan node; if (vdom.type === 'VirtualText') { node = document.createTextNode(vdom.text } else { node = typeof vdom.namespace === 'tidak terdefinisi' ? document.createElement(vdom.tagName) : document.createElementNS(vdom.namespace, vdom.tagName); vdom.attributes) { node.setAttribute(nama, vdom.attributes[nama]); } vdom.children.forEach((cnode) => { const childNode = createElement(cnode, nodeFilter); if (childNode && nodeFilter(childNode) ) { node.appendChild(childNode); } } } jika (vdom.__flow) { node.__flow = vdom.__flow; } simpul kembali;}Pemantauan perubahan struktur DOM
Di sini kami menggunakan API: MutationObserver. Yang lebih menyenangkan lagi adalah API ini kompatibel dengan semua browser, sehingga kami dapat menggunakannya dengan berani.
Menggunakan MutationObserver:
const options = { childList: true, // Apakah akan mengamati perubahan pada node anak subpohon: true, // Apakah akan mengamati perubahan pada semua node turunan atribut: true, // Apakah akan mengamati perubahan pada atribut atributOldValue: true, // Apakah akan mengamati perubahan pada atribut atributOldValue: true, // Apakah untuk mengamati perubahan atribut Nilai lama yang diubah characterData: true, // Apakah konten node atau teks node berubah characterDataOldValue: true, // Apakah konten node atau teks node mengubah nilai lama // atributFilter: ['class', ' src'] Properti yang tidak ada dalam larik ini akan diabaikan saat mengubah};const pengamat = new MutationObserver((mutationList) => { // mutasiList: larik mutasi});observer.observe(document.documentElement, options);
Cara penggunaannya sangat mudah, Anda hanya perlu menentukan node root dan beberapa opsi yang perlu dipantau. Kemudian ketika DOM berubah, akan ada mutasiList di fungsi callback, yang merupakan daftar perubahan DOM mutasinya kira-kira:
{ ketik: 'childList', // atau characterData, atribut target: <DOM>, // parameter lainnya}
Kami menggunakan array untuk menyimpan mutasi. Callback spesifiknya adalah:
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // DOM yang baru dimasukkan tidak memiliki tanda, jadi di sini harus kompatibel if (!node.__flow) node.__flow = { id: uuid() }; return node.__flow.id; mutasi; const record = { tipe, target: getFlowId(target), }; switch (tipe) { case 'characterData': record.value = target.nodeValue; atributValue = 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, saudara berikutnya: getFlowId(n. saudara berikutnya), saudara sebelumnya: getFlowId(n.previousSibling) }; break; } this.records.push(record); });}fungsi takeSnapshot(node, options = {}) { this.markNodes(node); const snapshot = { vdom: createVirtualDom(node), }; jika (options.doctype === true) { snapshot.doctype = document.doctype.name; snapshot.clientWidth = document.body.clientWidth; snapshot.clientHeight = document.body.clientHeight } mengembalikan snapshot;}
Anda hanya perlu mencatat di sini bahwa ketika Anda memproses DOM baru, Anda memerlukan snapshot tambahan. Di sini Anda masih menggunakan Virtual DOM untuk merekam. Saat Anda memutarnya nanti, Anda masih membuat DOM dan memasukkannya ke dalam elemen induk, jadi di sini Anda perlu merujuk ke DOM, yang merupakan simpul saudara.
Pemantauan elemen formulirMutationObserver di atas tidak dapat memantau perubahan nilai input dan elemen lainnya, sehingga kita perlu melakukan pemrosesan khusus pada nilai elemen formulir.
mendengarkan acara oninputDokumentasi MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
Objek acara: pilih, masukan, area teks
window.addEventListener('input', this.onFormInput, true);onFormInput = (event) => { const target = event.target if ( target && target.__flow && ['pilih', 'textarea', 'input' ].includes(target.tagName.toLowerCase()) ) { this.records.push({ ketik: 'input', target: target.__flow.id, nilai: target.nilai, }); }}
Gunakan capture untuk menangkap peristiwa di jendela. Hal ini juga dilakukan kemudian. Alasannya adalah karena kita mungkin dan sering mencegah penggelembungan selama tahap penggelembungan untuk mengimplementasikan beberapa fungsi, sehingga menggunakan penangkapan dapat mengurangi hilangnya peristiwa Itu tidak akan menggelembung dan harus ditangkap.
mendengarkan acara onchangeDokumentasi MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
Event input tidak dapat memenuhi monitoring tipe checkbox dan radio, sehingga perlu menggunakan event onchange untuk monitoring.
window.addEventListener('change', this.onFormChange, true);onFormChange = (event) => { const target = event.target if (target && target.__flow) { if ( target.tagName.toLowerCase() == = 'input' && ['kotak centang', 'radio'].includes(target.getAttribute('type')) ) { this.records.push({ ketik: 'dicentang', target: target.__flow.id, dicentang: target.diperiksa, });mendengarkan acara onfokus
Dokumentasi MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onfocus
window.addEventListener('fokus', this.onFormFocus, true);onFormFocus = (event) => { const target = event.target if (target && target.__flow) { this.records.push({ ketik: 'fokus ', sasaran: target.__flow.id, }); }}mendengarkan acara onblur
Dokumentasi 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({ ketik: 'blur ', sasaran: target.__flow.id, }); }}Pemantauan perubahan elemen media
Ini mengacu pada audio dan video. Mirip dengan elemen formulir di atas, Anda dapat memantau acara onplay, onpause, pembaruan waktu, perubahan volume, dan acara lainnya, lalu menyimpannya dalam rekaman.
Pemantauan perubahan kanvas kanvasTidak ada peristiwa yang terjadi ketika konten kanvas berubah, sehingga kita dapat:
Kumpulkan elemen kanvas dan perbarui konten real-time secara rutin. Retas beberapa API gambar untuk mengadakan acara.
Penelitian mengenai canvas monitoring belum terlalu mendalam dan diperlukan penelitian yang lebih mendalam.
bermainIdenya relatif sederhana, cukup dapatkan beberapa informasi dari backend:
Dengan menggunakan informasi ini, pertama-tama Anda dapat membuat DOM halaman, yang menyertakan tag skrip pemfilteran, lalu membuat iframe dan menambahkannya ke kontainer, yang menggunakan peta untuk menyimpan DOM
fungsi putar(pilihan = {}) { const { wadah, catatan = [], snapshot ={} } = opsi; const { vdom, doctype, clientHeight, clientWidth } = snapshot this.nodeCache = {}; catatan; this.container = wadah; this.snapshot = snapshot; this.iframe = document.createElement('iframe'); (node) => { // Cache DOM const flowId = node.__flow && node.__flow.id; if (flowId) { this.nodeCache[flowId] = node } // Filter skrip kembali !(node.nodeType == = Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'skrip'); this.iframe.style.width = `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; container.appendChild(iframe); const doc = iframe.contentDocument; doc.write(`<!doctype ${doctype}><html><head></head><body></body></html>`); doc.close(); doc.replaceChild(documentElement, doc.documentElement); ini.execRecords();}
function execRecords(preDuration = 0) { const record = this.records.shift(); let node; if (record) { setTimeout(() => { switch (record.type) {// 'childList', 'characterData' , // Penanganan 'atribut', 'input', 'dicentang', // 'fokus', 'blur', 'play''pause' dan kejadian lainnya} this.execRecords(record.duration); record.duration - praDurasi) }}
Durasi di atas dihilangkan dalam artikel di atas. Anda dapat mengoptimalkan kelancaran pemutaran sesuai dengan pengoptimalan Anda sendiri, dan melihat apakah beberapa rekaman disajikan sebagai satu bingkai atau apa adanya.
Di atas adalah keseluruhan isi artikel ini, saya harap dapat bermanfaat untuk pembelajaran semua orang. Saya juga berharap semua orang mendukung VeVb Wulin Network.