diff --git a/guide.md b/guide.md index 1c52dcb8c0..54e9f9f94e 100644 --- a/guide.md +++ b/guide.md @@ -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. diff --git a/guide.zh_CN.md b/guide.zh_CN.md index 054fcb8b12..d69f5c77c6 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -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 个额外的事件: diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index df76ca9eeb..408d327d06 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -314,7 +314,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); @@ -433,12 +433,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 } }); } @@ -582,7 +592,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: @@ -603,6 +613,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 @@ -614,6 +625,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( @@ -734,7 +746,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({ @@ -745,12 +757,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); } } } @@ -984,6 +996,7 @@ export class Replayer { doAction() { // }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion delay: e.delay! - d.positions[0]?.timeOffset, }); } @@ -1721,14 +1734,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', diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index d9a14817cf..7a1779147c 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -743,6 +743,7 @@ export enum ReplayerEvents { Flush = 'flush', StateChange = 'state-change', PlayBack = 'play-back', + Destroy = 'destroy', } export type KeepIframeSrcFn = (src: string) => boolean; diff --git a/packages/rrweb/test/replayer.test.ts b/packages/rrweb/test/replayer.test.ts index e011c532dd..ee2ae63497 100644 --- a/packages/rrweb/test/replayer.test.ts +++ b/packages/rrweb/test/replayer.test.ts @@ -687,4 +687,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(); + }); });