use raf to impl a more accurate timer and replay events async
This commit is contained in:
@@ -1,62 +1,131 @@
|
|||||||
import { rebuild } from 'rrweb-snapshot';
|
import { rebuild } from 'rrweb-snapshot';
|
||||||
import { mirror } from '../utils';
|
import { mirror } from '../utils';
|
||||||
import { event, EventType, incrementalData, IncrementalSource } from '../types';
|
import later from './timer';
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
incrementalData,
|
||||||
|
IncrementalSource,
|
||||||
|
fullSnapshotEvent,
|
||||||
|
eventWithTime,
|
||||||
|
} from '../types';
|
||||||
import eventsStr from './events';
|
import eventsStr from './events';
|
||||||
|
|
||||||
const events: event[] = JSON.parse(eventsStr);
|
const _events: eventWithTime[] = JSON.parse(eventsStr);
|
||||||
|
|
||||||
function applyIncremental(d: incrementalData) {
|
class Replayer {
|
||||||
switch (d.source) {
|
private events: eventWithTime[] = [];
|
||||||
case IncrementalSource.Mutation:
|
private wrapper: HTMLDivElement;
|
||||||
case IncrementalSource.MouseMove:
|
private iframe: HTMLIFrameElement;
|
||||||
case IncrementalSource.MouseInteraction:
|
private mouse: HTMLDivElement;
|
||||||
break;
|
private startTime: number = 0;
|
||||||
case IncrementalSource.Scroll:
|
|
||||||
// TODO: maybe element
|
constructor(events: eventWithTime[]) {
|
||||||
window.scrollTo({
|
this.events = events;
|
||||||
top: d.y,
|
|
||||||
left: d.x,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case IncrementalSource.ViewportResize:
|
|
||||||
case IncrementalSource.Input:
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function replay() {
|
public play() {
|
||||||
const iframe = document.createElement('iframe');
|
this.setupDom();
|
||||||
for (const event of events) {
|
for (const event of this.events) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case EventType.DomContentLoaded:
|
case EventType.DomContentLoaded:
|
||||||
|
this.startTime = event.timestamp;
|
||||||
|
break;
|
||||||
|
case EventType.Load:
|
||||||
|
this.iframe.width = `${event.data.width}px`;
|
||||||
|
this.iframe.height = `${event.data.height}px`;
|
||||||
|
break;
|
||||||
|
case EventType.FullSnapshot:
|
||||||
|
later(() => {
|
||||||
|
this.rebuildFullSnapshot(event);
|
||||||
|
}, this.getDelay(event));
|
||||||
|
break;
|
||||||
|
case EventType.IncrementalSnapshot:
|
||||||
|
later(() => {
|
||||||
|
this.applyIncremental(event.data);
|
||||||
|
}, this.getDelay(event));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupDom() {
|
||||||
|
this.wrapper = document.createElement('div');
|
||||||
|
this.wrapper.classList.add('replayer-wrapper');
|
||||||
|
document.body.appendChild(this.wrapper);
|
||||||
|
|
||||||
|
this.mouse = document.createElement('div');
|
||||||
|
this.mouse.classList.add('replayer-mouse');
|
||||||
|
this.wrapper.appendChild(this.mouse);
|
||||||
|
|
||||||
|
this.iframe = document.createElement('iframe');
|
||||||
|
this.wrapper.appendChild(this.iframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDelay(event: eventWithTime): number {
|
||||||
|
if (
|
||||||
|
event.type === EventType.IncrementalSnapshot &&
|
||||||
|
event.data.source === IncrementalSource.MouseMove
|
||||||
|
) {
|
||||||
|
const firstOffset = event.data.positions[0].timeOffset;
|
||||||
|
// timeoffset is a negative offset to event.timestamp
|
||||||
|
const firstTimestamp = event.timestamp + firstOffset;
|
||||||
|
event.data.positions = event.data.positions.map(p => {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
timeOffset: p.timeOffset - firstOffset,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return firstTimestamp - this.startTime;
|
||||||
|
}
|
||||||
|
return event.timestamp - this.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuildFullSnapshot(event: fullSnapshotEvent) {
|
||||||
|
const [doc, map] = rebuild(event.data.node);
|
||||||
|
mirror.map = map;
|
||||||
|
if (doc) {
|
||||||
|
this.iframe.contentDocument!.open();
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
||||||
|
this.iframe.contentDocument!.write(
|
||||||
|
(doc as Document).documentElement.outerHTML
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>'),
|
||||||
|
);
|
||||||
|
this.iframe.contentDocument!.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyIncremental(d: incrementalData) {
|
||||||
|
switch (d.source) {
|
||||||
|
case IncrementalSource.Mutation:
|
||||||
break;
|
break;
|
||||||
case EventType.Load:
|
case IncrementalSource.MouseMove:
|
||||||
iframe.width = `${event.data.width}px`;
|
d.positions.forEach(p => {
|
||||||
iframe.height = `${event.data.height}px`;
|
later(() => {
|
||||||
|
this.mouse.style.left = `${p.x}px`;
|
||||||
|
this.mouse.style.top = `${p.y}px`;
|
||||||
|
}, p.timeOffset);
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case EventType.FullSnapshot:
|
case IncrementalSource.MouseInteraction:
|
||||||
const [doc, map] = rebuild(event.data.node);
|
|
||||||
mirror.map = map;
|
|
||||||
if (doc) {
|
|
||||||
document.body.appendChild(iframe);
|
|
||||||
iframe.contentDocument!.open();
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
|
||||||
iframe.contentDocument!.write(
|
|
||||||
(doc as Document).documentElement.outerHTML
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>'),
|
|
||||||
);
|
|
||||||
iframe.contentDocument!.close();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case EventType.IncrementalSnapshot:
|
case IncrementalSource.Scroll:
|
||||||
applyIncremental(event.data);
|
// TODO: maybe element
|
||||||
|
this.iframe.contentWindow!.scrollTo({
|
||||||
|
top: d.y,
|
||||||
|
left: d.x,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
case IncrementalSource.ViewportResize:
|
||||||
|
case IncrementalSource.Input:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default replay;
|
const replayer = new Replayer(_events);
|
||||||
|
|
||||||
|
export default replayer;
|
||||||
|
|||||||
10
src/replay/styles/style.css
Normal file
10
src/replay/styles/style.css
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.replayer-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.replayer-mouse {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: thistle;
|
||||||
|
}
|
||||||
17
src/replay/timer.ts
Normal file
17
src/replay/timer.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const FRAME_MS = 16;
|
||||||
|
|
||||||
|
function later(cb: () => void, delayMs: number) {
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
function check(step: number) {
|
||||||
|
if (step - now > delayMs - FRAME_MS) {
|
||||||
|
cb();
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default later;
|
||||||
Reference in New Issue
Block a user