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:
@@ -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.
|
||||||
|
|||||||
16
src/utils.ts
16
src/utils.ts
@@ -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>(
|
||||||
|
|||||||
Reference in New Issue
Block a user