use raf to impl a more accurate timer and replay events async

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 9fda4c0809
commit 499d84fc70
3 changed files with 142 additions and 46 deletions

View File

@@ -1,62 +1,131 @@
import { rebuild } from 'rrweb-snapshot';
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';
const events: event[] = JSON.parse(eventsStr);
const _events: eventWithTime[] = JSON.parse(eventsStr);
function applyIncremental(d: incrementalData) {
switch (d.source) {
case IncrementalSource.Mutation:
case IncrementalSource.MouseMove:
case IncrementalSource.MouseInteraction:
break;
case IncrementalSource.Scroll:
// TODO: maybe element
window.scrollTo({
top: d.y,
left: d.x,
behavior: 'smooth',
});
break;
case IncrementalSource.ViewportResize:
case IncrementalSource.Input:
default:
class Replayer {
private events: eventWithTime[] = [];
private wrapper: HTMLDivElement;
private iframe: HTMLIFrameElement;
private mouse: HTMLDivElement;
private startTime: number = 0;
constructor(events: eventWithTime[]) {
this.events = events;
}
}
function replay() {
const iframe = document.createElement('iframe');
for (const event of events) {
switch (event.type) {
case EventType.DomContentLoaded:
public play() {
this.setupDom();
for (const event of this.events) {
switch (event.type) {
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(/&lt;/g, '<')
.replace(/&gt;/g, '>'),
);
this.iframe.contentDocument!.close();
}
}
private applyIncremental(d: incrementalData) {
switch (d.source) {
case IncrementalSource.Mutation:
break;
case EventType.Load:
iframe.width = `${event.data.width}px`;
iframe.height = `${event.data.height}px`;
case IncrementalSource.MouseMove:
d.positions.forEach(p => {
later(() => {
this.mouse.style.left = `${p.x}px`;
this.mouse.style.top = `${p.y}px`;
}, p.timeOffset);
});
break;
case EventType.FullSnapshot:
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(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>'),
);
iframe.contentDocument!.close();
}
case IncrementalSource.MouseInteraction:
break;
case EventType.IncrementalSnapshot:
applyIncremental(event.data);
case IncrementalSource.Scroll:
// TODO: maybe element
this.iframe.contentWindow!.scrollTo({
top: d.y,
left: d.x,
behavior: 'smooth',
});
break;
case IncrementalSource.ViewportResize:
case IncrementalSource.Input:
default:
}
}
}
export default replay;
const replayer = new Replayer(_events);
export default replayer;

View 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
View 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;