コメントを読んだ後、私は事前に説明していなかったことに気づきました。この記事は、実行可能であると思われる解決策のセットです。私自身の知識を補うためにオープンソースになっています。詳細が不足しているため、学習用テキストとして扱い、運用環境では注意して使用してください。
エラーシナリオを再現するための画面録画アプリケーションが Web APM システムに接続されている場合は、APM システムがページ上で発生する捕捉されなかったエラーをキャプチャし、エラー スタックを提供し、バグを特定するのに役立つことをご存じかもしれません。しかし、ユーザーの具体的な操作が分からない場合、バグを再現することができない場合があります。このとき、操作画面が記録されていれば、ユーザーの操作経路が明確に把握でき、バグを再現することができます。バグと修理。
実装のアイデアアイデア 1: Canvas を使用してスクリーンショットを撮るこのアイデアは比較的単純で、canvas を使用して Web コンテンツを描画するというものです。より有名なライブラリは html2canvas です。このライブラリの簡単な原理は次のとおりです。
この実装は比較的複雑ですが、直接使用できるため、必要な Web ページのスクリーンショットを取得できます。
生成されたビデオをより滑らかにするには、1 秒あたり約 25 フレームを生成する必要があります。つまり、25 枚のスクリーンショットが必要になります。このアイデアのフローチャートは次のとおりです。
ただし、このアイデアには最も致命的な欠陥があります。ビデオを滑らかにするには、1 秒間に 25 枚の写真が必要で、30 秒のビデオが必要な場合、1 枚の写真の合計サイズは 220M になります。このような大きなネットワーク オーバーヘッドは明らかにそうではありません。
アイデア 2: すべての操作の繰り返しを記録するネットワークのオーバーヘッドを軽減するために、最初のページに基づいて次のステップごとの操作を記録し、それらの操作を順番に適用して、ページの変化を確認できるようにします。ページ。この考え方では、マウス操作と DOM の変更を分離します。
マウスの変更:
もちろん、この説明は比較的簡単です。ここでは主に DOM モニタリングの実装アイデアについて説明します。
ページの最初の完全なスナップショットまず、ページの完全なスナップショットを取得するには、 outerHTML
直接使用できると思われるかもしれません。
const content = document.documentElement.outerHTML;
これにより、ページのすべての DOM が記録されるだけです。最初にタグ ID を DOM に追加し、次に、outerHTML を取得して、JS スクリプトを削除するだけです。
ただし、ここで問題が発生します。outerHTML outerHTML
使用して記録された DOM は、2 つの隣接する TextNode を 1 つのノードにマージします。このとき、このマージに対応するために、 MutationObserver
使用することになります。そうしないと、復元操作中に操作のターゲット ノードを見つけることができなくなります。
では、ページ DOM の元の構造を維持する方法はあるのでしょうか?
答えは「はい」です。ここでは、Virtual DOM を使用して DOM 構造を記録し、documentElement を Virtual DOM に変換して記録し、後で復元するときに DOM を再生成します。
DOM を仮想 DOM に変換するここでは、 Node.TEXT_NODE
とNode.ELEMENT_NODE
という 2 つのノード タイプについてのみ注意する必要があります。同時に、SVG および SVG サブ要素の作成には API: createElementNS を使用する必要があることに注意してください。そのため、仮想 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'); デフォルト: return null }}function createVirtualText(element) { const vText = {テキスト: element.nodeValue、タイプ: 'VirtualText', } if (要素のタイプ.__flow; !== '未定義') { vText.__flow = element.__flow; } return vText;}function createVirtualElement(element, isSVG = false) { const tagName = element.tagName.toLowerCase(); ); const { 属性、名前空間 } = getNodeAttributes(要素、isSVG); tagName、タイプ: 'VirtualElement'、子、属性: attr、名前空間、}; if (typeof element.__flow !== 'unknown') { 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属性 = element.attributes ? [...element.attributes] : [] const attr = {};属性.forEach(({ ノード名, ノード値 }) => { attr[ノード名] = ノード値; if (XML_NAMESPACES.includes(nodeName)) { 名前空間 = ノード値; } else if (isSVG) { 名前空間 = SVG_NAMESPACE; } }); return { 属性、名前空間 };}
上記のコードを通じて、documentElement 全体を仮想 DOM に変換できます。この仮想 DOM では、__flow を使用して、タグ ID などのいくつかのパラメーターが記録されます。仮想ノードは、タイプ、属性、子、名前空間を記録します。
仮想 DOM が DOM に復元されました仮想 DOM を DOM に復元するのは比較的簡単です。JS スクリプトを実行する必要がないため、DOM を再帰的に作成するだけで済みます。
function createElement(vdom, nodeFilter = () => true) { let ノード; if (vdom.type === 'VirtualText') { ノード = document.createTextNode(vdom.text) } else { ノード = typeof vdom.namespace; === '未定義' ? document.createElement(vdom.tagName) : document.createElementNS(vdom.namespace, vdom.tagName); vdom.attributes) {node.setAttribute(name, vdom.attributes[name]); } vdom.children.forEach((cnode) => { const childNode = createElement(cnode, nodeFilter); if (childNode && nodeFilter(childNode) ) { ノード.appendChild(childNode) } }); vdom.__flow; } ノードを返します;}DOM構造変更監視
ここでは、MutationObserver という API を使用します。さらにうれしいのは、この API がすべてのブラウザと互換性があるため、大胆に使用できることです。
MutationObserver の使用:
const options = { childList: true, // 子ノードの変更を監視するかどうか subtree: true, // すべての子孫ノードの変更を監視するかどうか attribute: true, // 属性の変更を監視するかどうかattributeOldValue: true, // かどうか属性の変更を観察する 変更された古い値 CharacterData: true, // ノードのコンテンツまたはノード テキストが変更されるかどうか CharacterDataOldValue: true, // ノードのコンテンツまたはノード テキストが古い値を変更するかどうか //attributeFilter: ['class', 'ソース']この配列にないプロパティは変更時に無視されます。};const observer = new MutationObserver((mutationList) => { // mutationList: 変異の配列});observer.observe(document.documentElement, options);
使い方は非常に簡単で、ルート ノードと監視する必要があるいくつかのオプションを指定するだけで、DOM が変更されると、DOM の変更のリストである mutationList が作成されます。突然変異はおおよそ次のとおりです。
{ type: 'childList', // またはcharacterData、属性ターゲット: <DOM>, // その他のパラメータ}
配列を使用して突然変異を保存します。具体的なコールバックは次のとおりです。
const onMutationChange = (mutationsList) => { const getFlowId = (node) => { if (node) { // 新しく挿入された DOM にはマークがないため、ここで互換性がある必要があります if (!node.__flow) node.__flow = { id: uuid() }; 戻り値 node.__flow.id } };突然変異; const レコード = { タイプ, ターゲット: getFlowId(target), }; スイッチ (タイプ) { ケース '文字データ': レコード値 = ターゲット.ノード値; ケース '属性名 = レコード。属性値 = target.getAttribute(属性名); ケース 'childList': Record.removedNodes = [...mutation.removedNodes].map(n => getFlowId(n)); Record.addedNodes = [...mutation.addedNodes].map((n) => { const snapshot = this.takeSnapshot(n); return { ...スナップショット、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; スナップショット.clientWidth = document.body.clientWidth = document.body.clientHeight; }
ここで注意する必要があるのは、新しい DOM を処理するときに増分スナップショットが必要になることだけです。後で再生するときにも引き続き仮想 DOM を使用して、それを親要素に挿入します。兄弟ノードである DOM を参照する必要があります。
フォーム要素の監視上記のMutationObserverではinputやその他の要素の値の変化を監視することができないため、form要素の値に対して特別な処理を行う必要があります。
oninput イベントのリスニングMDN ドキュメント: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/oninput
イベントオブジェクト: 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、値: 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 などのイベントを監視し、レコードに保存できます。
Canvasキャンバス変更監視キャンバスのコンテンツが変更されてもイベントはスローされないため、次のことが可能です。
キャンバス要素を収集し、リアルタイム コンテンツを定期的に更新して、イベントをスローする描画 API をハックします。
キャンバスのモニタリングに関する研究はそれほど詳細ではないため、さらに詳細な研究が必要です。
遊ぶアイデアは比較的単純で、バックエンドから情報を取得するだけです。
この情報を使用すると、まずフィルタリング スクリプト タグを含むページ DOM を生成し、次に iframe を作成して、マップを使用して DOM を保存するコンテナに追加できます。
function play(options = {}) { const { コンテナ、レコード = []、スナップショット ={} } = オプション; const { vdom、doctype、clientHeight、clientWidth } = this.nodeCache = {};レコード; this.container = コンテナ; this.snapshot = スナップショット; const documentElement(vdom, (node) => { // キャッシュ DOM const flowId = node.__flow && node.__flow.id; if (flowId) { this.nodeCache[flowId] = node } // フィルター スクリプト return !(node.nodeType == = Node.ELEMENT_NODE &&node.tagName.toLowerCase() === 'script') }); `${clientWidth}px`; this.iframe.style.height = `${clientHeight}px`; const doc = iframe.contentDocument = doc.open(); 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 ノード; if (record) { setTimeout(() => { switch (record.type) { // 'childList', 'characterData' , // 'attributes'、'input'、'checked'、// 'focus'、'blur'、'play''pause' およびその他のイベントの処理} this.execRecords(record.duration) };レコード.duration - preDuration) }}
上記の記事では上記の継続時間は省略されていますが、独自の最適化に従って再生の滑らかさを最適化し、複数のレコードが 1 つのフレームとして表示されるか、そのまま表示されるかを確認できます。
以上がこの記事の全内容です。皆様の学習のお役に立てれば幸いです。また、VeVb Wulin Network をご支援いただければ幸いです。