diff --git a/README.md b/README.md index 05ebcd4f..2cd3cecb 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ const elementList: [HTMLElement, string][] = [ 用户在网页中移动鼠标会产生很多`mouseMove`事件,通过 `const {x,y} = event.target` 获取到了轨迹的坐标与时间戳 假如我在页面上用鼠标划过一个💖的轨迹,可能会得到下图这样的坐标点 -![heart1](./heart1.png) +![heart1](./assets/heart1.png) 但是对于录屏这个业务场景来说,大部分场合我们并不要求100%还原精确的鼠标轨迹,我门只会关心两种情况: ``` @@ -144,7 +144,7 @@ const elementList: [HTMLElement, string][] = [ 那么通过这个两个策略对鼠标轨迹进行精简后,画一个💖大约只需要6个点,通过样条曲线来模拟鼠标的虚拟轨迹,当 t = 0.2 的时候,就可以得到一个下图这样带着弧度的轨迹了 -![heart2](./heart2.png) +![heart2](./assets/heart2.png) // TODO DETAIL @@ -154,7 +154,7 @@ const elementList: [HTMLElement, string][] = [ 这里需要注意的地方是当页面切换的时候我们需要重置热力图,如果是单页应用,通过 `History` 的 `popstate` 与 `hashchange` 可以监听页面的变化 -![heatmap](./heatmap.png) +![heatmap](./assets/heatmap.png) ##### 对于用户隐私的脱敏 diff --git a/heart1.png b/assets/heart1.png similarity index 100% rename from heart1.png rename to assets/heart1.png diff --git a/heart2.png b/assets/heart2.png similarity index 100% rename from heart2.png rename to assets/heart2.png diff --git a/heatmap.png b/assets/heatmap.png similarity index 100% rename from heatmap.png rename to assets/heatmap.png diff --git a/assets/template.html b/assets/template.html new file mode 100644 index 00000000..6dab556a --- /dev/null +++ b/assets/template.html @@ -0,0 +1,19 @@ + + + + + + + Web Replay + + + + + + + + + diff --git a/examples/test.html b/examples/test.html index a473abd4..19e5efec 100644 --- a/examples/test.html +++ b/examples/test.html @@ -5,8 +5,6 @@ WebReplay - - `, 'text/html').head.firstChild as HTMLElement return style } - - renderHTML() { - const html = convertVNode(this.vNode, null) - if (html) { - this.convertHTMLElement = html as HTMLElement - } - } } diff --git a/packages/player/src/index.ts b/packages/player/src/index.ts index f954228d..05673d30 100644 --- a/packages/player/src/index.ts +++ b/packages/player/src/index.ts @@ -6,12 +6,6 @@ export async function replay() { const indexDB = await DBPromise const { width, height, vNode, data } = await indexDB.getData() - document.documentElement.innerHTML = '' - - Array.from(listenerStore.entries()).forEach(([name, handle]) => { - document.removeEventListener(name, handle) - }) - const contain = new Container({ vNode, width, diff --git a/packages/record/src/record.ts b/packages/record/src/record.ts index f030919f..348b57bd 100644 --- a/packages/record/src/record.ts +++ b/packages/record/src/record.ts @@ -1,12 +1,20 @@ -import { snapshot, SnapshotData } from '@WebReplay/snapshot' +import { snapshots, SnapshotData } from '@WebReplay/snapshot' import { RecordOptions } from './types' +import { listenerStore } from '@WebReplay/utils' + +const ctrl = { + uninstall: () => { + Array.from(listenerStore.values()).forEach(un => un()) + } +} export const record = ({ emitter }: RecordOptions = {}) => { recordAll(emitter) + return ctrl } function recordAll(emitter?: (e: SnapshotData) => void) { - const recordTasks: Function[] = [...Object.values(snapshot)] + const recordTasks: Function[] = [...Object.values(snapshots)] recordTasks.forEach(task => { task(emitter) diff --git a/packages/snapshot/src/snapshot.ts b/packages/snapshot/src/snapshot.ts index 65db7878..9b9fdd79 100644 --- a/packages/snapshot/src/snapshot.ts +++ b/packages/snapshot/src/snapshot.ts @@ -62,8 +62,12 @@ function mouseObserve(emit: SnapshotEvent) { const listenerHandle = throttle(evt, 100, { trailing: true }) - listenerStore.set(name, listenerHandle) + document.addEventListener(name, listenerHandle) + + listenerStore.add(() => { + document.removeEventListener(name, listenerHandle) + }) } function mouseClick() { @@ -82,7 +86,9 @@ function mouseObserve(emit: SnapshotEvent) { const name = 'click' const listenerHandle = throttle(evt, 250) - listenerStore.set(name, listenerHandle) + listenerStore.add(() => { + document.removeEventListener(name, listenerHandle) + }) document.addEventListener(name, listenerHandle) } @@ -119,7 +125,6 @@ function DOMObserve(emit: SnapshotEvent) { break case 'characterData': const parent = target.parentNode! - joinData({ parentId: nodeStore.getNodeId(parent), value: target.nodeValue, @@ -193,49 +198,77 @@ function DOMObserve(emit: SnapshotEvent) { childList: true, subtree: true }) + + listenerStore.add(() => { + observer.disconnect() + }) } function formElementObserve(emit: SnapshotEvent) { - const els = nodeStore.getAllInputs() + listenInputs(emit) + kidnapInputs(emit) // for sys write in input +} - listenInput(emit) // for sys write in input +function listenInputs(emit: SnapshotEvent) { + const eventTypes = ['input', 'change', 'focus', 'blur'] - els.forEach(el => { - el.addEventListener('input', (e: InputEvent) => { - emit({ - type: SnapshotType.FORM_EL_UPDATE, - data: { - type: FormElementEvent.INPUT, - id: nodeStore.getNodeId(e.target as Node)!, - value: (e.target as HTMLInputElement).value - }, - time: Date.now().toString() - }) - }) - el.addEventListener('focus', (e: InputEvent) => { - emit({ - type: SnapshotType.FORM_EL_UPDATE, - data: { - type: FormElementEvent.FOCUS, - id: nodeStore.getNodeId(e.target as Node)! - }, - time: Date.now().toString() - }) + eventTypes + .map(type => { + return (fn: (e: InputEvent) => void) => { + document.addEventListener(type, fn, { passive: true, capture: true, once: false }) + } }) - el.addEventListener('blur', (e: InputEvent) => { - emit({ - type: SnapshotType.FORM_EL_UPDATE, - data: { - type: FormElementEvent.BLUR, - id: nodeStore.getNodeId(e.target as Node)! - }, - time: Date.now().toString() - }) + .forEach(handle => handle(handleFn)) + + listenerStore.add(() => { + eventTypes.forEach(type => { + document.removeEventListener(type, handleFn) }) }) + + function handleFn(e: InputEvent) { + const eventType = e.type + + switch (eventType) { + case 'input': + case 'change': + emit({ + type: SnapshotType.FORM_EL_UPDATE, + data: { + type: FormElementEvent.INPUT, + id: nodeStore.getNodeId(e.target as Node)!, + value: (e.target as HTMLInputElement).value + }, + time: Date.now().toString() + }) + break + case 'focus': + emit({ + type: SnapshotType.FORM_EL_UPDATE, + data: { + type: FormElementEvent.FOCUS, + id: nodeStore.getNodeId(e.target as Node)! + }, + time: Date.now().toString() + }) + break + case 'blur': + emit({ + type: SnapshotType.FORM_EL_UPDATE, + data: { + type: FormElementEvent.BLUR, + id: nodeStore.getNodeId(e.target as Node)! + }, + time: Date.now().toString() + }) + break + default: + break + } + } } -function listenInput(emit: SnapshotEvent) { +function kidnapInputs(emit: SnapshotEvent) { const elementList: [HTMLElement, string][] = [ [HTMLInputElement.prototype, 'value'], [HTMLInputElement.prototype, 'checked'], @@ -243,21 +276,25 @@ function listenInput(emit: SnapshotEvent) { [HTMLTextAreaElement.prototype, 'value'] ] - elementList.forEach(item => { - const [target, key] = item - const original = Object.getOwnPropertyDescriptor(target, key) - Object.defineProperty(target, key, { - set: function(value: string | boolean) { - setTimeout(() => { - handleEvent.call(this, key, value) - }) - if (original && original.set) { - original.set.call(this, value) + const handles = elementList.map(item => { + return () => { + const [target, key] = item + const original = Object.getOwnPropertyDescriptor(target, key) + Object.defineProperty(target, key, { + set: function(value: string | boolean) { + setTimeout(() => { + handleEvent.call(this, key, value) + }) + if (original && original.set) { + original.set.call(this, value) + } } - } - }) + }) + } }) + handles.concat([]).forEach(handle => handle()) + function handleEvent(this: HTMLElement, key: string, value: string) { emit({ type: SnapshotType.FORM_EL_UPDATE, @@ -272,7 +309,7 @@ function listenInput(emit: SnapshotEvent) { } } -export const snapshot = { +export const snapshots = { windowSnapshot, DOMSnapshot, mouseObserve, diff --git a/packages/snapshot/src/types.ts b/packages/snapshot/src/types.ts index 8cb3922b..c242e6c7 100644 --- a/packages/snapshot/src/types.ts +++ b/packages/snapshot/src/types.ts @@ -11,6 +11,7 @@ export enum SnapshotType { export enum FormElementEvent { 'ATTR' = 'ATTR', 'INPUT' = 'INPUT', + 'CHANGE' = 'CHANGE', 'FOCUS' = 'FOCUS', 'BLUR' = 'BLUR' } diff --git a/packages/utils/src/store/data.ts b/packages/utils/src/store/data.ts index e1d24541..df4c948f 100644 --- a/packages/utils/src/store/data.ts +++ b/packages/utils/src/store/data.ts @@ -19,7 +19,7 @@ export class IndexDBOperator { request.onsuccess = e => { this.db = request.result - this.clear() + // this.clear() callback(this.db) } diff --git a/packages/utils/src/store/listener.ts b/packages/utils/src/store/listener.ts index 6c88d9a4..c3caf2de 100644 --- a/packages/utils/src/store/listener.ts +++ b/packages/utils/src/store/listener.ts @@ -1 +1 @@ -export const listenerStore = new Map() +export const listenerStore = new Set() diff --git a/packages/utils/src/store/node.ts b/packages/utils/src/store/node.ts index 1acba604..f18a95ff 100644 --- a/packages/utils/src/store/node.ts +++ b/packages/utils/src/store/node.ts @@ -24,12 +24,6 @@ class NodeStore { return this.idMap.get(node) } - public getAllInputs() { // TODO IMPROVE - return [...this.nodeMap.values()].filter((node: Element) => - ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName) - ) - } - public updateNode(id: number, node: Node) { this.idMap.set(node, id) this.nodeMap.set(id, node) diff --git a/packages/virtual-dom/src/deserialize.ts b/packages/virtual-dom/src/deserialize.ts index fee331c5..d85e338e 100644 --- a/packages/virtual-dom/src/deserialize.ts +++ b/packages/virtual-dom/src/deserialize.ts @@ -20,7 +20,6 @@ export function convertVNode(vNode: VNode | string | null, node: Element | null) function travel(vNode: VNode, node: Element): void { const nodeChildren: Element[] = [] const vNodeChildren = vNode.children.slice() - node.setAttribute('vid', vNode.id.toString()) vNodeChildren.forEach(vChild => { let child = nodeChildren.pop() as Element | null child = convertVNode(vChild, child) diff --git a/rollup.config.dev.js b/rollup.config.dev.js index f19e1b8b..631f429c 100644 --- a/rollup.config.dev.js +++ b/rollup.config.dev.js @@ -10,19 +10,21 @@ import { string } from 'rollup-plugin-string' export default { input: 'index.ts', output: { + name: 'wr', + format: 'esm', file: 'dist/web-replay.js', - format: 'umd', sourcemap: true }, plugins: [ ts(), - node({ - jsnext: true - }), + node(), sourcemaps(), html({ - // template: () => fs.readFileSync('examples/test.html') - template: () => fs.readFileSync('examples/todo.html') + template: () => fs.readFileSync('examples/test.html') + }), + html({ + fileName: 'replay.html', + template: () => fs.readFileSync('assets/template.html') }), string({ include: ['**/*.html', '**/*.css'], diff --git a/rollup.config.prod.js b/rollup.config.prod.js index 2acd31dc..e102707f 100644 --- a/rollup.config.prod.js +++ b/rollup.config.prod.js @@ -21,9 +21,12 @@ export default { }), sourcemaps(), html({ - // template: () => fs.readFileSync('examples/test.html') template: () => fs.readFileSync('examples/todo.html') }), + html({ + fileName: 'replay.html', + template: () => fs.readFileSync('assets/template.html') + }), string({ include: ['**/*.html', '**/*.css'], exclude: ['**/index.html', '**/index.css']