feature: make mirror independent in Replayer (#407)

Co-authored-by: zhaoziqiu <zhaoziqiu@meituan.com>
Co-authored-by: yz-yu <yanzhen@smartx.com>
This commit is contained in:
Ziqiu Zhao
2021-05-22 18:01:11 +08:00
committed by GitHub
parent e71a8d3fc3
commit 3b4ff0e201
2 changed files with 80 additions and 57 deletions

View File

@@ -26,13 +26,14 @@ import {
scrollData, scrollData,
inputData, inputData,
canvasMutationData, canvasMutationData,
Mirror,
ElementState, ElementState,
LogReplayConfig, LogReplayConfig,
logData, logData,
ReplayLogger, ReplayLogger,
} from '../types'; } from '../types';
import { import {
mirror, createMirror,
polyfill, polyfill,
TreeIndex, TreeIndex,
queueToResolveTrees, queueToResolveTrees,
@@ -120,6 +121,7 @@ export class Replayer {
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map(); private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
private mirror: Mirror = createMirror();
/** The first time the player is playing. */ /** The first time the player is playing. */
private firstPlayedEvent: eventWithTime | null = null; private firstPlayedEvent: eventWithTime | null = null;
@@ -178,10 +180,27 @@ export class Replayer {
for (const d of inputMap.values()) { for (const d of inputMap.values()) {
this.applyInput(d); this.applyInput(d);
} }
for (const [frag, parent] of this.fragmentParentMap.entries()) {
this.mirror.map[parent.__sn.id] = parent;
/**
* If we have already set value attribute on textarea,
* then we could not apply text content as default value any more.
*/
if (
parent.__sn.type === NodeType.Element &&
parent.__sn.tagName === 'textarea' &&
frag.textContent
) {
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
}
parent.appendChild(frag);
}
this.fragmentParentMap.clear();
}); });
this.emitter.on(ReplayerEvents.PlayBack, () => { this.emitter.on(ReplayerEvents.PlayBack, () => {
this.firstPlayedEvent = null; this.firstPlayedEvent = null;
mirror.reset(); this.mirror.reset();
}); });
const timer = new Timer([], config?.speed || defaultConfig.speed); const timer = new Timer([], config?.speed || defaultConfig.speed);
@@ -557,7 +576,7 @@ export class Replayer {
} }
this.legacy_missingNodeRetryMap = {}; this.legacy_missingNodeRetryMap = {};
const collected: AppendedIframe[] = []; const collected: AppendedIframe[] = [];
mirror.map = rebuild(event.data.node, { this.mirror.map = rebuild(event.data.node, {
doc: this.iframe.contentDocument, doc: this.iframe.contentDocument,
afterAppend: (builtNode) => { afterAppend: (builtNode) => {
this.collectIframeAndAttachDocument(collected, builtNode); this.collectIframeAndAttachDocument(collected, builtNode);
@@ -629,7 +648,7 @@ export class Replayer {
} }
buildNodeWithSN(mutation.node, { buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument!, doc: iframeEl.contentDocument!,
map: mirror.map, map: this.mirror.map,
hackCss: true, hackCss: true,
skipChild: false, skipChild: false,
afterAppend: (builtNode) => { afterAppend: (builtNode) => {
@@ -810,7 +829,7 @@ export class Replayer {
break; break;
} }
const event = new Event(MouseInteractions[d.type].toLowerCase()); const event = new Event(MouseInteractions[d.type].toLowerCase());
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -893,7 +912,7 @@ export class Replayer {
break; break;
} }
case IncrementalSource.MediaInteraction: { case IncrementalSource.MediaInteraction: {
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -921,7 +940,7 @@ export class Replayer {
break; break;
} }
case IncrementalSource.StyleSheetRule: { case IncrementalSource.StyleSheetRule: {
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -993,7 +1012,7 @@ export class Replayer {
if (!this.config.UNSAFE_replayCanvas) { if (!this.config.UNSAFE_replayCanvas) {
return; return;
} }
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -1061,11 +1080,11 @@ export class Replayer {
private applyMutation(d: mutationData, useVirtualParent: boolean) { private applyMutation(d: mutationData, useVirtualParent: boolean) {
d.removes.forEach((mutation) => { d.removes.forEach((mutation) => {
const target = mirror.getNode(mutation.id); const target = this.mirror.getNode(mutation.id);
if (!target) { if (!target) {
return this.warnNodeNotFound(d, mutation.id); return this.warnNodeNotFound(d, mutation.id);
} }
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId); let parent: INode | null | ShadowRoot = this.mirror.getNode(mutation.parentId);
if (!parent) { if (!parent) {
return this.warnNodeNotFound(d, mutation.parentId); return this.warnNodeNotFound(d, mutation.parentId);
} }
@@ -1073,7 +1092,7 @@ export class Replayer {
parent = parent.shadowRoot; parent = parent.shadowRoot;
} }
// target may be removed with its parents before // target may be removed with its parents before
mirror.removeNodeFromMap(target); this.mirror.removeNodeFromMap(target);
if (parent) { if (parent) {
const realParent = const realParent =
'__sn' in parent ? this.fragmentParentMap.get(parent) : undefined; '__sn' in parent ? this.fragmentParentMap.get(parent) : undefined;
@@ -1100,10 +1119,10 @@ export class Replayer {
const queue: addedNodeMutation[] = []; const queue: addedNodeMutation[] = [];
// next not present at this moment // next not present at this moment
function nextNotInDOM(mutation: addedNodeMutation) { const nextNotInDOM = (mutation: addedNodeMutation) => {
let next: Node | null = null; let next: Node | null = null;
if (mutation.nextId) { if (mutation.nextId) {
next = mirror.getNode(mutation.nextId) as Node; next = this.mirror.getNode(mutation.nextId) as Node;
} }
// next not present at this moment // next not present at this moment
if ( if (
@@ -1115,13 +1134,13 @@ export class Replayer {
return true; return true;
} }
return false; return false;
} };
const appendNode = (mutation: addedNodeMutation) => { const appendNode = (mutation: addedNodeMutation) => {
if (!this.iframe.contentDocument) { if (!this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.'); return console.warn('Looks like your replayer has been destroyed.');
} }
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId); let parent: INode | null | ShadowRoot = this.mirror.getNode(mutation.parentId);
if (!parent) { if (!parent) {
if (mutation.node.type === NodeType.Document) { if (mutation.node.type === NodeType.Document) {
// is newly added document, maybe the document node of an iframe // is newly added document, maybe the document node of an iframe
@@ -1153,7 +1172,7 @@ export class Replayer {
!hasIframeChild !hasIframeChild
) { ) {
const virtualParent = (document.createDocumentFragment() as unknown) as INode; const virtualParent = (document.createDocumentFragment() as unknown) as INode;
mirror.map[mutation.parentId] = virtualParent; this.mirror.map[mutation.parentId] = virtualParent;
this.fragmentParentMap.set(virtualParent, parent); this.fragmentParentMap.set(virtualParent, parent);
// store the state, like scroll position, of child nodes before they are unmounted from dom // store the state, like scroll position, of child nodes before they are unmounted from dom
@@ -1172,21 +1191,21 @@ export class Replayer {
let previous: Node | null = null; let previous: Node | null = null;
let next: Node | null = null; let next: Node | null = null;
if (mutation.previousId) { if (mutation.previousId) {
previous = mirror.getNode(mutation.previousId) as Node; previous = this.mirror.getNode(mutation.previousId) as Node;
} }
if (mutation.nextId) { if (mutation.nextId) {
next = mirror.getNode(mutation.nextId) as Node; next = this.mirror.getNode(mutation.nextId) as Node;
} }
if (nextNotInDOM(mutation)) { if (nextNotInDOM(mutation)) {
return queue.push(mutation); return queue.push(mutation);
} }
if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { if (mutation.node.rootId && !this.mirror.getNode(mutation.node.rootId)) {
return; return;
} }
const targetDoc = mutation.node.rootId const targetDoc = mutation.node.rootId
? mirror.getNode(mutation.node.rootId) ? this.mirror.getNode(mutation.node.rootId)
: this.iframe.contentDocument; : this.iframe.contentDocument;
if (isIframeINode(parent)) { if (isIframeINode(parent)) {
this.attachDocumentToIframe(mutation, parent); this.attachDocumentToIframe(mutation, parent);
@@ -1194,7 +1213,7 @@ export class Replayer {
} }
const target = buildNodeWithSN(mutation.node, { const target = buildNodeWithSN(mutation.node, {
doc: targetDoc as Document, doc: targetDoc as Document,
map: mirror.map, map: this.mirror.map,
skipChild: true, skipChild: true,
hackCss: true, hackCss: true,
}) as INode; }) as INode;
@@ -1272,7 +1291,7 @@ export class Replayer {
break; break;
} }
for (const tree of resolveTrees) { for (const tree of resolveTrees) {
let parent = mirror.getNode(tree.value.parentId); let parent = this.mirror.getNode(tree.value.parentId);
if (!parent) { if (!parent) {
this.debug( this.debug(
'Drop resolve tree since there is no parent for the root node.', 'Drop resolve tree since there is no parent for the root node.',
@@ -1291,7 +1310,7 @@ export class Replayer {
} }
d.texts.forEach((mutation) => { d.texts.forEach((mutation) => {
let target = mirror.getNode(mutation.id); let target = this.mirror.getNode(mutation.id);
if (!target) { if (!target) {
return this.warnNodeNotFound(d, mutation.id); return this.warnNodeNotFound(d, mutation.id);
} }
@@ -1304,7 +1323,7 @@ export class Replayer {
target.textContent = mutation.value; target.textContent = mutation.value;
}); });
d.attributes.forEach((mutation) => { d.attributes.forEach((mutation) => {
let target = mirror.getNode(mutation.id); let target = this.mirror.getNode(mutation.id);
if (!target) { if (!target) {
return this.warnNodeNotFound(d, mutation.id); return this.warnNodeNotFound(d, mutation.id);
} }
@@ -1334,7 +1353,7 @@ export class Replayer {
} }
private applyScroll(d: scrollData) { private applyScroll(d: scrollData) {
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -1358,7 +1377,7 @@ export class Replayer {
} }
private applyInput(d: inputData) { private applyInput(d: inputData) {
const target = mirror.getNode(d.id); const target = this.mirror.getNode(d.id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, d.id); return this.debugNodeNotFound(d, d.id);
} }
@@ -1453,7 +1472,7 @@ export class Replayer {
} }
private moveAndHover(d: incrementalData, x: number, y: number, id: number) { private moveAndHover(d: incrementalData, x: number, y: number, id: number) {
const target = mirror.getNode(id); const target = this.mirror.getNode(id);
if (!target) { if (!target) {
return this.debugNodeNotFound(d, id); return this.debugNodeNotFound(d, id);
} }
@@ -1546,7 +1565,7 @@ export class Replayer {
* @param parent real parent element * @param parent real parent element
*/ */
private restoreRealParent(frag: INode, parent: INode) { private restoreRealParent(frag: INode, parent: INode) {
mirror.map[parent.__sn.id] = parent; this.mirror.map[parent.__sn.id] = parent;
/** /**
* If we have already set value attribute on textarea, * If we have already set value attribute on textarea,
* then we could not apply text content as default value any more. * then we could not apply text content as default value any more.

View File

@@ -34,7 +34,8 @@ export function on(
return () => target.removeEventListener(type, fn, options); return () => target.removeEventListener(type, fn, options);
} }
export const mirror: Mirror = { export function createMirror (): Mirror {
return {
map: {}, map: {},
getId(n) { getId(n) {
// if n is not a serialized INode, use -1 as its id. // if n is not a serialized INode, use -1 as its id.
@@ -44,25 +45,28 @@ export const mirror: Mirror = {
return n.__sn.id; return n.__sn.id;
}, },
getNode(id) { getNode(id) {
return mirror.map[id] || null; return this.map[id] || null;
}, },
// TODO: use a weakmap to get rid of manually memory management // TODO: use a weakmap to get rid of manually memory management
removeNodeFromMap(n) { removeNodeFromMap(n) {
const id = n.__sn && n.__sn.id; const id = n.__sn && n.__sn.id;
delete mirror.map[id]; delete this.map[id];
if (n.childNodes) { if (n.childNodes) {
n.childNodes.forEach((child) => n.childNodes.forEach((child) =>
mirror.removeNodeFromMap((child as Node) as INode), this.removeNodeFromMap((child as Node) as INode),
); );
} }
}, },
has(id) { has(id) {
return mirror.map.hasOwnProperty(id); return this.map.hasOwnProperty(id);
}, },
reset() { reset() {
mirror.map = {}; this.map = {};
}, },
}; };
}
export const mirror: Mirror = createMirror();
// copy from underscore and modified // copy from underscore and modified
export function throttle<T>( export function throttle<T>(