Прочитав комментарии, я внезапно понял, что не объяснил это заранее. Эту статью можно назвать исследовательской и обучающей. Это набор решений, которые, как мне кажется, осуществимы. Я прочитаю несколько подобных библиотек кода. был открыт с открытым исходным кодом, чтобы дополнить мои собственные знания. В нем отсутствуют некоторые детали, поэтому вы можете рассматривать его как учебный текст и использовать его с осторожностью в производственных средах.
Запись экрана для воспроизведения сценария ошибкиЕсли ваше приложение подключено к системе веб-APM, вы, возможно, знаете, что система apm может помочь вам обнаружить необнаруженные ошибки, возникающие на странице, предоставить стек ошибок и помочь вам обнаружить ОШИБКУ. Однако иногда, когда вы не знаете конкретную операцию пользователя, невозможно воспроизвести ошибку. В это время, если есть запись экрана операции, вы можете четко понять путь действия пользователя, воспроизводя таким образом. БАГ. И ремонт.
Идеи реализации Идея 1: используйте Canvas для создания снимков экранаЭта идея относительно проста: использовать холст для рисования веб-контента. Наиболее известные библиотеки: html2canvas. Простой принцип этой библиотеки:
Эта реализация относительно сложна, но мы можем использовать ее напрямую и получить скриншот нужной веб-страницы.
Чтобы сгенерированное видео было более плавным, нам нужно генерировать около 25 кадров в секунду, а это значит, что нам нужно 25 скриншотов. Блок-схема идеи следующая:
Однако у этой идеи есть самый фатальный недостаток: чтобы видео было плавным, нам нужно 25 картинок в одну секунду, а одна картинка весит 300 КБ. Когда нам нужно 30-секундное видео, общий размер картинок составляет 220М. Такие большие сетевые издержки. Очевидно, нет.
Идея 2. Записывайте повторение всех операций.Чтобы снизить нагрузку на сеть, мы меняем свое мышление. Мы записываем следующие пошаговые операции на основе начальной страницы. Когда нам нужно играть, мы применяем эти операции по порядку, чтобы увидеть изменения в файле. страница. Эта идея разделяет операции мыши и изменения DOM:
Изменения мыши:
Конечно, это объяснение относительно краткое. Запись с помощью мыши относительно проста. Мы не будем вдаваться в подробности. Мы в основном объясним идеи реализации DOM-мониторинга.
Первый полный снимок страницы Прежде всего, вы можете подумать, что для получения полного снимка страницы можно напрямую использовать outerHTML
константное содержимое = document.documentElement.outerHTML;
Это просто записывает весь DOM страницы. Вам нужно только сначала добавить идентификатор тега в DOM, затем получить внешний HTML, а затем удалить сценарий JS.
Однако здесь есть проблема. DOM, записанный с использованием outerHTML
, объединит два соседних TextNodes в один узел. Когда мы впоследствии будем отслеживать изменения DOM, мы будем использовать MutationObserver
. На данный момент вам потребуется много обработки, чтобы обеспечить совместимость с этим объединением. TextNodes , иначе вы не сможете найти целевой узел операции во время операции восстановления.
Итак, есть ли способ сохранить исходную структуру DOM страницы?
Ответ — да. Здесь мы используем Virtual DOM для записи структуры DOM, превращаем documentElement в Virtual DOM, записываем его и регенерируем DOM при последующем восстановлении.
Преобразование DOM в виртуальный DOM Здесь нам нужно заботиться только о двух типах узлов: 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'];function createVirtualDom(element, isSVG = false) { переключатель (element.nodeType) {case Node.TEXT_NODE: возврат createVirtualText(element); case Node.ELEMENT_NODE: return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg'); default: 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); const vElement = { tagName, type: 'VirtualElement', дочерние элементы, атрибуты: attr, namespace, }; if (typeof element.__flow !== 'undefined') { vElement.__flow = element.__flow; } return vElement;}function getNodeChildren(element, isSVG = false) { const childNodes = element.childNodes ? [...element.childNodes]: []; const Children = []; childNodes.forEach((cnode) => { Children.push(createVirtualDom(cnode, isSVG)); }); return Children.filter(c => !!c);}function getNodeAttributes(element, isSVG = false) { const атрибуты = element.attributes ? [...element.attributes]: []; const attr = {}; let namespace; атрибуты.forEach(({ nodeName, nodeValue }) => { attr[nodeName] = nodeValue; if (XML_NAMESPACES.includes(nodeName)) { namespace = nodeValue; } else if (isSVG) { namespace = SVG_NAMESPACE; } }); return { attr, пространство имен };}
С помощью приведенного выше кода мы можем преобразовать весь documentElement в Virtual DOM, в котором __flow используется для записи некоторых параметров, включая идентификатор тега и т. д. Записи виртуального узла: тип, атрибуты, дочерние элементы, пространство имен.
Виртуальный DOM восстановлен в DOMВосстановить виртуальный DOM в DOM относительно просто. Вам нужно только создать DOM рекурсивно. NodeFilter используется для фильтрации элементов сценария, поскольку нам не требуется выполнение JS-скриптов.
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
Здесь мы используем API: MutationObserver. Что еще более приятно, так это то, что этот API совместим со всеми браузерами, поэтому мы можем смело его использовать.
Использование MutationObserver:
const options = { childList: true, // Следует ли наблюдать за изменениями в поддереве дочерних узлов: true, // Следует ли наблюдать за изменениями во всех узлах-потомках. наблюдать за изменениями атрибутов Измененное старое значениеcharacterData: true, // Изменяется ли содержимое узла или текст узлаcharacterDataOldValue: true, // Изменяет ли содержимое узла или текст узла старое значение // AttributeFilter: ['class', ' источник'] Свойства, не входящие в этот массив, будут игнорироваться при изменении};const Observer = new MutationObserver((mutationList) => { //mutationList: массив мутаций});observer.observe(document.documentElement, options);
Его очень просто использовать. Вам нужно только указать корневой узел и некоторые параметры, которые необходимо отслеживать. Затем, когда DOM изменится, в функции обратного вызова появится список изменений, который представляет собой список изменений DOM. мутация примерно равна:
{ type: 'childList', // илиcharacterData, атрибуты target: <DOM>, // другие параметры}
Мы используем массив для хранения мутаций. Конкретный обратный вызов:
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // Недавно вставленный DOM не имеет метки, поэтому он должен быть совместим здесь if (!node.__flow) node.__flow = { id: uuid() }; return node.__flow.id; } }; мутация; const запись = {тип, цель: getFlowId (цель), }; переключатель (тип) {case 'characterData': запись.значение = target.nodeValue; случай 'атрибуты': запись.attributeName = имя_атрибута; атрибутValue = 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), previousSibling: 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 вам понадобится инкрементальный снимок. Здесь вы по-прежнему используете 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 = (event) => { const target = event.target; if ( target && target.__flow && ['select', 'textarea', 'input' ].includes(target.tagName.toLowerCase()) ) { this.records.push({ type: 'input', target: 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' && ['флажок', 'радио'].includes(target.getAttribute('type')) ) { this.records.push({ type: 'проверено', цель: 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 {Container, Records = [], Snapshot = {} } = options; const { vdom, doctype, clientHeight, clientWidth } = snapshot; 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() === 'script' }); `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`;Container.appendChild(iframe); 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();}
function execRecords(preDuration = 0) { const Record = this.records.shift(); let node if (record) { setTimeout(() => { switch (record.type) { // 'childList', 'characterData' , // Обработка «атрибутов», «ввода», «проверено», // «фокуса», «размытия», «воспроизведения», «паузы» и других событий} this.execRecords(record.duration }, запись.длительность - preDuration) }}
Вышеуказанная продолжительность опущена в статье выше. Вы можете оптимизировать плавность воспроизведения в соответствии с вашей собственной оптимизацией и посмотреть, представлены ли несколько записей как один кадр или как есть.
Выше приведено все содержание этой статьи. Я надеюсь, что она будет полезна для изучения всеми. Я также надеюсь, что все поддержат сеть VeVb Wulin.