diff --git a/package.json b/package.json index 296fed78..75f9e54a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rrweb", - "version": "0.3.0", + "version": "0.4.0", "description": "record and replay the web", "main": "dist/index.js", "module": "dist/module.js", @@ -40,6 +40,6 @@ }, "dependencies": { "mitt": "^1.1.3", - "rrweb-snapshot": "^0.4.3" + "rrweb-snapshot": "^0.5.2" } } diff --git a/rollup.config.js b/rollup.config.js index a06d92f9..417c05b6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -15,7 +15,7 @@ export default [ file: './dist/record/module.js', }, { - name: 'record', + name: 'record1', format: 'iife', file: './dist/record/browser.js', }, diff --git a/src/record/index.ts b/src/record/index.ts index e10db65e..9076d168 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -27,106 +27,121 @@ function record(options: recordOptions = {}) { emit( wrapEvent({ type: EventType.DomContentLoaded, - data: { - href: window.location.href, - }, + data: {}, }), ); }); - on( - 'load', - () => { - emit( - wrapEvent({ - type: EventType.Load, - data: { - width: getWindowWidth(), - height: getWindowHeight(), + const init = () => { + emit( + wrapEvent({ + type: EventType.Meta, + data: { + href: window.location.href, + width: getWindowWidth(), + height: getWindowHeight(), + }, + }), + ); + const [node, idNodeMap] = snapshot(document); + if (!node) { + return console.warn('Failed to snapshot the document'); + } + mirror.map = idNodeMap; + emit( + wrapEvent({ + type: EventType.FullSnapshot, + data: { + node, + initialOffset: { + left: document.documentElement.scrollLeft, + top: document.documentElement.scrollTop, }, - }), - ); - const [node, idNodeMap] = snapshot(document); - if (!node) { - return console.warn('Failed to snapshot the document'); - } - mirror.map = idNodeMap; - emit( - wrapEvent({ - type: EventType.FullSnapshot, - data: { - node, - initialOffset: { - left: document.documentElement.scrollLeft, - top: document.documentElement.scrollTop, + }, + }), + ); + initObservers({ + mutationCb: m => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Mutation, + ...m, }, - }, - }), - ); - initObservers({ - mutationCb: m => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.Mutation, - ...m, - }, - }), - ), - mousemoveCb: positions => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.MouseMove, - positions, - }, - }), - ), - mouseInteractionCb: d => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.MouseInteraction, - ...d, - }, - }), - ), - scrollCb: p => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.Scroll, - ...p, - }, - }), - ), - viewportResizeCb: d => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.ViewportResize, - ...d, - }, - }), - ), - inputCb: v => - emit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.Input, - ...v, - }, - }), - ), - }); - }, - window, - ); + }), + ), + mousemoveCb: positions => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseMove, + positions, + }, + }), + ), + mouseInteractionCb: d => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseInteraction, + ...d, + }, + }), + ), + scrollCb: p => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Scroll, + ...p, + }, + }), + ), + viewportResizeCb: d => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.ViewportResize, + ...d, + }, + }), + ), + inputCb: v => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.Input, + ...v, + }, + }), + ), + }); + }; + if ( + document.readyState === 'interactive' || + document.readyState === 'complete' + ) { + init(); + } else { + on( + 'load', + () => { + emit( + wrapEvent({ + type: EventType.Load, + data: {}, + }), + ); + init(); + }, + window, + ); + } } catch (error) { // TODO: handle internal error console.warn(error); diff --git a/src/record/observer.ts b/src/record/observer.ts index 9c33eaa1..d219a08d 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -74,13 +74,6 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { item.attributes[attributeName!] = value; } case 'childList': { - removedNodes.forEach(n => { - removes.push({ - parentId: id, - id: mirror.getId(n as INode), - }); - mirror.removeNodeFromMap(n as INode); - }); addedNodes.forEach(n => { adds.push({ parentId: id, @@ -90,9 +83,15 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { nextId: !nextSibling ? nextSibling : mirror.getId(nextSibling as INode), + node: serializeNodeWithId(n, document, mirror.map)!, + }); + }); + removedNodes.forEach(n => { + removes.push({ + parentId: id, id: mirror.getId(n as INode), }); - serializeNodeWithId(n as INode, document, mirror.map); + mirror.removeNodeFromMap(n as INode); }); break; } diff --git a/src/replay/index.ts b/src/replay/index.ts index 12f9e370..6ec195f3 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -1,4 +1,4 @@ -import { rebuild, serializeNodeWithId } from 'rrweb-snapshot'; +import { rebuild, buildNodeWithSN } from 'rrweb-snapshot'; import * as mittProxy from 'mitt'; import { later, clear } from './timer'; import { @@ -12,7 +12,7 @@ import { playerMetaData, viewportResizeDimention, } from '../types'; -import { mirror, getIdNodeMap } from '../utils'; +import { mirror } from '../utils'; // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 // tslint:disable-next-line @@ -82,8 +82,9 @@ export class Replayer { let castFn: undefined | (() => void); switch (event.type) { case EventType.DomContentLoaded: - break; case EventType.Load: + break; + case EventType.Meta: castFn = () => this.emitter.emit('resize', { width: event.data.width, @@ -141,6 +142,7 @@ export class Replayer { this.timerIds.push(id); } + // TODO: add speed to mouse move timestamp calculation private getDelay(event: eventWithTime): number { // Mouse move events was recorded in a throttle function, // so we need to find the real timestamp by traverse the time offsets. @@ -163,29 +165,35 @@ export class Replayer { } private rebuildFullSnapshot(event: fullSnapshotEvent) { - const doc = rebuild(event.data.node); - if (doc) { - this.iframe.contentDocument!.open(); - // https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML - this.iframe.contentDocument!.write( - new XMLSerializer() - .serializeToString(doc as Document) - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>'), - ); - this.iframe.contentDocument!.close(); - mirror.map = getIdNodeMap(this.iframe.contentDocument!); - // avoid form submit to refresh the iframe - this.iframe.contentDocument!.querySelectorAll('form').forEach(form => { - form.addEventListener('submit', evt => evt.preventDefault()); - }); - } + mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1]; + // avoid form submit to refresh the iframe + this.iframe.contentDocument!.querySelectorAll('form').forEach(form => { + form.addEventListener('submit', evt => evt.preventDefault()); + }); } private applyIncremental(d: incrementalData, isSync: boolean) { switch (d.source) { case IncrementalSource.Mutation: { + d.adds.forEach(mutation => { + const target = buildNodeWithSN( + mutation.node, + this.iframe.contentDocument!, + mirror.map, + ) as Node; + const parent = (mirror.getNode(mutation.parentId) as Node) as Element; + if (mutation.nextId) { + const next = (mirror.getNode(mutation.nextId) as Node) as Element; + parent.insertBefore(target, next); + } else if (mutation.previousId) { + const previous = (mirror.getNode( + mutation.previousId, + ) as Node) as Element; + parent.insertBefore(target, previous.nextSibling); + } else { + parent.appendChild(target); + } + }); d.texts.forEach(mutation => { const target = (mirror.getNode(mutation.id) as Node) as Text; target.textContent = mutation.value; @@ -209,26 +217,6 @@ export class Replayer { parent.removeChild(target); delete mirror.map[mutation.id]; }); - d.adds.forEach(mutation => { - const target = (mirror.getNode(mutation.id) as Node) as Element; - const parent = (mirror.getNode(mutation.parentId) as Node) as Element; - if (mutation.nextId) { - const next = (mirror.getNode(mutation.nextId) as Node) as Element; - parent.insertBefore(target, next); - } else if (mutation.previousId) { - const previous = (mirror.getNode( - mutation.previousId, - ) as Node) as Element; - parent.insertBefore(target, previous.nextSibling); - } else { - parent.appendChild(target); - } - serializeNodeWithId( - mirror.getNode(mutation.id), - this.iframe.contentDocument!, - mirror.map, - ); - }); break; } case IncrementalSource.MouseMove: diff --git a/src/types.ts b/src/types.ts index 517d46d1..35c7c74d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,21 +5,17 @@ export enum EventType { Load, FullSnapshot, IncrementalSnapshot, + Meta, } export type domContentLoadedEvent = { type: EventType.DomContentLoaded; - data: { - href: string; - }; + data: {}; }; export type loadedEvent = { type: EventType.Load; - data: { - width: number; - height: number; - }; + data: {}; }; export type fullSnapshotEvent = { @@ -38,6 +34,15 @@ export type incrementalSnapshotEvent = { data: incrementalData; }; +export type metaEvent = { + type: EventType.Meta; + data: { + href: string; + width: number; + height: number; + }; +}; + export enum IncrementalSource { Mutation, MouseMove, @@ -85,7 +90,8 @@ export type event = | domContentLoadedEvent | loadedEvent | fullSnapshotEvent - | incrementalSnapshotEvent; + | incrementalSnapshotEvent + | metaEvent; export type eventWithTime = event & { timestamp: number; @@ -125,7 +131,7 @@ export type addedNodeMutation = { parentId: number; previousId: number | null; nextId: number | null; - id: number; + node: serializedNodeWithId; }; type mutationCallbackParam = { diff --git a/src/utils.ts b/src/utils.ts index 2b1ee979..0d1e4b18 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ -import { idNodeMap, NodeType, serializeNodeWithId, resetId } from 'rrweb-snapshot'; import { Mirror, throttleOptions, @@ -30,38 +29,6 @@ export const mirror: Mirror = { }, }; -// TODO: transform this into the snapshot repo -export function getIdNodeMap(doc: Document) { - resetId(); - const map: idNodeMap = {}; - - function walk(n: Node) { - const node = serializeNodeWithId(n, doc, map); - if (!node) { - return null; - } - if (node.type === NodeType.Document || node.type === NodeType.Element) { - let dataStr: string | null = null; - let extraChildIndexes: number[] = []; - if (node.type === NodeType.Element) { - dataStr = (n as Element).getAttribute('data-extra-child-index'); - } - if (dataStr) { - extraChildIndexes = JSON.parse(dataStr); - } - n.childNodes.forEach((childNode, index) => { - // skip extra DOM created when rebuild - if (extraChildIndexes.indexOf(index) < 0) { - walk(childNode); - } - }); - } - } - - walk(doc); - return map; -} - // copy from underscore and modified export function throttle( func: (arg: T) => void,