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
replayer.pause(5000);
// destroy the replayer (hint: this operation is irreversible)
replayer.destroy();
```
#### Options
@@ -385,6 +388,7 @@ The event list:
| mouse-interaction | mouse interaction has been replayed | { type, target } |
| event-cast | 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.

View File

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

View File

@@ -322,7 +322,7 @@ export class Replayer {
this.rebuildFullSnapshot(
firstFullsnapshot as fullSnapshotEvent & { timestamp: number },
);
this.iframe.contentWindow!.scrollTo(
this.iframe.contentWindow?.scrollTo(
(firstFullsnapshot as fullSnapshotEvent).data.initialOffset,
);
}, 1);
@@ -441,12 +441,22 @@ export class Replayer {
public resume(timeOffset = 0) {
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.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) {
this.service.send({ type: 'TO_LIVE', payload: { baselineTime } });
}
@@ -590,7 +600,7 @@ export class Replayer {
this.firstFullSnapshot = true;
}
this.rebuildFullSnapshot(event, isSync);
this.iframe.contentWindow!.scrollTo(event.data.initialOffset);
this.iframe.contentWindow?.scrollTo(event.data.initialOffset);
};
break;
case EventType.IncrementalSnapshot:
@@ -611,6 +621,7 @@ export class Replayer {
}
if (this.isUserInteraction(_event)) {
if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
_event.delay! - event.delay! >
SKIP_TIME_THRESHOLD *
this.speedService.state.context.timer.speed
@@ -622,6 +633,7 @@ export class Replayer {
}
if (this.nextUserInteractionEvent) {
const skipTime =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.nextUserInteractionEvent.delay! - event.delay!;
const payload = {
speed: Math.min(
@@ -742,7 +754,7 @@ export class Replayer {
styleEl,
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++) {
// push virtual styles
styleEl.rules.push({
@@ -753,12 +765,12 @@ export class Replayer {
}
} else {
const styleEl = document.createElement('style');
(documentElement as HTMLElement)!.insertBefore(
(documentElement as HTMLElement).insertBefore(
styleEl,
head as HTMLHeadElement,
);
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() {
//
},
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
delay: e.delay! - d.positions[0]?.timeOffset,
});
}
@@ -1708,14 +1721,14 @@ export class Replayer {
}
const sn = this.mirror.getMeta(target);
if (target === this.iframe.contentDocument) {
this.iframe.contentWindow!.scrollTo({
this.iframe.contentWindow?.scrollTo({
top: d.y,
left: d.x,
behavior: isSync ? 'auto' : 'smooth',
});
} else if (sn?.type === NodeType.Document) {
// nest iframe content document
(target as Document).defaultView!.scrollTo({
(target as Document).defaultView?.scrollTo({
top: d.y,
left: d.x,
behavior: isSync ? 'auto' : 'smooth',

View File

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

View File

@@ -701,4 +701,21 @@ describe('replayer', function () {
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();
});
});