From 499d84fc704ab8386f5bd54d66ebf2c229005574 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] use raf to impl a more accurate timer and replay events async --- src/replay/index.ts | 161 +++++++++++++++++++++++++----------- src/replay/styles/style.css | 10 +++ src/replay/timer.ts | 17 ++++ 3 files changed, 142 insertions(+), 46 deletions(-) create mode 100644 src/replay/styles/style.css create mode 100644 src/replay/timer.ts diff --git a/src/replay/index.ts b/src/replay/index.ts index 231d6585..a8d284a4 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -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(/</g, '<') + .replace(/>/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(/&/g, '&') - .replace(/</g, '<') - .replace(/>/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; diff --git a/src/replay/styles/style.css b/src/replay/styles/style.css new file mode 100644 index 00000000..cda05005 --- /dev/null +++ b/src/replay/styles/style.css @@ -0,0 +1,10 @@ +.replayer-wrapper { + position: relative; +} +.replayer-mouse { + position: absolute; + width: 20px; + height: 20px; + border-radius: 10px; + background: thistle; +} diff --git a/src/replay/timer.ts b/src/replay/timer.ts new file mode 100644 index 00000000..2f9d7dd1 --- /dev/null +++ b/src/replay/timer.ts @@ -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;