слова, написанные впереди
Увидев комментарии, я вдруг понял, что не объяснил заранее.Эту статью можно назвать научно-исследовательской.Это набор решений, которые я считаю осуществимыми.Я прочитаю некоторые аналогичные кодовые базы, которые были открыты в будущем, чтобы дополнить себя.Есть некоторые недостающие детали, поэтому каждый может использовать его в качестве учебного текста и использовать его с осторожностью в производственной среде.
Запись экрана для воспроизведения сцены ошибки
Если ваше приложение подключено к системе веб-apm, то вы можете знать, что система apm может помочь вам отловить необнаруженные ошибки, возникающие на странице, предоставить стек ошибок и помочь найти ошибку. Однако иногда, когда вы не знаете конкретную операцию пользователя, невозможно воспроизвести эту ошибку.В это время, если есть экран записи операции, вы можете четко понять путь операции пользователя, чтобы воспроизвести ошибку и исправить.
Реализовать идеи
Идея 1: Используйте Canvas для создания снимков экрана
Эта идея относительно проста: использовать холст для рисования содержимого веб-страницы.Наиболее известные библиотеки:html2canvas, простой принцип этой библиотеки:
- Соберите весь DOM и сохраните его в очереди;
- Согласно zIndex, DOM проходит через определенные правила одно за другим по порядку, а DOM и его CSS-стиль отрисовываются на Canvas вместе.
Эта реализация более сложная, но мы можем использовать ее напрямую, чтобы получить скриншот нужной веб-страницы.
Для того, чтобы сгенерированное видео было более плавным, нам нужно сгенерировать около 25 кадров в секунду, то есть скриншотов 25. Блок-схема идеи выглядит следующим образом:
Однако у этой идеи есть самый фатальный недостаток: чтобы сделать видео плавным, нам нужно 25 картинок в секунду, а одна картинка весит 300 Кб, а когда нам нужно 30 секунд видео, размер картинки в сумме составляет 220 Мб, такие большие накладные расходы на сеть.Очевидно, что нет.
Идея 2: Запишите все операции для воспроизведения
Для того, чтобы уменьшить нагрузку на сеть, мы меняем образ мышления. Мы записываем следующие шаги на основе начальной страницы. Когда нам нужно «поиграть», мы применяем эти операции по порядку, чтобы мы могли видеть изменения. страницы. Эта идея отделяет операции мыши от изменений DOM:
Изменения мыши:
- Прослушайте событие mouseover и запишите clientX и clientY мыши.
- При воспроизведении используйте js, чтобы нарисовать поддельную мышь и изменить положение «мыши» в соответствии с записью координат.
DOM-изменения:
- Сделайте полный снимок DOM страницы. Включая сбор стилей, удаление JS-скриптов и маркировку каждого текущего DOM-элемента id через определенные правила.
- Слушайте все события, которые могут повлиять на интерфейс, такие как различные события мыши, события ввода, события прокрутки, события масштабирования и т. д. Каждое событие записывает параметры и целевые элементы, а целевым элементом может быть только что записанный идентификатор. записываться как инкрементный снимок.
- Отправить определенное количество снимков на серверную часть.
- Воспроизведение в фоновом режиме на основе снимков и цепочек действий.
Конечно, это описание относительно краткое, а запись мыши относительно простая, мы не будем говорить об этом, а в основном поясним идеи реализации DOM-мониторинга.
Первый полный снимок страницы
Во-первых, вы можете подумать, что для получения снимков полной страницы можно напрямую использоватьouterHTML
const content = document.documentElement.outerHTML;
Это просто записывает весь DOM страницы, вам нужно только сначала добавить идентификатор тега в DOM, затем получить externalHTML, а затем удалить скрипт JS.
Однако здесь возникает проблема, используяouterHTML
Записанный DOM объединит два соседних узла TextNodes в один узел, который мы будем использовать при отслеживании изменений DOM позже.MutationObserver
, в настоящее время требуется много обработки, чтобы быть совместимым с этой комбинацией TextNodes, иначе вы не сможете найти целевой узел операции при восстановлении операции.
Итак, есть ли способ сохранить исходную структуру страницы DOM?
Ответ положительный, здесь мы используем виртуальный DOM для записи структуры DOM, меняем documentElement на виртуальный 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) {
switch (element.nodeType) {
case Node.TEXT_NODE:
return createVirtualText(element);
case Node.ELEMENT_NODE:
return createVirtualElement(element, isSVG || element.tagName.toLowerCase() === 'svg');
default:
return null;
}
}
function createVirtualText(element) {
const vText = {
text: element.nodeValue,
type: 'VirtualText',
};
if (typeof element.__flow !== 'undefined') {
vText.__flow = element.__flow;
}
return vText;
}
function createVirtualElement(element, isSVG = false) {
const tagName = element.tagName.toLowerCase();
const children = getNodeChildren(element, isSVG);
const { attr, namespace } = getNodeAttributes(element, isSVG);
const vElement = {
tagName, type: 'VirtualElement', children, attributes: 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 attributes = element.attributes ? [...element.attributes] : [];
const attr = {};
let namespace;
attributes.forEach(({ nodeName, nodeValue }) => {
attr[nodeName] = nodeValue;
if (XML_NAMESPACES.includes(nodeName)) {
namespace = nodeValue;
} else if (isSVG) {
namespace = SVG_NAMESPACE;
}
});
return { attr, namespace };
}
С помощью приведенного выше кода мы можем преобразовать весь documentElement в виртуальный 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 === 'undefined'
? document.createElement(vdom.tagName)
: document.createElementNS(vdom.namespace, vdom.tagName);
for (let name in 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;
}
return node;
}
Мониторинг изменения структуры DOM
Здесь мы используем API: MutationObserver, и хорошая новость заключается в том, что этот API совместим со всеми браузерами, поэтому мы можем смело его использовать.
Используя MutationObserver:
const options = {
childList: true, // 是否观察子节点的变动
subtree: true, // 是否观察所有后代节点的变动
attributes: true, // 是否观察属性的变动
attributeOldValue: true, // 是否观察属性的变动的旧值
characterData: true, // 是否节点内容或节点文本的变动
characterDataOldValue: true, // 是否节点内容或节点文本的变动的旧值
// attributeFilter: ['class', 'src'] 不在此数组中的属性变化时将被忽略
};
const observer = new MutationObserver((mutationList) => {
// mutationList: array of mutation
});
observer.observe(document.documentElement, options);
Он очень прост в использовании, вам нужно только указать корневой узел и некоторые параметры для мониторинга, тогда при изменении DOM в функции обратного вызова будет мутацияList, которая представляет собой список изменений DOM. мутация примерно:
{
type: 'childList', // or characterData、attributes
target: <DOM>,
// other params
}
Мы используем массив для хранения мутаций, и конкретный обратный вызов:
const onMutationChange = (mutationsList) => {
const getFlowId = (node) => {
if (node) {
// 新插入的DOM没有标记,所以这里需要兼容
if (!node.__flow) node.__flow = { id: uuid() };
return node.__flow.id;
}
};
mutationsList.forEach((mutation) => {
const { target, type, attributeName } = mutation;
const record = {
type,
target: getFlowId(target),
};
switch (type) {
case 'characterData':
record.value = target.nodeValue;
break;
case 'attributes':
record.attributeName = attributeName;
record.attributeValue = 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,
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;
}
Нужно только отметить, что когда вы имеете дело с новым DOM, вам нужен инкрементный снимок, который все еще записывается в Virtual DOM, при последующем воспроизведении DOM все еще генерируется и вставляется в родительский элемент, поэтому здесь нужно для ссылки на DOM, который является родственным узлом.
мониторинг элементов формы
Вышеупомянутый MutationObserver не может отслеживать изменения значений таких элементов, как ввод, поэтому нам необходимо выполнить специальную обработку значений элементов формы.
прослушиватель событий oninput
Документация MDN:developer.Mozilla.org/en-US/docs/…
Объекты событий: select, input, textarea
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,
value: target.value,
});
}
}
Используйте захват в окне для захвата событий, которые также обрабатываются позже. Причина этого в том, что мы можем и часто предотвращаем всплытие на этапе всплытия для выполнения некоторых функций, поэтому использование захвата может уменьшить потерю событий. Кроме того, такие события, как события прокрутки не всплывают, необходимо использовать захват.
прослушиватель событий onchange
Документация MDN:developer.Mozilla.org/en-US/docs/…
Событие ввода не может обеспечить мониторинг флажка типа и радио, поэтому для мониторинга необходимо использовать событие onchange.
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: 'checked',
target: target.__flow.id,
checked: target.checked,
});
}
}
}
прослушиватель событий onfocus
Документация MDN:developer.Mozilla.org/en-US/docs/…
window.addEventListener('focus', this.onFormFocus, true);
onFormFocus = (event) => {
const target = event.target;
if (target && target.__flow) {
this.records.push({
type: 'focus',
target: target.__flow.id,
});
}
}
прослушиватель событий onblur
Документация MDN:developer.Mozilla.org/en-US/docs/…
window.addEventListener('blur', this.onFormBlur, true);
onFormBlur = (event) => {
const target = event.target;
if (target && target.__flow) {
this.records.push({
type: 'blur',
target: target.__flow.id,
});
}
}
Мониторинг изменений медиа-элементов
Это относится к аудио и видео, аналогично элементам формы выше, вы можете прослушивать события onplay, onpause, timeupdate, volumechange и другие события, а затем сохранять их в записях.
Слушатель изменения холста холста
Изменения содержимого холста не вызывают событий, поэтому мы можем:
- Собирайте элементы холста и регулярно обновляйте контент в реальном времени.
- Взломайте какой-нибудь API рисования, чтобы генерировать события
Исследование мониторинга холста не очень глубокое, и необходимы дальнейшие углубленные исследования.
играть
Идея относительно проста, просто получите некоторую информацию из серверной части:
- Полный снимок виртуального DOM
- Записи цепочки действий
- Разрешение экрана
- doctype
Используя эту информацию, вы можете сначала сгенерировать DOM страницы, которая включает в себя фильтрующие теги сценария, затем создать iframe, добавить его в контейнер и использовать карту для хранения DOM.
function play(options = {}) {
const { container, records = [], snapshot ={} } = options;
const { vdom, doctype, clientHeight, clientWidth } = snapshot;
this.nodeCache = {};
this.records = records;
this.container = container;
this.snapshot = 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;
}
// 过滤script
return !(node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'script');
});
this.iframe.style.width = `${clientWidth}px`;
this.iframe.style.height = `${clientHeight}px`;
container.appendChild(iframe);
const doc = iframe.contentDocument;
this.iframeDocument = doc;
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'、
// 'attributes'、'input'、'checked'、
// 'focus'、'blur'、'play''pause'等事件的处理
}
this.execRecords(record.duration);
}, record.duration - preDuration)
}
}
Указанная выше продолжительность опущена в приведенном выше.Вы можете настроить плавность воспроизведения в соответствии с собственной оптимизацией, в зависимости от того, представлены ли несколько записей как один кадр или изначально.