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", "name": "rrweb",
"version": "0.3.0", "version": "0.4.0",
"description": "record and replay the web", "description": "record and replay the web",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/module.js", "module": "dist/module.js",
@@ -40,6 +40,6 @@
}, },
"dependencies": { "dependencies": {
"mitt": "^1.1.3", "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', file: './dist/record/module.js',
}, },
{ {
name: 'record', name: 'record1',
format: 'iife', format: 'iife',
file: './dist/record/browser.js', file: './dist/record/browser.js',
}, },

View File

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

View File

@@ -74,13 +74,6 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
item.attributes[attributeName!] = value; item.attributes[attributeName!] = value;
} }
case 'childList': { case 'childList': {
removedNodes.forEach(n => {
removes.push({
parentId: id,
id: mirror.getId(n as INode),
});
mirror.removeNodeFromMap(n as INode);
});
addedNodes.forEach(n => { addedNodes.forEach(n => {
adds.push({ adds.push({
parentId: id, parentId: id,
@@ -90,9 +83,15 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
nextId: !nextSibling nextId: !nextSibling
? nextSibling ? nextSibling
: mirror.getId(nextSibling as INode), : mirror.getId(nextSibling as INode),
node: serializeNodeWithId(n, document, mirror.map)!,
});
});
removedNodes.forEach(n => {
removes.push({
parentId: id,
id: mirror.getId(n as INode), id: mirror.getId(n as INode),
}); });
serializeNodeWithId(n as INode, document, mirror.map); mirror.removeNodeFromMap(n as INode);
}); });
break; 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 * as mittProxy from 'mitt';
import { later, clear } from './timer'; import { later, clear } from './timer';
import { import {
@@ -12,7 +12,7 @@ import {
playerMetaData, playerMetaData,
viewportResizeDimention, viewportResizeDimention,
} from '../types'; } from '../types';
import { mirror, getIdNodeMap } from '../utils'; import { mirror } from '../utils';
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
// tslint:disable-next-line // tslint:disable-next-line
@@ -82,8 +82,9 @@ export class Replayer {
let castFn: undefined | (() => void); let castFn: undefined | (() => void);
switch (event.type) { switch (event.type) {
case EventType.DomContentLoaded: case EventType.DomContentLoaded:
break;
case EventType.Load: case EventType.Load:
break;
case EventType.Meta:
castFn = () => castFn = () =>
this.emitter.emit('resize', { this.emitter.emit('resize', {
width: event.data.width, width: event.data.width,
@@ -141,6 +142,7 @@ export class Replayer {
this.timerIds.push(id); this.timerIds.push(id);
} }
// TODO: add speed to mouse move timestamp calculation
private getDelay(event: eventWithTime): number { private getDelay(event: eventWithTime): number {
// Mouse move events was recorded in a throttle function, // Mouse move events was recorded in a throttle function,
// so we need to find the real timestamp by traverse the time offsets. // so we need to find the real timestamp by traverse the time offsets.
@@ -163,29 +165,35 @@ export class Replayer {
} }
private rebuildFullSnapshot(event: fullSnapshotEvent) { private rebuildFullSnapshot(event: fullSnapshotEvent) {
const doc = rebuild(event.data.node); mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1];
if (doc) { // avoid form submit to refresh the iframe
this.iframe.contentDocument!.open(); this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML form.addEventListener('submit', evt => evt.preventDefault());
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());
});
}
} }
private applyIncremental(d: incrementalData, isSync: boolean) { private applyIncremental(d: incrementalData, isSync: boolean) {
switch (d.source) { switch (d.source) {
case IncrementalSource.Mutation: { 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 => { d.texts.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Text; const target = (mirror.getNode(mutation.id) as Node) as Text;
target.textContent = mutation.value; target.textContent = mutation.value;
@@ -209,26 +217,6 @@ export class Replayer {
parent.removeChild(target); parent.removeChild(target);
delete mirror.map[mutation.id]; 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; break;
} }
case IncrementalSource.MouseMove: case IncrementalSource.MouseMove:

View File

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

View File

@@ -1,4 +1,3 @@
import { idNodeMap, NodeType, serializeNodeWithId, resetId } from 'rrweb-snapshot';
import { import {
Mirror, Mirror,
throttleOptions, 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 // copy from underscore and modified
export function throttle<T>( export function throttle<T>(
func: (arg: T) => void, func: (arg: T) => void,