add meta event and fix childList observer, also update related replayer

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent cd0889e9c5
commit 487f1d0c9a
7 changed files with 164 additions and 189 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "rrweb",
"version": "0.3.0",
"version": "0.4.0",
"description": "record and replay the web",
"main": "dist/index.js",
"module": "dist/module.js",
@@ -40,6 +40,6 @@
},
"dependencies": {
"mitt": "^1.1.3",
"rrweb-snapshot": "^0.4.3"
"rrweb-snapshot": "^0.5.2"
}
}

View File

@@ -15,7 +15,7 @@ export default [
file: './dist/record/module.js',
},
{
name: 'record',
name: 'record1',
format: 'iife',
file: './dist/record/browser.js',
},

View File

@@ -27,106 +27,121 @@ function record(options: recordOptions = {}) {
emit(
wrapEvent({
type: EventType.DomContentLoaded,
data: {
href: window.location.href,
},
data: {},
}),
);
});
on(
'load',
() => {
emit(
wrapEvent({
type: EventType.Load,
data: {
width: getWindowWidth(),
height: getWindowHeight(),
const init = () => {
emit(
wrapEvent({
type: EventType.Meta,
data: {
href: window.location.href,
width: getWindowWidth(),
height: getWindowHeight(),
},
}),
);
const [node, idNodeMap] = snapshot(document);
if (!node) {
return console.warn('Failed to snapshot the document');
}
mirror.map = idNodeMap;
emit(
wrapEvent({
type: EventType.FullSnapshot,
data: {
node,
initialOffset: {
left: document.documentElement.scrollLeft,
top: document.documentElement.scrollTop,
},
}),
);
const [node, idNodeMap] = snapshot(document);
if (!node) {
return console.warn('Failed to snapshot the document');
}
mirror.map = idNodeMap;
emit(
wrapEvent({
type: EventType.FullSnapshot,
data: {
node,
initialOffset: {
left: document.documentElement.scrollLeft,
top: document.documentElement.scrollTop,
},
}),
);
initObservers({
mutationCb: m =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
},
}),
);
initObservers({
mutationCb: m =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
),
mousemoveCb: positions =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseMove,
positions,
},
}),
),
mouseInteractionCb: d =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseInteraction,
...d,
},
}),
),
scrollCb: p =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Scroll,
...p,
},
}),
),
viewportResizeCb: d =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.ViewportResize,
...d,
},
}),
),
inputCb: v =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Input,
...v,
},
}),
),
});
},
window,
);
}),
),
mousemoveCb: positions =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseMove,
positions,
},
}),
),
mouseInteractionCb: d =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.MouseInteraction,
...d,
},
}),
),
scrollCb: p =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Scroll,
...p,
},
}),
),
viewportResizeCb: d =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.ViewportResize,
...d,
},
}),
),
inputCb: v =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Input,
...v,
},
}),
),
});
};
if (
document.readyState === 'interactive' ||
document.readyState === 'complete'
) {
init();
} else {
on(
'load',
() => {
emit(
wrapEvent({
type: EventType.Load,
data: {},
}),
);
init();
},
window,
);
}
} catch (error) {
// TODO: handle internal error
console.warn(error);

View File

@@ -74,13 +74,6 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
item.attributes[attributeName!] = value;
}
case 'childList': {
removedNodes.forEach(n => {
removes.push({
parentId: id,
id: mirror.getId(n as INode),
});
mirror.removeNodeFromMap(n as INode);
});
addedNodes.forEach(n => {
adds.push({
parentId: id,
@@ -90,9 +83,15 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
nextId: !nextSibling
? nextSibling
: mirror.getId(nextSibling as INode),
node: serializeNodeWithId(n, document, mirror.map)!,
});
});
removedNodes.forEach(n => {
removes.push({
parentId: id,
id: mirror.getId(n as INode),
});
serializeNodeWithId(n as INode, document, mirror.map);
mirror.removeNodeFromMap(n as INode);
});
break;
}

View File

