Après avoir lu les commentaires, j'ai soudain réalisé que je ne l'avais pas expliqué à l'avance.Cet article peut être considéré comme un article de recherche et d'apprentissage.C'est un ensemble de solutions que je considère réalisables.Je vais lire des bibliothèques de codes similaires. ont été open source pour compléter mes propres connaissances. Il manque quelques détails, vous pouvez donc le traiter comme un texte d'apprentissage et l'utiliser avec prudence dans les environnements de production.
Enregistrement d'écran pour reproduire le scénario d'erreurSi votre application est connectée au système Web apm, vous savez peut-être que le système apm peut vous aider à capturer les erreurs non détectées qui se produisent sur la page, à fournir une pile d'erreurs et à localiser le BUG. Cependant, parfois, lorsque vous ne connaissez pas l'opération spécifique de l'utilisateur, il n'y a aucun moyen de reproduire le bug. À ce stade, s'il y a un enregistrement d'écran d'opération, vous pouvez clairement comprendre le chemin d'opération de l'utilisateur, reproduisant ainsi le. BUG. Et réparation.
Idées de mise en œuvre Idée 1 : utiliser Canvas pour prendre des captures d'écranCette idée est relativement simple, elle consiste à utiliser Canvas pour dessiner du contenu Web. Les bibliothèques les plus connues sont : html2canvas. Le principe simple de cette bibliothèque est :
Cette implémentation est relativement compliquée, mais nous pouvons l'utiliser directement, afin d'obtenir la capture d'écran de la page Web souhaitée.
Afin de rendre la vidéo générée plus fluide, nous devons générer environ 25 images par seconde, ce qui signifie que nous avons besoin de 25 captures d'écran. L'organigramme de l'idée est le suivant :
Cependant, cette idée a le défaut le plus fatal : pour rendre la vidéo fluide, nous avons besoin de 25 images en une seconde, et une image fait 300 Ko. Lorsque nous avons besoin d'une vidéo de 30 secondes, la taille totale des images est de 220 Mo. Une surcharge réseau aussi importante Évidemment non.
Idée 2 : Enregistrer la récurrence de toutes les opérationsAfin de réduire la surcharge du réseau, nous modifions notre façon de penser. Nous enregistrons les opérations suivantes étape par étape en fonction de la page initiale. Lorsque nous devons jouer, nous appliquons ces opérations dans l'ordre, afin que nous puissions voir les changements dans la page initiale. page. Cette idée sépare les opérations de la souris et les modifications du DOM :
Modifications de la souris :
Bien entendu, cette explication est relativement brève. L'enregistrement par la souris est relativement simple. Nous n'entrerons pas dans les détails. Nous expliquerons principalement les idées de mise en œuvre de la surveillance DOM.
Le premier instantané complet de la page Tout d’abord, vous pensez peut-être que pour obtenir un instantané complet de la page, vous pouvez directement utiliser outerHTML
const content = document.documentElement.outerHTML;
Cela enregistre simplement tout le DOM de la page. Il vous suffit d'ajouter d'abord l'identifiant de la balise au DOM, puis d'obtenir le HTML externe, puis de supprimer le script JS.
Cependant, il y a un problème ici. Le DOM enregistré à l'aide outerHTML
fusionnera deux TextNodes adjacents en un seul nœud. Lorsque nous surveillerons ensuite les modifications du DOM, nous utiliserons MutationObserver
. À ce stade, vous aurez besoin de beaucoup de traitement pour être compatible avec cette fusion. de TextNodes , sinon vous ne pourrez pas localiser le nœud cible de l'opération pendant l'opération de restauration.
Alors, existe-t-il un moyen de conserver la structure originale de la page DOM ?
La réponse est oui. Ici, nous utilisons Virtual DOM pour enregistrer la structure du DOM, transformer documentElement en Virtual DOM, l'enregistrer et régénérer le DOM lors d'une restauration ultérieure.
Convertir le DOM en DOM virtuel Nous n'avons besoin de nous soucier ici que de deux types de nœuds : Node.TEXT_NODE
et Node.ELEMENT_NODE
. Dans le même temps, il convient de noter que la création de SVG et de sous-éléments SVG nécessite l'utilisation de l'API : createElementNS. Par conséquent, lorsque nous enregistrons le DOM virtuel, nous devons faire attention au code ci-dessus.
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 : retour createVirtualText(element); case Node.ELEMENT_NODE : return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg'); default : return null }}function createVirtualText(element) { const vText = { texte : element.nodeValue, tapez : 'VirtualText', } if (type d'élément.__flow !== 'indéfini') { vText.__flow = element.__flow; } return vText;}function createVirtualElement(element, isSVG = false) { const tagName = element.tagName.toLowerCase(); ); const { attr, espace de noms } = getNodeAttributes(element, isSVG); const vElement = { tagName, tapez : 'VirtualElement', enfants, attributs : attr, espace de noms, }; if (typeof element.__flow !== 'undefined') { 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 attributs = element.attributes ? [...element.attributes] : []; const attr = {}; attributs.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { namespace = nodeValue; } else if (isSVG) { namespace = SVG_NAMESPACE; } }); return { attr, espace de noms };}
Grâce au code ci-dessus, nous pouvons convertir l'intégralité du documentElement en DOM virtuel, dans lequel __flow est utilisé pour enregistrer certains paramètres, y compris l'ID de balise, etc. Enregistrements de nœud virtuel : type, attributs, enfants, espace de noms.
DOM virtuel restauré en DOMIl est relativement simple de restaurer le DOM virtuel vers le DOM. Il vous suffit de créer le DOM de manière récursive. Le nodeFilter est utilisé pour filtrer les éléments de script car nous n'avons pas besoin de l'exécution de scripts JS.
function createElement(vdom, nodeFilter = () => true) { let node; if (vdom.type === 'VirtualText') { node = document.createTextNode(vdom.text } else { node = typeof vdom.namespace); === 'indéfini' ? document.createElement(vdom.tagName) : document.createElementNS(vdom.namespace, vdom.tagName for (laisser le nom); vdom.attributes) { node.setAttribute(nom, vdom.attributes[nom]); } vdom.children.forEach((cnode) => { const childNode = createElement(cnode, nodeFilter); if (childNode && nodeFilter(childNode) ) { node.appendChild(childNode); } }); if (vdom.__flow) { node.__flow = vdom.__flow; } nœud de retour ;}Surveillance des changements de structure du DOM
Ici, nous utilisons l'API : MutationObserver. Ce qui est encore plus gratifiant, c'est que cette API est compatible avec tous les navigateurs, nous pouvons donc l'utiliser avec audace.
Utilisation de MutationObserver :
const options = { childList : true, // S'il faut observer les changements dans le sous-arbre des nœuds enfants : true, // S'il faut observer les changements dans tous les attributs des nœuds descendants : true, // S'il faut observer les changements dans les attributsattributOldValue : true, // S'il faut pour observer les changements dans les attributs. L'ancienne valeur modifiée. src'] Les propriétés qui ne figurent pas dans ce tableau seront ignorées lors de la modification};const observer = new MutationObserver((mutationList) => { // mutationList: array of mutation});observer.observe(document.documentElement, options);
C'est très simple à utiliser. Il vous suffit de spécifier un nœud racine et certaines options qui doivent être surveillées. Ensuite, lorsque le DOM change, il y aura une mutationList dans la fonction de rappel, qui est une liste des modifications du DOM. de la mutation est à peu près :
{ type : 'childList', // ou CharacterData, attributs cible : <DOM>, // autres paramètres}
Nous utilisons un tableau pour stocker les mutations. Le rappel spécifique est :
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // Le DOM nouvellement inséré n'a pas de marque, il doit donc être compatible ici if (!node.__flow) node.__flow = { id: uuid() }; return node.__flow.id; mutationsList.forEach((mutation) => { const { cible, type, nom d'attribut } = mutation; const record = { type, target: getFlowId(target), }; switch (type) { case 'characterData': record.value = target.nodeValue; break 'attributs': record.attributeName =attributName record; attributValue = 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 { ...instantané, 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 ;}
Vous devez seulement noter ici que lorsque vous traitez un nouveau DOM, vous avez besoin d'un instantané incrémentiel. Ici, vous utilisez toujours le DOM virtuel pour enregistrer. Lorsque vous le lisez plus tard, vous générez toujours le DOM et l'insérez dans l'élément parent, donc ici. Vous devez vous référer au DOM, qui est le nœud frère.
Surveillance des éléments de formulaireLe MutationObserver ci-dessus ne peut pas surveiller les changements de valeur des éléments d'entrée et d'autres éléments, nous devons donc effectuer un traitement spécial sur les valeurs des éléments du formulaire.
écoute d'événement oninputDocumentation MDN : https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
Objets événement : sélection, saisie, zone de texte
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', cible : target.__flow.id, valeur : target.value, } );
Utilisez la capture pour capturer les événements sur la fenêtre. Cela se fait également plus tard. La raison en est que nous pouvons et souvent empêcher le bouillonnement pendant la phase de bouillonnement pour implémenter certaines fonctions, donc l'utilisation de la capture peut réduire la perte d'événements, comme les événements de défilement. Il ne fera pas de bulles et doit être capturé.
écoute d'événement onchangeDocumentation MDN : https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
L'événement d'entrée ne peut pas satisfaire la surveillance de type case à cocher et radio, il est donc nécessaire d'utiliser l'événement onchange pour la surveillance.
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 : 'vérifié', cible : target.__flow.id, vérifié : target.checked, } } }}écoute d'événement onfocus
Documentation 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 ', cible : cible.__flow.id, }); }}écoute d'événement onblur
Documentation 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 ', cible : cible.__flow.id, }); }}Surveillance des modifications des éléments multimédias
Cela fait référence à l'audio et à la vidéo. Semblable aux éléments de formulaire ci-dessus, vous pouvez surveiller les événements en lecture, en pause, la mise à jour de l'heure, le changement de volume et d'autres événements, puis les stocker dans des enregistrements.
Surveillance des modifications du canevasAucun événement n'est généré lorsque le contenu du canevas change, nous pouvons donc :
Collectez des éléments de canevas et mettez régulièrement à jour le contenu en temps réel. Piratez certaines API de dessin pour lancer des événements.
Les recherches sur la surveillance des canevas ne sont pas très approfondies et des recherches plus approfondies sont nécessaires.
jouerL’idée est relativement simple, il suffit de récupérer quelques informations depuis le backend :
À l'aide de ces informations, vous pouvez d'abord générer le DOM de la page, qui inclut des balises de script de filtrage, puis créer une iframe et l'ajouter à un conteneur, qui utilise une carte pour stocker le DOM.
function play(options = {}) { const { conteneur, records = [], snapshot ={} } = options ; const { vdom, doctype, clientHeight, clientWidth } = snapshot this.nodeCache = {} ; enregistrements ; this.container = conteneur ; this.snapshot = snapshot ; this.iframe = document.createElement('iframe'); (node) => { // Cache DOM const flowId = node.__flow && node.__flow.id; if (flowId) { this.nodeCache[flowId] = node; } // Retour du script de filtre !(node.nodeType == = Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'script' }); `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; conteneur.appendChild(iframe); const doc = iframe.contentDocument = 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' , // Gestion des 'attributs', 'input', 'checked', // 'focus', 'flou', 'play''pause' et autres événements} this.execRecords(record.duration }, record.duration - préDuration) }}
La durée ci-dessus est omise dans l'article ci-dessus. Vous pouvez optimiser la fluidité de la lecture en fonction de votre propre optimisation et voir si plusieurs enregistrements sont présentés comme une seule image ou tels quels.
Ce qui précède représente l’intégralité du contenu de cet article. J’espère qu’il sera utile à l’étude de chacun. J’espère également que tout le monde soutiendra le réseau VeVb Wulin.