Después de leer los comentarios, de repente me di cuenta de que no lo había explicado de antemano. Se puede decir que este artículo es un artículo de investigación y aprendizaje. Creo que leeré algunas bibliotecas de códigos similares. Ha sido de código abierto para complementar mi propio conocimiento. Faltan algunos detalles, por lo que puede tratarlo como un texto de aprendizaje y utilizarlo con precaución en entornos de producción.
Grabación de pantalla para reproducir el escenario de error.Si su aplicación está conectada al sistema web apm, entonces sabrá que el sistema apm puede ayudarlo a capturar errores no detectados que ocurren en la página, proporcionar una pila de errores y ayudarlo a localizar el ERROR. Sin embargo, a veces, cuando no conoce la operación específica del usuario, no hay forma de reproducir el error. En este momento, si hay una grabación de la pantalla de operación, puede comprender claramente la ruta de operación del usuario, reproduciendo así la operación. ERROR Y reparación.
Ideas de implementación Idea 1: use Canvas para tomar capturas de pantallaEsta idea es relativamente simple: utilizar lienzo para dibujar contenido web. Las bibliotecas más famosas son: html2canvas. El principio simple de esta biblioteca es:
Esta implementación es relativamente complicada, pero podemos usarla directamente, para que podamos obtener la captura de pantalla de la página web que queremos.
Para que el video generado sea más fluido, necesitamos generar aproximadamente 25 fotogramas por segundo, lo que significa que necesitamos 25 capturas de pantalla. El diagrama de flujo de la idea es el siguiente:
Sin embargo, esta idea tiene el defecto más fatal: para que el video sea fluido, necesitamos 25 imágenes en un segundo y una imagen tiene 300 KB. Cuando necesitamos un video de 30 segundos, el tamaño total de las imágenes es 220 M. Una sobrecarga de red tan grande Obviamente no.
Idea 2: registrar la recurrencia de todas las operacionesPara reducir la sobrecarga de la red, cambiamos nuestra forma de pensar. Registramos las siguientes operaciones paso a paso en función de la página inicial. Cuando necesitamos jugar, aplicamos estas operaciones en orden para que podamos ver los cambios en la página. página. Esta idea separa las operaciones del mouse y los cambios DOM:
Cambios del ratón:
Por supuesto, esta explicación es relativamente breve. La grabación del mouse es relativamente simple. No entraremos en detalles. Explicaremos principalmente las ideas de implementación del monitoreo DOM.
La primera instantánea completa de la página. En primer lugar, puede pensar que para lograr una instantánea completa de la página, puede usar outerHTML
directamente.
contenido constante = document.documentElement.outerHTML;
Esto simplemente registra todo el DOM de la página. Solo necesita agregar primero la identificación de la etiqueta al DOM, luego obtener el HTML externo y luego eliminar el script JS.
Sin embargo, hay un problema aquí. El DOM registrado usando outerHTML
fusionará dos TextNodes adyacentes en un nodo. Cuando monitoreemos los cambios de DOM posteriormente, usaremos MutationObserver
. En este momento, se necesita mucho procesamiento para ser compatible con esta fusión. de TextNodes. De lo contrario, no podrá localizar el nodo de destino de la operación durante la operación de restauración.
Entonces, ¿hay alguna manera de que podamos mantener la estructura original de la página DOM?
La respuesta es sí. Aquí usamos Virtual DOM para registrar la estructura DOM, convertir documentElement en Virtual DOM, registrarlo y regenerar el DOM cuando se restaure más tarde.
Convertir DOM a DOM virtual Aquí solo debemos preocuparnos por dos tipos de nodos: Node.TEXT_NODE
y Node.ELEMENT_NODE
. Al mismo tiempo, cabe señalar que la creación de subelementos SVG y SVG requiere el uso de API: createElementNS. Por lo tanto, cuando registramos DOM virtual, debemos prestar atención al registro del espacio de nombres.
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';const XML_NAMESPACES = ['xmlns', 'xmlns:svg', 'xmlns:xlink'];función createVirtualDom(elemento, isSVG = false) { cambiar (element.nodeType) { caso Node.TEXT_NODE: retorno createVirtualText(element); case Node.ELEMENT_NODE: return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg'); valor predeterminado: return null }}function createVirtualText(element) { const vText = { texto: elemento.nodeValue, escriba: 'VirtualText', }; si (tipo de elemento.__flujo !== 'indefinido') { vText.__flow = element.__flow; } return vText;}función createVirtualElement(elemento, isSVG = false) { const tagName = element.tagName.toLowerCase(); ); const { atributo, espacio de nombres } = getNodeAttributes(elemento, isSVG const vElement); tagName, tipo: 'VirtualElement', hijos, atributos: attr, namespace, }; if (typeof element.__flow !== 'indefinido') { vElement.__flow = element.__flow;} function getNodeChildren(element, isSVG = falso) { const childNodes = element.childNodes ? [...element.childNodes] : []; const niños = []; childNodes.forEach((cnode) => { niños.push(createVirtualDom(cnode, isSVG)); }); return niños.filter(c => !!c);}función getNodeAttributes(elemento, isSVG = false) { const atributos = elemento.atributos ? [...elemento.atributos] : []; const attr = {}; atributos.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { namespace = nodeValue; } else if (isSVG) { namespace = SVG_NAMESPACE; } }); return {atributo, espacio de nombres};}
A través del código anterior, podemos convertir todo el elemento del documento en DOM virtual, en el que __flow se usa para registrar algunos parámetros, incluido el ID de la etiqueta, etc. Registros del nodo virtual: tipo, atributos, elementos secundarios, espacio de nombres.
DOM virtual restaurado a DOMEs relativamente sencillo restaurar DOM virtual a DOM. Solo necesita crear el DOM de forma recursiva. El nodeFilter se usa para filtrar elementos de script porque no necesitamos la ejecución de scripts JS.
función createElement(vdom, nodeFilter = () => true) { let node; if (vdom.type === 'VirtualText') { nodo = document.createTextNode(vdom.text } else { nodo = tipo de vdom.namespace); === 'indefinido'? document.createElement(vdom.tagName): document.createElementNS(vdom.namespace, vdom.tagName para (deje el nombre en); vdom.attributes) { node.setAttribute(nombre, vdom.attributes[nombre]); vdom.children.forEach((cnode) => { const childNode = createElement(cnode, nodeFilter); if (childNode && nodeFilter(childNode) ) { nodo.appendChild(niñoNodo); vdom.__flow; } nodo de retorno;}Monitoreo de cambios de estructura DOM
Aquí usamos la API: MutationObserver. Lo que es aún más gratificante es que esta API es compatible con todos los navegadores, por lo que podemos usarla con valentía.
Usando MutationObserver:
opciones const = { childList: true, // Si se deben observar cambios en el subárbol de los nodos secundarios: true, // Si se deben observar cambios en todos los atributos de los nodos descendientes: true, // Si se deben observar cambios en los atributos atributoOldValue: true, // Si para observar cambios en los atributos El valor antiguo cambiado caracterData: true, // Si el contenido del nodo o el texto del nodo cambia caracterDataOldValue: true, // Si el contenido del nodo o el texto del nodo cambia el valor anterior // atributoFilter: ['class', ' fuente'] Las propiedades que no estén en esta matriz se ignorarán al cambiar};const observer = new MutationObserver((mutationList) => { // mutationList: matriz de mutación});observer.observe(document.documentElement, options);
Es muy simple de usar. Solo necesita especificar un nodo raíz y algunas opciones que deben monitorearse. Luego, cuando cambie el DOM, habrá una lista de mutaciones en la función de devolución de llamada, que es una lista de cambios de DOM. de la mutación es aproximadamente:
{ tipo: 'childList', // o CharacterData, atributos objetivo: <DOM>, // otros parámetros}
Usamos una matriz para almacenar mutaciones. La devolución de llamada específica es:
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // El DOM recién insertado no tiene ninguna marca, por lo que debe ser compatible aquí if (!node.__flow) node.__flow = { id: uuid() }; return node.__flow.id; } }; mutacionesList.forEach((mutación) => { const {destino, tipo, nombre de atributo } = mutación; const registro = { tipo, destino: getFlowId(destino), }; cambiar (tipo) { case 'characterData': record.value = target.nodeValue; break case 'atributos' = registro de atributo. atributoValue = target.getAttribute(attributeName); break; case 'childList': record.removedNodes = [...mutación.removedNodes].map(n => getFlowId(n)); record.addedNodes = [...mutation.addedNodes].map((n) => { const snapshot = this.takeSnapshot(n); return { ...snapshot, nextSibling: getFlowId(n. nextSibling), anteriorSibling: getFlowId(n.previousSibling) }; });}función takeSnapshot(nodo, opciones = {}) { this.markNodes(nodo); const instantánea = { vdom: createVirtualDom(nodo), } if (options.doctype === true) { snapshot.doctype = document.doctype.name; snapshot.clientWidth = document.body.clientWidth; snapshot.clientHeight = document.body.clientHeight } devolver instantánea;}
Solo debe tener en cuenta aquí que cuando procesa un nuevo DOM, necesita una instantánea incremental. Aquí todavía usa Virtual DOM para grabar. Cuando lo reproduzca más tarde, aún generará el DOM y lo insertará en el elemento principal. Debe consultar el DOM, que es el nodo hermano.
Monitoreo de elementos de formularioEl MutationObserver anterior no puede monitorear los cambios de valor de la entrada y otros elementos, por lo que debemos realizar un procesamiento especial en los valores de los elementos del formulario.
escucha de eventos en entradaDocumentación de MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
Objetos de evento: selección, entrada, área de texto
window.addEventListener('input', this.onFormInput, true);onFormInput = (evento) => { const target = event.target if ( target && target.__flow && ['select', 'textarea', 'input' ].includes(target.tagName.toLowerCase()) ) { this.records.push({ tipo: 'entrada', destino: objetivo.__flujo.id, valor: objetivo.valor, });
Use la captura para capturar eventos en la ventana. Esto también se hace más adelante. La razón de esto es que podemos y a menudo evitamos la propagación durante la etapa de propagación para implementar algunas funciones, por lo que el uso de la captura puede reducir la pérdida de eventos. No burbujeará y debe ser capturado.
escucha de eventos onchangeDocumentación de MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
El evento de entrada no puede satisfacer la supervisión de la casilla de verificación de tipo y la radio, por lo que es necesario utilizar el evento onchange para la supervisión.
window.addEventListener('change', this.onFormChange, true);onFormChange = (evento) => { const target = event.target if (target && target.__flow) { if ( target.tagName.toLowerCase() == = 'entrada' && ['casilla de verificación', 'radio'].includes(target.getAttribute('tipo')) ) { this.records.push({ tipo: 'marcado', objetivo: objetivo.__flow.id, marcado: objetivo.marcado, });escucha de eventos enfocados
Documentación de MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onfocus
window.addEventListener('focus', this.onFormFocus, true);onFormFocus = (evento) => { const target = event.target; if (target && target.__flow) { this.records.push({ tipo: 'focus ', objetivo: objetivo.__flujo.id, });escucha de eventos onblur
Documentación de MDN: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onblur
window.addEventListener('desenfoque', this.onFormBlur, true); onFormBlur = (evento) => { const target = event.target if (destino && target.__flow) { this.records.push({ tipo: 'desenfoque; ', objetivo: objetivo.__flujo.id, });Monitoreo de cambios de elementos multimedia
Esto se refiere a audio y video. De manera similar a los elementos de formulario anteriores, puede monitorear los eventos de reproducción, pausa, actualización de tiempo, cambio de volumen y otros eventos, y luego almacenarlos en registros.
Monitoreo de cambios de lienzoNo se genera ningún evento cuando cambia el contenido del lienzo, por lo que podemos:
Recopile elementos del lienzo y actualice periódicamente el contenido en tiempo real. Hackee algunas API de dibujo para generar eventos.
La investigación sobre el monitoreo de lienzos no es muy profunda y se necesita más investigación en profundidad.
jugarLa idea es relativamente simple, solo obtenga información del backend:
Usando esta información, primero puede generar el DOM de la página, que incluye etiquetas de script de filtrado, luego crear un iframe y agregarlo a un contenedor, que usa un mapa para almacenar el DOM.
función reproducir (opciones = {}) { const { contenedor, registros = [], instantánea ={} } = opciones; const {vdom, doctype, clientHeight, clientWidth } = this.nodeCache = {}; registros; this.container = contenedor; this.snapshot = instantánea; this.iframe = document.createElement('iframe'); (nodo) => { // Caché DOM const flowId = node.__flow && node.__flow.id; if (flowId) { this.nodeCache[flowId] = node } // Filtrar script return !(node.nodeType == = Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'script' }); `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; contenedor.appendChild(iframe); const doc = iframe.contentDocument; doc.write(`<!doctype ${doctype}><html><head></head><body></body></html>`); doc.close(); doc.replaceChild(documentElement, doc.documentElement);
function execRecords(preDuration = 0) { const record = this.records.shift(); let node if (record) { setTimeout(() => { switch (record.type) { // 'childList', 'characterData' , // Manejo de 'atributos', 'entrada', 'marcado', // 'enfoque', 'desenfoque', 'reproducir''pausa' y otros eventos} this.execRecords(record.duration }, registro.duración - preDuración) }}
La duración anterior se omite en el artículo anterior. Puede optimizar la fluidez de la reproducción de acuerdo con su propia optimización y ver si varios registros se presentan como un cuadro o como están.
Lo anterior es el contenido completo de este artículo. Espero que sea útil para el estudio de todos. También espero que todos apoyen VeVb Wulin Network.