@@ -1,4 +1,4 @@
import { rebuild, serializeNodeWithId } from 'rrweb-snapshot';
import { rebuild, buildNodeWithSN } from 'rrweb-snapshot';
import * as mittProxy from 'mitt';
import { later, clear } from './timer';
import {
@@ -12,7 +12,7 @@ import {
playerMetaData,
viewportResizeDimention,
} from '../types';
import { mirror, getIdNodeMap } from '../utils';
import { mirror } from '../utils';
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
// tslint:disable-next-line
@@ -82,8 +82,9 @@ export class Replayer {
let castFn: undefined | (() => void);
switch (event.type) {
case EventType.DomContentLoaded:
break;
case EventType.Load:
break;
case EventType.Meta:
castFn = () =>
this.emitter.emit('resize', {
width: event.data.width,
@@ -141,6 +142,7 @@ export class Replayer {
this.timerIds.push(id);
}
// TODO: add speed to mouse move timestamp calculation
private getDelay(event: eventWithTime): number {
// Mouse move events was recorded in a throttle function,
// so we need to find the real timestamp by traverse the time offsets.
@@ -163,29 +165,35 @@ export class Replayer {
}
private rebuildFullSnapshot(event: fullSnapshotEvent) {
const doc = rebuild(event.data.node);
if (doc) {
this.iframe.contentDocument!.open();
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
this.iframe.contentDocument!.write(
new XMLSerializer()
.serializeToString(doc as Document)
.replace(/&/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>'),
);
this.iframe.contentDocument!.close();
mirror.map = getIdNodeMap(this.iframe.contentDocument!);
// avoid form submit to refresh the iframe
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', evt => evt.preventDefault());
});
}
mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1];
// avoid form submit to refresh the iframe
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', evt => evt.preventDefault());
});
}
private applyIncremental(d: incrementalData, isSync: boolean) {
switch (d.source) {
case IncrementalSource.Mutation: {
d.adds.forEach(mutation => {
const target = buildNodeWithSN(
mutation.node,
this.iframe.contentDocument!,
mirror.map,
) as Node;
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
if (mutation.nextId) {
const next = (mirror.getNode(mutation.nextId) as Node) as Element;
parent.insertBefore(target, next);
} else if (mutation.previousId) {
const previous = (mirror.getNode(
mutation.previousId,
) as Node) as Element;
parent.insertBefore(target, previous.nextSibling);
} else {
parent.appendChild(target);
}
});
d.texts.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Text;
target.textContent = mutation.value;
@@ -209,26 +217,6 @@ export class Replayer {
parent.removeChild(target);
delete mirror.map[mutation.id];
});
d.adds.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Element;
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
if (mutation.nextId) {
const next = (mirror.getNode(mutation.nextId) as Node) as Element;
parent.insertBefore(target, next);
} else if (mutation.previousId) {
const previous = (mirror.getNode(
mutation.previousId,
) as Node) as Element;
parent.insertBefore(target, previous.nextSibling);
} else {
parent.appendChild(target);
}
serializeNodeWithId(
mirror.getNode(mutation.id),
this.iframe.contentDocument!,
mirror.map,
);
});
break;
}
case IncrementalSource.MouseMove:

View File

@@ -5,21 +5,17 @@ export enum EventType {
Load,
FullSnapshot,
IncrementalSnapshot,
Meta,
}
export type domContentLoadedEvent = {
type: EventType.DomContentLoaded;
data: {
href: string;
};
data: {};
};
export type loadedEvent = {
type: EventType.Load;
data: {
width: number;
height: number;
};
data: {};
};
export type fullSnapshotEvent = {
@@ -38,6 +34,15 @@ export type incrementalSnapshotEvent = {
data: incrementalData;
};
export type metaEvent = {
type: EventType.Meta;
data: {
href: string;
width: number;
height: number;
};
};
export enum IncrementalSource {
Mutation,
MouseMove,
@@ -85,7 +90,8 @@ export type event =
| domContentLoadedEvent
| loadedEvent
| fullSnapshotEvent
| incrementalSnapshotEvent;
| incrementalSnapshotEvent
| metaEvent;
export type eventWithTime = event & {
timestamp: number;
@@ -125,7 +131,7 @@ export type addedNodeMutation = {
parentId: number;
previousId: number | null;
nextId: number | null;
id: number;
node: serializedNodeWithId;
};
type mutationCallbackParam = {

View File

@@ -1,4 +1,3 @@
import { idNodeMap, NodeType, serializeNodeWithId, resetId } from 'rrweb-snapshot';
import {
Mirror,
throttleOptions,
@@ -30,38 +29,6 @@ export const mirror: Mirror = {
},
};
// TODO: transform this into the snapshot repo
export function getIdNodeMap(doc: Document) {
resetId();
const map: idNodeMap = {};
function walk(n: Node) {
const node = serializeNodeWithId(n, doc, map);
if (!node) {
return null;
}
if (node.type === NodeType.Document || node.type === NodeType.Element) {
let dataStr: string | null = null;
let extraChildIndexes: number[] = [];
if (node.type === NodeType.Element) {
dataStr = (n as Element).getAttribute('data-extra-child-index');
}
if (dataStr) {
extraChildIndexes = JSON.parse(dataStr);
}
n.childNodes.forEach((childNode, index) => {
// skip extra DOM created when rebuild
if (extraChildIndexes.indexOf(index) < 0) {
walk(childNode);
}
});
}
}
walk(doc);
return map;
}
// copy from underscore and modified
export function throttle<T>(
func: (arg: T) => void,