feat: add a destroy function to destroy the whole player (#953)

* feat: add a destroy function to destroy the whole player

* doc: update guidance document

* Update packages/rrweb/src/replay/index.ts

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 8fd9d3cea9
commit 132ed2ade0
5 changed files with 47 additions and 8 deletions

View File

@@ -280,6 +280,9 @@ replayer.pause();
// pause at the fifth seconds // pause at the fifth seconds
replayer.pause(5000); replayer.pause(5000);
// destroy the replayer (hint: this operation is irreversible)
replayer.destroy();
``` ```
#### Options #### Options
@@ -385,6 +388,7 @@ The event list:
| mouse-interaction | mouse interaction has been replayed | { type, target } | | mouse-interaction | mouse interaction has been replayed | { type, target } |
| event-cast | event has been replayed | event | | event-cast | event has been replayed | event |
| custom-event | custom event has been replayed | event | | custom-event | custom event has been replayed | event |
| destroy | destroyed the replayer | - |
The rrweb-replayer also re-expose the event listener via a `component.addEventListener` API. The rrweb-replayer also re-expose the event listener via a `component.addEventListener` API.

View File

@@ -276,6 +276,9 @@ replayer.pause();
// 暂停至第 5 秒处 // 暂停至第 5 秒处
replayer.pause(5000); replayer.pause(5000);
// 销毁播放器 (提示: 这个操作不可逆)
replayer.destroy();
``` ```
#### 配置参数 #### 配置参数
@@ -384,6 +387,7 @@ replayer.on(EVENT_NAME, (payload) => {
| mouse-interaction | 回放鼠标交互事件 | { type, target } | | mouse-interaction | 回放鼠标交互事件 | { type, target } |
| event-cast | 回放 event | event | | event-cast | 回放 event | event |
| custom-event | 回放自定义事件 | event | | custom-event | 回放自定义事件 | event |
| destroy | 销毁播放器 | - |
使用 `rrweb-player` 时,也可以通过 `addEventListener` API 使用相同的事件功能,并且会获得 3 个额外的事件: 使用 `rrweb-player` 时,也可以通过 `addEventListener` API 使用相同的事件功能,并且会获得 3 个额外的事件:

View File

@@ -322,7 +322,7 @@ export class Replayer {
this.rebuildFullSnapshot( this.rebuildFullSnapshot(
firstFullsnapshot as fullSnapshotEvent & { timestamp: number }, firstFullsnapshot as fullSnapshotEvent & { timestamp: number },
); );
this.iframe.contentWindow!.scrollTo( this.iframe.contentWindow?.scrollTo(
(firstFullsnapshot as fullSnapshotEvent).data.initialOffset, (firstFullsnapshot as fullSnapshotEvent).data.initialOffset,
); );
}, 1); }, 1);
@@ -441,12 +441,22 @@ export class Replayer {
public resume(timeOffset = 0) { public resume(timeOffset = 0) {
console.warn( console.warn(
`The 'resume' will be departed in 1.0. Please use 'play' method which has the same interface.`, `The 'resume' was deprecated in 1.0. Please use 'play' method which has the same interface.`,
); );
this.play(timeOffset); this.play(timeOffset);
this.emitter.emit(ReplayerEvents.Resume); this.emitter.emit(ReplayerEvents.Resume);
} }
/**
* Totally destroy this replayer and please be careful that this operation is irreversible.
* Memory occupation can be released by removing all references to this replayer.
*/
public destroy() {
this.pause();
this.config.root.removeChild(this.wrapper);
this.emitter.emit(ReplayerEvents.Destroy);
}
public startLive(baselineTime?: number) { public startLive(baselineTime?: number) {
this.service.send({ type: 'TO_LIVE', payload: { baselineTime } }); this.service.send({ type: 'TO_LIVE', payload: { baselineTime } });
} }
@@ -590,7 +600,7 @@ export class Replayer {
this.firstFullSnapshot = true; this.firstFullSnapshot = true;
} }
this.rebuildFullSnapshot(event, isSync); this.rebuildFullSnapshot(event, isSync);
this.iframe.contentWindow!.scrollTo(event.data.initialOffset); this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
}; };
break; break;
case EventType.IncrementalSnapshot: case EventType.IncrementalSnapshot:
@@ -611,6 +621,7 @@ export class Replayer {
} }
if (this.isUserInteraction(_event)) { if (this.isUserInteraction(_event)) {
if ( if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
_event.delay! - event.delay! > _event.delay! - event.delay! >
SKIP_TIME_THRESHOLD * SKIP_TIME_THRESHOLD *
this.speedService.state.context.timer.speed this.speedService.state.context.timer.speed
@@ -622,6 +633,7 @@ export class Replayer {
} }
if (this.nextUserInteractionEvent) { if (this.nextUserInteractionEvent) {
const skipTime = const skipTime =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.nextUserInteractionEvent.delay! - event.delay!; this.nextUserInteractionEvent.delay! - event.delay!;
const payload = { const payload = {
speed: Math.min( speed: Math.min(
@@ -742,7 +754,7 @@ export class Replayer {
styleEl, styleEl,
getDefaultSN(styleEl, this.virtualDom.unserializedId), getDefaultSN(styleEl, this.virtualDom.unserializedId),
); );
(documentElement as RRElement)!.insertBefore(styleEl, head as RRElement); (documentElement as RRElement).insertBefore(styleEl, head as RRElement);
for (let idx = 0; idx < injectStylesRules.length; idx++) { for (let idx = 0; idx < injectStylesRules.length; idx++) {
// push virtual styles // push virtual styles
styleEl.rules.push({ styleEl.rules.push({
@@ -753,12 +765,12 @@ export class Replayer {
} }
} else { } else {
const styleEl = document.createElement('style'); const styleEl = document.createElement('style');
(documentElement as HTMLElement)!.insertBefore( (documentElement as HTMLElement).insertBefore(
styleEl, styleEl,
head as HTMLHeadElement, head as HTMLHeadElement,
); );
for (let idx = 0; idx < injectStylesRules.length; idx++) { for (let idx = 0; idx < injectStylesRules.length; idx++) {
styleEl.sheet!.insertRule(injectStylesRules[idx], idx); styleEl.sheet?.insertRule(injectStylesRules[idx], idx);
} }
} }
} }
@@ -992,6 +1004,7 @@ export class Replayer {
doAction() { doAction() {
// //
}, },
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
delay: e.delay! - d.positions[0]?.timeOffset, delay: e.delay! - d.positions[0]?.timeOffset,
}); });
} }
@@ -1708,14 +1721,14 @@ export class Replayer {
} }
const sn = this.mirror.getMeta(target); const sn = this.mirror.getMeta(target);
if (target === this.iframe.contentDocument) { if (target === this.iframe.contentDocument) {
this.iframe.contentWindow!.scrollTo({ this.iframe.contentWindow?.scrollTo({
top: d.y, top: d.y,
left: d.x, left: d.x,
behavior: isSync ? 'auto' : 'smooth', behavior: isSync ? 'auto' : 'smooth',
}); });
} else if (sn?.type === NodeType.Document) { } else if (sn?.type === NodeType.Document) {
// nest iframe content document // nest iframe content document
(target as Document).defaultView!.scrollTo({ (target as Document).defaultView?.scrollTo({
top: d.y, top: d.y,
left: d.x, left: d.x,
behavior: isSync ? 'auto' : 'smooth', behavior: isSync ? 'auto' : 'smooth',

View File

@@ -743,6 +743,7 @@ export enum ReplayerEvents {
Flush = 'flush', Flush = 'flush',
StateChange = 'state-change', StateChange = 'state-change',
PlayBack = 'play-back', PlayBack = 'play-back',
Destroy = 'destroy',
} }
export type KeepIframeSrcFn = (src: string) => boolean; export type KeepIframeSrcFn = (src: string) => boolean;

View File

@@ -701,4 +701,21 @@ describe('replayer', function () {
await assertDomSnapshot(page); await assertDomSnapshot(page);
}); });
it('should destroy the replayer after calling destroy()', async () => {
await page.evaluate(`events = ${JSON.stringify(events)}`);
await page.evaluate(`
const { Replayer } = rrweb;
let replayer = new Replayer(events);
replayer.play();
`);
const replayerWrapperClassName = 'replayer-wrapper';
let wrapper = await page.$(`.${replayerWrapperClassName}`);
expect(wrapper).not.toBeNull();
await page.evaluate(`replayer.destroy(); replayer = null;`);
wrapper = await page.$(`.${replayerWrapperClassName}`);
expect(wrapper).toBeNull();
});
}); });