From cca8c25e6065dd0b9dbb97e05e44449af2fea904 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 27 Nov 2020 14:16:42 +0800 Subject: [PATCH 1/3] fix: elements would lose some state like scroll position because of "virtual parent" optimization --- src/replay/index.ts | 51 +++++++++++++++++++++++++++++++++++++++ typings/replay/index.d.ts | 2 ++ 2 files changed, 53 insertions(+) diff --git a/src/replay/index.ts b/src/replay/index.ts index e2163fea99..c90b457ce8 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -45,6 +45,7 @@ const SKIP_TIME_INTERVAL = 5 * 1000; const mitt = (mittProxy as any).default || mittProxy; const REPLAY_CONSOLE_PREFIX = '[replayer]'; +const SCROLL_ATTRIBUTE_NAME = '__rrweb_scroll__'; const defaultMouseTailConfig = { duration: 500, @@ -130,6 +131,8 @@ export class Replayer { ((parent as unknown) as HTMLTextAreaElement).value = frag.textContent; } parent.appendChild(frag); + // restore state of elements after they are mounted + this.restoreState(parent); } this.fragmentParentMap.clear(); @@ -964,6 +967,10 @@ export class Replayer { const virtualParent = (document.createDocumentFragment() as unknown) as INode; mirror.map[mutation.parentId] = virtualParent; this.fragmentParentMap.set(virtualParent, parent); + + // store the state, like scroll position, of child nodes before they are unmounted from dom + this.storeState(parent); + while (parent.firstChild) { virtualParent.appendChild(parent.firstChild); } @@ -1248,6 +1255,50 @@ export class Replayer { }); } + /** + * store state of elements before unmounted from dom recursively + * the state should be restored in the handler of event ReplayerEvents.Flush + * e.g. browser would lose scroll position after the process that we add children of parent node to Fragment Document as virtual dom + */ + private storeState(parent: INode) { + if (parent) { + if (parent.nodeType === parent.ELEMENT_NODE) { + const parentElement = (parent as unknown) as HTMLElement; + if (parentElement.scrollLeft || parentElement.scrollTop) { + // store scroll position on the node itself + parentElement.setAttribute( + SCROLL_ATTRIBUTE_NAME, + `${parentElement.scrollLeft},${parentElement.scrollTop}`, + ); + } + const children = parentElement.children; + for (let i = 0; i < children.length; i++) + this.storeState((children[i] as unknown) as INode); + } + } + } + + /** + * restore the state of elements recursively, which was stored before elements were unmounted from dom in virtual parent mode + * this function corresponds to function storeState + */ + private restoreState(parent: INode) { + if (parent.nodeType === parent.ELEMENT_NODE) { + const parentElement = (parent as unknown) as HTMLElement; + const scrollData = parentElement.getAttribute(SCROLL_ATTRIBUTE_NAME); // e.g. "scrollLeft,scrollTop" + if (scrollData) { + const scrollDataArray = scrollData.split(','); + parentElement.scrollLeft = Number(scrollDataArray[0]); + parentElement.scrollTop = Number(scrollDataArray[1]); + parentElement.removeAttribute(SCROLL_ATTRIBUTE_NAME); + } + const children = parentElement.children; + for (let i = 0; i < children.length; i++) { + this.restoreState((children[i] as unknown) as INode); + } + } + } + private warnNodeNotFound(d: incrementalData, id: number) { this.warn(`Node with id '${id}' not found in`, d); } diff --git a/typings/replay/index.d.ts b/typings/replay/index.d.ts index 9ce4f55a65..42f3c032f1 100644 --- a/typings/replay/index.d.ts +++ b/typings/replay/index.d.ts @@ -47,6 +47,8 @@ export declare class Replayer { private hoverElements; private isUserInteraction; private backToNormal; + private storeState; + private restoreState; private warnNodeNotFound; private warnCanvasMutationFailed; private debugNodeNotFound; From 516da12d3cd056360777ed324d00eca0234d06a4 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 27 Nov 2020 17:13:58 +0800 Subject: [PATCH 2/3] refactor: the bugfix code bug: elements would lose some state like scroll position because of "virtual parent" optimization --- src/replay/index.ts | 27 ++++++++++++++++----------- src/types.ts | 6 ++++++ typings/replay/index.d.ts | 1 + typings/types.d.ts | 3 +++ 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index c90b457ce8..672b493938 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -26,6 +26,7 @@ import { scrollData, inputData, canvasMutationData, + ElementState, } from '../types'; import { mirror, @@ -79,6 +80,7 @@ export class Replayer { private treeIndex!: TreeIndex; private fragmentParentMap!: Map; + private elementStateMap!: Map; private imageMap: Map = new Map(); @@ -114,6 +116,7 @@ export class Replayer { this.treeIndex = new TreeIndex(); this.fragmentParentMap = new Map(); + this.elementStateMap = new Map(); this.emitter.on(ReplayerEvents.Flush, () => { const { scrollMap, inputMap } = this.treeIndex.flush(); @@ -135,6 +138,7 @@ export class Replayer { this.restoreState(parent); } this.fragmentParentMap.clear(); + this.elementStateMap.clear(); for (const d of scrollMap.values()) { this.applyScroll(d); @@ -1265,11 +1269,10 @@ export class Replayer { if (parent.nodeType === parent.ELEMENT_NODE) { const parentElement = (parent as unknown) as HTMLElement; if (parentElement.scrollLeft || parentElement.scrollTop) { - // store scroll position on the node itself - parentElement.setAttribute( - SCROLL_ATTRIBUTE_NAME, - `${parentElement.scrollLeft},${parentElement.scrollTop}`, - ); + // store scroll position state + this.elementStateMap.set(parent, { + scroll: [parentElement.scrollLeft, parentElement.scrollTop], + }); } const children = parentElement.children; for (let i = 0; i < children.length; i++) @@ -1285,12 +1288,14 @@ export class Replayer { private restoreState(parent: INode) { if (parent.nodeType === parent.ELEMENT_NODE) { const parentElement = (parent as unknown) as HTMLElement; - const scrollData = parentElement.getAttribute(SCROLL_ATTRIBUTE_NAME); // e.g. "scrollLeft,scrollTop" - if (scrollData) { - const scrollDataArray = scrollData.split(','); - parentElement.scrollLeft = Number(scrollDataArray[0]); - parentElement.scrollTop = Number(scrollDataArray[1]); - parentElement.removeAttribute(SCROLL_ATTRIBUTE_NAME); + if (this.elementStateMap.has(parent)) { + const storedState = this.elementStateMap.get(parent)!; + // restore scroll position + if (storedState.scroll) { + parentElement.scrollLeft = storedState.scroll[0]; + parentElement.scrollTop = storedState.scroll[1]; + } + this.elementStateMap.delete(parent); } const children = parentElement.children; for (let i = 0; i < children.length; i++) { diff --git a/src/types.ts b/src/types.ts index 3f503009d9..daefcb06fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -466,3 +466,9 @@ export enum ReplayerEvents { } export type MaskInputFn = (text: string) => string; + +// store the state that would be changed during the process(unmount from dom and mount again) +export type ElementState = { + // [scrollLeft,scrollTop] + scroll?: [number, number]; +}; diff --git a/typings/replay/index.d.ts b/typings/replay/index.d.ts index 42f3c032f1..fa7093c0b6 100644 --- a/typings/replay/index.d.ts +++ b/typings/replay/index.d.ts @@ -17,6 +17,7 @@ export declare class Replayer { private legacy_missingNodeRetryMap; private treeIndex; private fragmentParentMap; + private elementStateMap; private imageMap; constructor(events: Array, config?: Partial); on(event: string, handler: Handler): this; diff --git a/typings/types.d.ts b/typings/types.d.ts index 0c3f0330aa..373dfb5c02 100644 --- a/typings/types.d.ts +++ b/typings/types.d.ts @@ -354,4 +354,7 @@ export declare enum ReplayerEvents { StateChange = "state-change" } export declare type MaskInputFn = (text: string) => string; +export declare type ElementState = { + scroll?: [number, number]; +}; export {}; From bfeafd87052f4dbc2e602644b0177c76222d4ffd Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Fri, 27 Nov 2020 17:40:28 +0800 Subject: [PATCH 3/3] fix: an error occured at applyMutation(remove nodes part) error message: Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node --- src/replay/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/replay/index.ts b/src/replay/index.ts index 672b493938..0bf90f68a0 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -920,6 +920,14 @@ export class Replayer { const realParent = this.fragmentParentMap.get(parent); if (realParent && realParent.contains(target)) { realParent.removeChild(target); + } else if (this.fragmentParentMap.has(target)) { + /** + * the target itself is a fragment document and it's not in the dom + * so we should remove the real target from its parent + */ + const realTarget = this.fragmentParentMap.get(target)!; + parent.removeChild(realTarget); + this.fragmentParentMap.delete(target); } else { parent.removeChild(target); }